feat: phase 1 of python scripting
This commit is contained in:
@@ -151,6 +151,24 @@ export const importDefinitions = sqliteTable('import_definitions', {
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
|
||||
// Scripts table - stores metadata for Python scripts persisted in scripts/*.py
|
||||
export const scripts = sqliteTable('scripts', {
|
||||
id: text('id').primaryKey(),
|
||||
projectId: text('project_id').notNull(),
|
||||
slug: text('slug').notNull(),
|
||||
title: text('title').notNull(),
|
||||
kind: text('kind', { enum: ['macro', 'utility', 'transform'] }).notNull().default('utility'),
|
||||
entrypoint: text('entrypoint').notNull().default('render'),
|
||||
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
||||
version: integer('version').notNull().default(1),
|
||||
filePath: text('file_path').notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
}, (table) => ({
|
||||
// Composite unique index: slug must be unique within each project
|
||||
projectSlugIdx: uniqueIndex('scripts_project_slug_idx').on(table.projectId, table.slug),
|
||||
}));
|
||||
|
||||
// Types for TypeScript
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
export type NewProject = typeof projects.$inferInsert;
|
||||
@@ -174,3 +192,5 @@ export type ChatMessage = typeof chatMessages.$inferSelect;
|
||||
export type NewChatMessage = typeof chatMessages.$inferInsert;
|
||||
export type ImportDefinition = typeof importDefinitions.$inferSelect;
|
||||
export type NewImportDefinition = typeof importDefinitions.$inferInsert;
|
||||
export type Script = typeof scripts.$inferSelect;
|
||||
export type NewScript = typeof scripts.$inferInsert;
|
||||
|
||||
264
src/main/engine/ScriptEngine.ts
Normal file
264
src/main/engine/ScriptEngine.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { app } from 'electron';
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
import { getDatabase } from '../database';
|
||||
import { scripts, type NewScript, type Script } from '../database/schema';
|
||||
|
||||
export type ScriptKind = 'macro' | 'utility' | 'transform';
|
||||
|
||||
export interface ScriptData {
|
||||
id: string;
|
||||
projectId: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
kind: ScriptKind;
|
||||
entrypoint: string;
|
||||
enabled: boolean;
|
||||
version: number;
|
||||
filePath: string;
|
||||
content: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateScriptInput {
|
||||
title: string;
|
||||
kind: ScriptKind;
|
||||
content: string;
|
||||
slug?: string;
|
||||
entrypoint?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateScriptInput {
|
||||
title?: string;
|
||||
kind?: ScriptKind;
|
||||
content?: string;
|
||||
slug?: string;
|
||||
entrypoint?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export class ScriptEngine extends EventEmitter {
|
||||
private currentProjectId = 'default';
|
||||
private dataDir: string | null = null;
|
||||
|
||||
setProjectContext(projectId: string, dataDir?: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
this.dataDir = dataDir || null;
|
||||
}
|
||||
|
||||
getProjectContext(): string {
|
||||
return this.currentProjectId;
|
||||
}
|
||||
|
||||
async createScript(input: CreateScriptInput): Promise<ScriptData> {
|
||||
const now = new Date();
|
||||
const allScripts = await this.getAllScriptRows();
|
||||
const desiredSlug = this.normalizeSlug(input.slug || input.title || 'script');
|
||||
const uniqueSlug = this.ensureUniqueSlug(desiredSlug, allScripts);
|
||||
const scriptId = uuidv4();
|
||||
const filePath = this.getScriptFilePath(uniqueSlug);
|
||||
|
||||
await fs.mkdir(this.getScriptsDir(), { recursive: true });
|
||||
await fs.writeFile(filePath, input.content, 'utf-8');
|
||||
|
||||
const row: NewScript = {
|
||||
id: scriptId,
|
||||
projectId: this.currentProjectId,
|
||||
slug: uniqueSlug,
|
||||
title: input.title,
|
||||
kind: input.kind,
|
||||
entrypoint: input.entrypoint || 'render',
|
||||
enabled: input.enabled ?? true,
|
||||
version: 1,
|
||||
filePath,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await getDatabase().getLocal().insert(scripts).values(row);
|
||||
|
||||
const created = await this.toScriptData(row as Script);
|
||||
this.emit('scriptCreated', created);
|
||||
return created;
|
||||
}
|
||||
|
||||
async updateScript(id: string, updates: UpdateScriptInput): Promise<ScriptData | null> {
|
||||
const existing = await this.getScriptRow(id);
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allScripts = await this.getAllScriptRows();
|
||||
const desiredSlug = this.normalizeSlug(updates.slug || updates.title || existing.slug);
|
||||
const nextSlug = this.ensureUniqueSlug(desiredSlug, allScripts, existing.id);
|
||||
const nextFilePath = this.getScriptFilePath(nextSlug);
|
||||
const now = new Date();
|
||||
|
||||
if (existing.filePath !== nextFilePath) {
|
||||
await fs.mkdir(this.getScriptsDir(), { recursive: true });
|
||||
await fs.rename(existing.filePath, nextFilePath);
|
||||
}
|
||||
|
||||
if (typeof updates.content === 'string') {
|
||||
await fs.writeFile(nextFilePath, updates.content, 'utf-8');
|
||||
}
|
||||
|
||||
await getDatabase().getLocal()
|
||||
.update(scripts)
|
||||
.set({
|
||||
title: updates.title ?? existing.title,
|
||||
slug: nextSlug,
|
||||
kind: updates.kind ?? existing.kind,
|
||||
entrypoint: updates.entrypoint ?? existing.entrypoint,
|
||||
enabled: updates.enabled ?? existing.enabled,
|
||||
filePath: nextFilePath,
|
||||
version: existing.version + 1,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(and(eq(scripts.id, existing.id), eq(scripts.projectId, this.currentProjectId)));
|
||||
|
||||
const updatedRow = await this.getScriptRow(existing.id);
|
||||
if (!updatedRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updated = await this.toScriptData(updatedRow);
|
||||
this.emit('scriptUpdated', updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteScript(id: string): Promise<boolean> {
|
||||
const existing = await this.getScriptRow(id);
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await getDatabase().getLocal()
|
||||
.delete(scripts)
|
||||
.where(and(eq(scripts.id, existing.id), eq(scripts.projectId, this.currentProjectId)));
|
||||
|
||||
try {
|
||||
await fs.unlink(existing.filePath);
|
||||
} catch (error) {
|
||||
const fsError = error as NodeJS.ErrnoException;
|
||||
if (fsError.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('scriptDeleted', id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async getScript(id: string): Promise<ScriptData | null> {
|
||||
const row = await this.getScriptRow(id);
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return this.toScriptData(row);
|
||||
}
|
||||
|
||||
async getAllScripts(): Promise<ScriptData[]> {
|
||||
const rows = await this.getAllScriptRows();
|
||||
return Promise.all(rows.map((item) => this.toScriptData(item)));
|
||||
}
|
||||
|
||||
private async getScriptRow(id: string): Promise<Script | null> {
|
||||
const rows = await this.getAllScriptRows();
|
||||
return rows.find((item) => item.id === id) || null;
|
||||
}
|
||||
|
||||
private async getAllScriptRows(): Promise<Script[]> {
|
||||
return getDatabase().getLocal()
|
||||
.select()
|
||||
.from(scripts)
|
||||
.where(eq(scripts.projectId, this.currentProjectId))
|
||||
.orderBy(desc(scripts.updatedAt))
|
||||
.all();
|
||||
}
|
||||
|
||||
private async toScriptData(row: Script): Promise<ScriptData> {
|
||||
let content = '';
|
||||
try {
|
||||
content = await fs.readFile(row.filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
const fsError = error as NodeJS.ErrnoException;
|
||||
if (fsError.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.projectId,
|
||||
slug: row.slug,
|
||||
title: row.title,
|
||||
kind: row.kind,
|
||||
entrypoint: row.entrypoint,
|
||||
enabled: row.enabled,
|
||||
version: row.version,
|
||||
filePath: row.filePath,
|
||||
content,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private getDataDir(): string {
|
||||
if (this.dataDir) {
|
||||
return this.dataDir;
|
||||
}
|
||||
|
||||
return path.join(app.getPath('userData'), 'projects', this.currentProjectId);
|
||||
}
|
||||
|
||||
private getScriptsDir(): string {
|
||||
return path.join(this.getDataDir(), 'scripts');
|
||||
}
|
||||
|
||||
private getScriptFilePath(slug: string): string {
|
||||
return path.join(this.getScriptsDir(), `${slug}.py`);
|
||||
}
|
||||
|
||||
private normalizeSlug(value: string): string {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
return normalized || 'script';
|
||||
}
|
||||
|
||||
private ensureUniqueSlug(slug: string, rows: Script[], excludeId?: string): string {
|
||||
const baseSlug = slug;
|
||||
const taken = new Set(
|
||||
rows
|
||||
.filter((item) => item.id !== excludeId)
|
||||
.map((item) => item.slug)
|
||||
);
|
||||
|
||||
if (!taken.has(baseSlug)) {
|
||||
return baseSlug;
|
||||
}
|
||||
|
||||
let suffix = 2;
|
||||
while (taken.has(`${baseSlug}-${suffix}`)) {
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
return `${baseSlug}-${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
let scriptEngineInstance: ScriptEngine | null = null;
|
||||
|
||||
export function getScriptEngine(): ScriptEngine {
|
||||
if (!scriptEngineInstance) {
|
||||
scriptEngineInstance = new ScriptEngine();
|
||||
}
|
||||
return scriptEngineInstance;
|
||||
}
|
||||
@@ -100,3 +100,11 @@ export {
|
||||
type MenuDocument,
|
||||
type MenuItemKind,
|
||||
} from './MenuEngine';
|
||||
export {
|
||||
ScriptEngine,
|
||||
getScriptEngine,
|
||||
type ScriptData,
|
||||
type ScriptKind,
|
||||
type CreateScriptInput,
|
||||
type UpdateScriptInput,
|
||||
} from './ScriptEngine';
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getMetaEngine } from '../engine/MetaEngine';
|
||||
import { getMenuEngine, type MenuDocument } from '../engine/MenuEngine';
|
||||
import { getTagEngine } from '../engine/TagEngine';
|
||||
import { getPostMediaEngine } from '../engine/PostMediaEngine';
|
||||
import { getScriptEngine, type CreateScriptInput, type UpdateScriptInput } from '../engine/ScriptEngine';
|
||||
import { getGitEngine } from '../engine/GitEngine';
|
||||
import { taskManager, TaskProgress } from '../engine/TaskManager';
|
||||
import { getDatabase } from '../database';
|
||||
@@ -284,11 +285,13 @@ export function registerIpcHandlers(): void {
|
||||
const metaEngine = getMetaEngine();
|
||||
const menuEngine = getMenuEngine();
|
||||
const tagEngine = getTagEngine();
|
||||
const scriptEngine = getScriptEngine();
|
||||
postEngine.setProjectContext(project.id, dataDir);
|
||||
mediaEngine.setProjectContext(project.id, dataDir, dataDir);
|
||||
metaEngine.setProjectContext(project.id, dataDir);
|
||||
menuEngine.setProjectContext(project.id, dataDir);
|
||||
tagEngine.setProjectContext(project.id, dataDir);
|
||||
scriptEngine.setProjectContext(project.id, dataDir);
|
||||
const postMediaEngine = getPostMediaEngine();
|
||||
postMediaEngine.setProjectContext(project.id);
|
||||
|
||||
@@ -322,11 +325,13 @@ export function registerIpcHandlers(): void {
|
||||
const metaEngine = getMetaEngine();
|
||||
const menuEngine = getMenuEngine();
|
||||
const tagEngine = getTagEngine();
|
||||
const scriptEngine = getScriptEngine();
|
||||
postEngine.setProjectContext(project.id, dataDir);
|
||||
mediaEngine.setProjectContext(project.id, dataDir, dataDir);
|
||||
metaEngine.setProjectContext(project.id, dataDir);
|
||||
menuEngine.setProjectContext(project.id, dataDir);
|
||||
tagEngine.setProjectContext(project.id, dataDir);
|
||||
scriptEngine.setProjectContext(project.id, dataDir);
|
||||
const postMediaEngine = getPostMediaEngine();
|
||||
postMediaEngine.setProjectContext(project.id);
|
||||
|
||||
@@ -723,6 +728,33 @@ export function registerIpcHandlers(): void {
|
||||
return engine.regenerateMissingThumbnails();
|
||||
});
|
||||
|
||||
// ============ Script Handlers ============
|
||||
|
||||
safeHandle('scripts:create', async (_, data: CreateScriptInput) => {
|
||||
const engine = getScriptEngine();
|
||||
return engine.createScript(data);
|
||||
});
|
||||
|
||||
safeHandle('scripts:update', async (_, id: string, data: UpdateScriptInput) => {
|
||||
const engine = getScriptEngine();
|
||||
return engine.updateScript(id, data);
|
||||
});
|
||||
|
||||
safeHandle('scripts:delete', async (_, id: string) => {
|
||||
const engine = getScriptEngine();
|
||||
return engine.deleteScript(id);
|
||||
});
|
||||
|
||||
safeHandle('scripts:get', async (_, id: string) => {
|
||||
const engine = getScriptEngine();
|
||||
return engine.getScript(id);
|
||||
});
|
||||
|
||||
safeHandle('scripts:getAll', async () => {
|
||||
const engine = getScriptEngine();
|
||||
return engine.getAllScripts();
|
||||
});
|
||||
|
||||
// ============ Task Handlers ============
|
||||
|
||||
safeHandle('tasks:getAll', async () => {
|
||||
|
||||
@@ -101,6 +101,15 @@ export const electronAPI: ElectronAPI = {
|
||||
regenerateMissingThumbnails: () => ipcRenderer.invoke('media:regenerateMissingThumbnails'),
|
||||
},
|
||||
|
||||
// Scripts
|
||||
scripts: {
|
||||
create: (data: { title: string; kind: import('./shared/electronApi').ScriptKind; content: string; slug?: string; entrypoint?: string; enabled?: boolean }) => ipcRenderer.invoke('scripts:create', data),
|
||||
update: (id: string, data: { title?: string; kind?: import('./shared/electronApi').ScriptKind; content?: string; slug?: string; entrypoint?: string; enabled?: boolean }) => ipcRenderer.invoke('scripts:update', id, data),
|
||||
delete: (id: string) => ipcRenderer.invoke('scripts:delete', id),
|
||||
get: (id: string) => ipcRenderer.invoke('scripts:get', id),
|
||||
getAll: () => ipcRenderer.invoke('scripts:getAll'),
|
||||
},
|
||||
|
||||
// Post-Media Links
|
||||
postMedia: {
|
||||
link: (postId: string, mediaId: string) => ipcRenderer.invoke('postMedia:link', postId, mediaId),
|
||||
|
||||
@@ -133,6 +133,23 @@ export interface MediaSearchResult {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type ScriptKind = 'macro' | 'utility' | 'transform';
|
||||
|
||||
export interface ScriptData {
|
||||
id: string;
|
||||
projectId: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
kind: ScriptKind;
|
||||
entrypoint: string;
|
||||
enabled: boolean;
|
||||
version: number;
|
||||
filePath: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TaskProgress {
|
||||
taskId: string;
|
||||
name: string;
|
||||
@@ -528,6 +545,27 @@ export interface ElectronAPI {
|
||||
getTags: () => Promise<string[]>;
|
||||
getTagsWithCounts: () => Promise<TagCount[]>;
|
||||
};
|
||||
scripts: {
|
||||
create: (data: {
|
||||
title: string;
|
||||
kind: ScriptKind;
|
||||
content: string;
|
||||
slug?: string;
|
||||
entrypoint?: string;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ScriptData>;
|
||||
update: (id: string, data: {
|
||||
title?: string;
|
||||
kind?: ScriptKind;
|
||||
content?: string;
|
||||
slug?: string;
|
||||
entrypoint?: string;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ScriptData | null>;
|
||||
delete: (id: string) => Promise<boolean>;
|
||||
get: (id: string) => Promise<ScriptData | null>;
|
||||
getAll: () => Promise<ScriptData[]>;
|
||||
};
|
||||
postMedia: {
|
||||
link: (postId: string, mediaId: string) => Promise<MediaLinkData>;
|
||||
unlink: (postId: string, mediaId: string) => Promise<void>;
|
||||
|
||||
@@ -30,6 +30,12 @@ const MediaIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ScriptsIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SettingsIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
||||
@@ -170,6 +176,13 @@ export const ActivityBar: React.FC = () => {
|
||||
>
|
||||
<MediaIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${isActivityActive(snapshot, 'scripts') ? 'active' : ''}`}
|
||||
onClick={() => executeActivityClick('scripts')}
|
||||
title={getTitle('scripts')}
|
||||
>
|
||||
<ScriptsIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${isActivityActive(snapshot, 'tags') ? 'active' : ''}`}
|
||||
onClick={() => executeActivityClick('tags')}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MetadataDiffPanel } from '../MetadataDiffPanel';
|
||||
import { GitDiffView } from '../GitDiffView/GitDiffView';
|
||||
import { DocumentationView } from '../DocumentationView/DocumentationView';
|
||||
import { SiteValidationView } from '../SiteValidationView';
|
||||
import { ScriptsView } from '../ScriptsView/ScriptsView';
|
||||
import { AutoSaveManager, getContrastColor } from '../../utils';
|
||||
import { InsertModal } from '../InsertModal';
|
||||
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||
@@ -1796,6 +1797,7 @@ export const Editor: React.FC = () => {
|
||||
: <Dashboard />,
|
||||
documentation: () => <DocumentationView />,
|
||||
'site-validation': () => <SiteValidationView />,
|
||||
scripts: () => <ScriptsView scriptId={editorRoute.tabId} />,
|
||||
post: () => (editorRoute.tabId ? <PostEditor key={editorRoute.tabId} postId={editorRoute.tabId} /> : <Dashboard />),
|
||||
media: () => (editorRoute.tabId ? <MediaEditor key={editorRoute.tabId} mediaId={editorRoute.tabId} /> : <Dashboard />),
|
||||
dashboard: () => <Dashboard />,
|
||||
|
||||
@@ -86,6 +86,22 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.output-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.output-item {
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-editor-foreground);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.task-group-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -43,6 +43,7 @@ export const Panel: React.FC = () => {
|
||||
panelVisible,
|
||||
panelActiveTab,
|
||||
setPanelActiveTab,
|
||||
panelOutputEntries,
|
||||
tasks,
|
||||
tabs,
|
||||
activeTabId,
|
||||
@@ -383,7 +384,17 @@ export const Panel: React.FC = () => {
|
||||
)}
|
||||
|
||||
{effectiveActivePanelTab === 'output' && (
|
||||
<div className="panel-empty">{t('panel.noOutput')}</div>
|
||||
panelOutputEntries.length === 0 ? (
|
||||
<div className="panel-empty">{t('panel.noOutput')}</div>
|
||||
) : (
|
||||
<div className="output-list">
|
||||
{panelOutputEntries.map((entry) => (
|
||||
<div key={entry.id} className={`output-item output-${entry.kind}`}>
|
||||
{entry.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{effectiveActivePanelTab === 'post-links' && (
|
||||
|
||||
32
src/renderer/components/ScriptsView/ScriptsView.css
Normal file
32
src/renderer/components/ScriptsView/ScriptsView.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.scripts-view {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scripts-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scripts-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.scripts-label {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.scripts-textarea {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
114
src/renderer/components/ScriptsView/ScriptsView.tsx
Normal file
114
src/renderer/components/ScriptsView/ScriptsView.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ScriptData } from '../../../main/shared/electronApi';
|
||||
import { useAppStore } from '../../store';
|
||||
import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance';
|
||||
import { useI18n } from '../../i18n';
|
||||
import './ScriptsView.css';
|
||||
|
||||
interface ScriptsViewProps {
|
||||
scriptId: string | null;
|
||||
}
|
||||
|
||||
export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
||||
const { t } = useI18n();
|
||||
const appendPanelOutputEntry = useAppStore((state) => state.appendPanelOutputEntry);
|
||||
const [script, setScript] = useState<ScriptData | null>(null);
|
||||
const [scriptContent, setScriptContent] = useState('');
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadScript = async () => {
|
||||
if (!scriptId) {
|
||||
setScript(null);
|
||||
setScriptContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = await window.electronAPI?.scripts.get(scriptId);
|
||||
if (cancelled || !item) {
|
||||
setScript(null);
|
||||
setScriptContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
setScript(item);
|
||||
setScriptContent(item.content || '');
|
||||
};
|
||||
|
||||
void loadScript();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [scriptId]);
|
||||
|
||||
const handleRunScript = async () => {
|
||||
if (!script || isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(true);
|
||||
|
||||
try {
|
||||
const runtimeManager = getPythonRuntimeManager();
|
||||
const result = await runtimeManager.execute(scriptContent);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
if (result.result.trim().length > 0) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-result`,
|
||||
message: result.result,
|
||||
createdAt: now,
|
||||
kind: 'result',
|
||||
});
|
||||
}
|
||||
|
||||
if (result.stdout.trim().length > 0) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-stdout`,
|
||||
message: result.stdout,
|
||||
createdAt: now,
|
||||
kind: 'stdout',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
appendPanelOutputEntry({
|
||||
id: `output-${Date.now()}-error`,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: 'error',
|
||||
});
|
||||
} finally {
|
||||
useAppStore.setState({
|
||||
panelVisible: true,
|
||||
panelActiveTab: 'output',
|
||||
});
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="scripts-view">
|
||||
<div className="scripts-editor">
|
||||
<div className="scripts-toolbar">
|
||||
<button type="button" onClick={handleRunScript} disabled={!script || isRunning}>
|
||||
{t('scripts.run')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="scripts-label" htmlFor="scripts-content">
|
||||
{t('scripts.content')}
|
||||
</label>
|
||||
<textarea
|
||||
id="scripts-content"
|
||||
className="scripts-textarea"
|
||||
value={scriptContent}
|
||||
onChange={(event) => setScriptContent(event.target.value)}
|
||||
disabled={!script}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/Setti
|
||||
import { scrollToTagsSection, TagsCategory } from '../TagsView';
|
||||
import { activateSidebarSection } from '../../navigation/sectionActivation';
|
||||
import { getPersistedSidebarSection, setPersistedSidebarSection } from '../../navigation/sidebarUiPersistence';
|
||||
import { openChatTab, openEntityTab, openImportTab, openSingletonToolTab } from '../../navigation/tabPolicy';
|
||||
import { openChatTab, openEntityTab, openImportTab, openScriptTab, openSingletonToolTab } from '../../navigation/tabPolicy';
|
||||
import { createAndFocusPost } from '../../navigation/postCreation';
|
||||
import type { SidebarView } from '../../navigation/sidebarViewRegistry';
|
||||
import { useI18n } from '../../i18n';
|
||||
@@ -1666,6 +1666,125 @@ const ImportList: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ScriptsList: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const { openTab, activeTabId } = useAppStore();
|
||||
const [scripts, setScripts] = useState<Array<{ id: string; title: string }>>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadScripts = useCallback(async () => {
|
||||
const items = await window.electronAPI?.scripts.getAll();
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScripts(items.map((item) => ({ id: item.id, title: item.title })));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadInitialScripts = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const items = await window.electronAPI?.scripts.getAll();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScripts((items ?? []).map((item) => ({ id: item.id, title: item.title })));
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadInitialScripts();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCreateScript = async () => {
|
||||
try {
|
||||
const created = await window.electronAPI?.scripts.create({
|
||||
title: t('sidebar.scripts.newScript'),
|
||||
kind: 'utility',
|
||||
content: 'print("new script")',
|
||||
entrypoint: 'render',
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScripts((prev) => [
|
||||
{ id: created.id, title: created.title },
|
||||
...prev.filter((script) => script.id !== created.id),
|
||||
]);
|
||||
openScriptTab(openTab, created.id, 'pin');
|
||||
void loadScripts();
|
||||
} catch (error) {
|
||||
console.error('Failed to create script:', error);
|
||||
showToast.error(t('sidebar.scripts.createFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>{t('sidebar.scripts.header')}</span>
|
||||
</div>
|
||||
<div className="chat-loading">{t('sidebar.loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-list">
|
||||
<div className="chat-list-header">
|
||||
<span>{t('sidebar.scripts.header')}</span>
|
||||
<button
|
||||
className="chat-new-button"
|
||||
onClick={handleCreateScript}
|
||||
aria-label={t('sidebar.scripts.newScript')}
|
||||
title={t('sidebar.scripts.newScript')}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-list-items">
|
||||
{scripts.length === 0 ? (
|
||||
<div className="chat-empty">
|
||||
<p>{t('sidebar.scripts.none')}</p>
|
||||
<button className="chat-start-button" onClick={handleCreateScript}>
|
||||
{t('sidebar.scripts.createScript')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
scripts.map((script) => (
|
||||
<button
|
||||
key={script.id}
|
||||
type="button"
|
||||
className={`chat-list-item ${activeTabId === script.id ? 'active' : ''}`}
|
||||
onClick={() => openScriptTab(openTab, script.id, 'preview')}
|
||||
onDoubleClick={() => openScriptTab(openTab, script.id, 'pin')}
|
||||
>
|
||||
<div className="chat-item-content">
|
||||
<div className="chat-item-title">{script.title}</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { activeView, sidebarVisible } = useAppStore();
|
||||
|
||||
@@ -1677,6 +1796,7 @@ export const Sidebar: React.FC = () => {
|
||||
posts: <PostsList mode="posts" isActive={true} />,
|
||||
pages: <PostsList mode="pages" isActive={true} />,
|
||||
media: <MediaList />,
|
||||
scripts: <ScriptsList />,
|
||||
settings: <SettingsNav />,
|
||||
tags: <TagsNav />,
|
||||
chat: <ChatList />,
|
||||
|
||||
@@ -80,6 +80,10 @@ const getTabTitle = (
|
||||
return tr('siteValidation.tabTitle');
|
||||
}
|
||||
|
||||
if (tab.type === 'scripts') {
|
||||
return tr('tabBar.scripts');
|
||||
}
|
||||
|
||||
return tr('tabBar.unknown');
|
||||
};
|
||||
|
||||
@@ -158,6 +162,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
||||
<path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.5 6.5 0 0 0 8 1.5zm0 1a5.5 5.5 0 0 1 4.39 8.82l-.88-.88a.5.5 0 0 0-.7.7l.8.8A5.5 5.5 0 1 1 8 2.5zm2.35 3.15L7 9 5.65 7.65a.5.5 0 1 0-.7.7l1.7 1.7a.5.5 0 0 0 .7 0l3.7-3.7a.5.5 0 1 0-.7-.7z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'scripts':
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -26,3 +26,4 @@ export { InsertModal } from './InsertModal';
|
||||
export { WindowTitleBar } from './WindowTitleBar';
|
||||
export { DocumentationView } from './DocumentationView/DocumentationView';
|
||||
export { SiteValidationView } from './SiteValidationView';
|
||||
export { ScriptsView } from './ScriptsView/ScriptsView';
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"activity.posts": "Beiträge",
|
||||
"activity.pages": "Seiten",
|
||||
"activity.media": "Medien",
|
||||
"activity.scripts": "Skripte",
|
||||
"activity.tags": "Schlagwörter",
|
||||
"activity.aiAssistant": "KI-Assistent",
|
||||
"activity.import": "Importieren",
|
||||
@@ -339,6 +340,7 @@
|
||||
"gitSidebar.placeholder.commitMessage": "Commit-Nachricht",
|
||||
"editor.untitled": "Unbenannt",
|
||||
"tabBar.style": "Stil",
|
||||
"tabBar.scripts": "Skripte",
|
||||
"tabBar.loading": "Laden...",
|
||||
"tabBar.unknown": "Unbekannt",
|
||||
"tabBar.preview": "Vorschau",
|
||||
@@ -415,6 +417,9 @@
|
||||
"sidebar.nav.publishing": "Veröffentlichung",
|
||||
"sidebar.nav.data": "Daten",
|
||||
"sidebar.nav.style": "Stil",
|
||||
"sidebar.nav.scripts": "Skripte",
|
||||
"scripts.run": "Skript ausführen",
|
||||
"scripts.content": "Skriptinhalt",
|
||||
"sidebar.tagCloud": "Tag-Wolke",
|
||||
"sidebar.createEdit": "Erstellen & Bearbeiten",
|
||||
"sidebar.mergeTags": "Tags zusammenführen",
|
||||
@@ -695,6 +700,11 @@
|
||||
"sidebar.chat.yesterday": "Gestern",
|
||||
"sidebar.import.header": "IMPORTE",
|
||||
"sidebar.import.newDefinition": "Neue Importdefinition",
|
||||
"sidebar.scripts.header": "SKRIPTE",
|
||||
"sidebar.scripts.newScript": "Neues Skript",
|
||||
"sidebar.scripts.none": "Noch keine Skripte",
|
||||
"sidebar.scripts.createScript": "Ein Skript erstellen",
|
||||
"sidebar.scripts.createFailed": "Skript konnte nicht erstellt werden",
|
||||
"sidebar.import.none": "Noch keine Importdefinitionen",
|
||||
"sidebar.import.createDefinition": "Eine Importdefinition erstellen",
|
||||
"sidebar.import.deleteDefinition": "Importdefinition löschen",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"activity.posts": "Posts",
|
||||
"activity.pages": "Pages",
|
||||
"activity.media": "Media",
|
||||
"activity.scripts": "Scripts",
|
||||
"activity.tags": "Tags",
|
||||
"activity.aiAssistant": "AI Assistant",
|
||||
"activity.import": "Import",
|
||||
@@ -339,6 +340,7 @@
|
||||
"gitSidebar.placeholder.commitMessage": "Commit message",
|
||||
"editor.untitled": "Untitled",
|
||||
"tabBar.style": "Style",
|
||||
"tabBar.scripts": "Scripts",
|
||||
"tabBar.loading": "Loading...",
|
||||
"tabBar.unknown": "Unknown",
|
||||
"tabBar.preview": "Preview",
|
||||
@@ -415,6 +417,9 @@
|
||||
"sidebar.nav.publishing": "Publishing",
|
||||
"sidebar.nav.data": "Data",
|
||||
"sidebar.nav.style": "Style",
|
||||
"sidebar.nav.scripts": "Scripts",
|
||||
"scripts.run": "Run Script",
|
||||
"scripts.content": "Script Content",
|
||||
"sidebar.tagCloud": "Tag Cloud",
|
||||
"sidebar.createEdit": "Create & Edit",
|
||||
"sidebar.mergeTags": "Merge Tags",
|
||||
@@ -695,6 +700,11 @@
|
||||
"sidebar.chat.yesterday": "Yesterday",
|
||||
"sidebar.import.header": "IMPORTS",
|
||||
"sidebar.import.newDefinition": "New Import Definition",
|
||||
"sidebar.scripts.header": "SCRIPTS",
|
||||
"sidebar.scripts.newScript": "New Script",
|
||||
"sidebar.scripts.none": "No scripts yet",
|
||||
"sidebar.scripts.createScript": "Create a script",
|
||||
"sidebar.scripts.createFailed": "Failed to create script",
|
||||
"sidebar.import.none": "No import definitions yet",
|
||||
"sidebar.import.createDefinition": "Create an import definition",
|
||||
"sidebar.import.deleteDefinition": "Delete import definition",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"activity.posts": "Entradas",
|
||||
"activity.pages": "Páginas",
|
||||
"activity.media": "Medios",
|
||||
"activity.scripts": "Scripts",
|
||||
"activity.tags": "Etiquetas",
|
||||
"activity.aiAssistant": "Asistente IA",
|
||||
"activity.import": "Importar",
|
||||
@@ -339,6 +340,7 @@
|
||||
"gitSidebar.placeholder.commitMessage": "Mensaje de commit",
|
||||
"editor.untitled": "Sin título",
|
||||
"tabBar.style": "Estilo",
|
||||
"tabBar.scripts": "Scripts",
|
||||
"tabBar.loading": "Cargando...",
|
||||
"tabBar.unknown": "Desconocido",
|
||||
"tabBar.preview": "Vista previa",
|
||||
@@ -415,6 +417,9 @@
|
||||
"sidebar.nav.publishing": "Publicación",
|
||||
"sidebar.nav.data": "Datos",
|
||||
"sidebar.nav.style": "Estilo",
|
||||
"sidebar.nav.scripts": "Scripts",
|
||||
"scripts.run": "Ejecutar script",
|
||||
"scripts.content": "Contenido del script",
|
||||
"sidebar.tagCloud": "Nube de etiquetas",
|
||||
"sidebar.createEdit": "Crear y editar",
|
||||
"sidebar.mergeTags": "Combinar etiquetas",
|
||||
@@ -695,6 +700,11 @@
|
||||
"sidebar.chat.yesterday": "Ayer",
|
||||
"sidebar.import.header": "Importación",
|
||||
"sidebar.import.newDefinition": "Nueva definición",
|
||||
"sidebar.scripts.header": "SCRIPTS",
|
||||
"sidebar.scripts.newScript": "Nuevo script",
|
||||
"sidebar.scripts.none": "Aún no hay scripts",
|
||||
"sidebar.scripts.createScript": "Crear un script",
|
||||
"sidebar.scripts.createFailed": "No se pudo crear el script",
|
||||
"sidebar.import.none": "Sin definiciones de importación",
|
||||
"sidebar.import.createDefinition": "Crear definición",
|
||||
"sidebar.import.deleteDefinition": "Eliminar definición",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"activity.posts": "Articles",
|
||||
"activity.pages": "Pages du site",
|
||||
"activity.media": "Médias",
|
||||
"activity.scripts": "Scripts",
|
||||
"activity.tags": "Étiquettes",
|
||||
"activity.aiAssistant": "Assistant IA",
|
||||
"activity.import": "Importation",
|
||||
@@ -339,6 +340,7 @@
|
||||
"gitSidebar.placeholder.commitMessage": "Message de commit",
|
||||
"editor.untitled": "Sans titre",
|
||||
"tabBar.style": "Apparence",
|
||||
"tabBar.scripts": "Scripts",
|
||||
"tabBar.loading": "Chargement...",
|
||||
"tabBar.unknown": "Inconnu",
|
||||
"tabBar.preview": "Aperçu",
|
||||
@@ -415,6 +417,9 @@
|
||||
"sidebar.nav.publishing": "Publication",
|
||||
"sidebar.nav.data": "Données",
|
||||
"sidebar.nav.style": "Style",
|
||||
"sidebar.nav.scripts": "Scripts",
|
||||
"scripts.run": "Exécuter le script",
|
||||
"scripts.content": "Contenu du script",
|
||||
"sidebar.tagCloud": "Nuage d’étiquettes",
|
||||
"sidebar.createEdit": "Créer & modifier",
|
||||
"sidebar.mergeTags": "Fusionner les étiquettes",
|
||||
@@ -695,6 +700,11 @@
|
||||
"sidebar.chat.yesterday": "Hier",
|
||||
"sidebar.import.header": "Import",
|
||||
"sidebar.import.newDefinition": "Nouvelle définition",
|
||||
"sidebar.scripts.header": "SCRIPTS",
|
||||
"sidebar.scripts.newScript": "Nouveau script",
|
||||
"sidebar.scripts.none": "Aucun script",
|
||||
"sidebar.scripts.createScript": "Créer un script",
|
||||
"sidebar.scripts.createFailed": "Impossible de créer le script",
|
||||
"sidebar.import.none": "Aucune définition d’import",
|
||||
"sidebar.import.createDefinition": "Créer une définition",
|
||||
"sidebar.import.deleteDefinition": "Supprimer la définition",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"activity.posts": "Post",
|
||||
"activity.pages": "Pagine",
|
||||
"activity.media": "Contenuti media",
|
||||
"activity.scripts": "Script",
|
||||
"activity.tags": "Tag",
|
||||
"activity.aiAssistant": "Assistente IA",
|
||||
"activity.import": "Importa",
|
||||
@@ -339,6 +340,7 @@
|
||||
"gitSidebar.placeholder.commitMessage": "Messaggio di commit",
|
||||
"editor.untitled": "Senza titolo",
|
||||
"tabBar.style": "Stile",
|
||||
"tabBar.scripts": "Script",
|
||||
"tabBar.loading": "Caricamento...",
|
||||
"tabBar.unknown": "Sconosciuto",
|
||||
"tabBar.preview": "Anteprima",
|
||||
@@ -415,6 +417,9 @@
|
||||
"sidebar.nav.publishing": "Pubblicazione",
|
||||
"sidebar.nav.data": "Dati",
|
||||
"sidebar.nav.style": "Stile",
|
||||
"sidebar.nav.scripts": "Script",
|
||||
"scripts.run": "Esegui script",
|
||||
"scripts.content": "Contenuto script",
|
||||
"sidebar.tagCloud": "Nuvola tag",
|
||||
"sidebar.createEdit": "Crea e modifica",
|
||||
"sidebar.mergeTags": "Unisci tag",
|
||||
@@ -695,6 +700,11 @@
|
||||
"sidebar.chat.yesterday": "Ieri",
|
||||
"sidebar.import.header": "Importazione",
|
||||
"sidebar.import.newDefinition": "Nuova definizione",
|
||||
"sidebar.scripts.header": "SCRIPTS",
|
||||
"sidebar.scripts.newScript": "Nuovo script",
|
||||
"sidebar.scripts.none": "Nessuno script",
|
||||
"sidebar.scripts.createScript": "Crea uno script",
|
||||
"sidebar.scripts.createFailed": "Impossibile creare lo script",
|
||||
"sidebar.import.none": "Nessuna definizione di importazione",
|
||||
"sidebar.import.createDefinition": "Crea definizione",
|
||||
"sidebar.import.deleteDefinition": "Elimina definizione",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Tab } from '../store/appStore';
|
||||
import type { SidebarView } from './sidebarViewRegistry';
|
||||
|
||||
export type ActivityId = 'posts' | 'pages' | 'media' | 'tags' | 'chat' | 'import' | 'git' | 'settings';
|
||||
export type ActivityId = 'posts' | 'pages' | 'media' | 'scripts' | 'tags' | 'chat' | 'import' | 'git' | 'settings';
|
||||
|
||||
export interface ActivitySnapshot {
|
||||
activeView: SidebarView;
|
||||
@@ -43,6 +43,13 @@ const ACTIVITY_CONFIG: Record<ActivityId, ActivityConfig> = {
|
||||
activeStrategy: 'sidebar-owner',
|
||||
clickStrategy: 'sidebar-toggle',
|
||||
},
|
||||
scripts: {
|
||||
id: 'scripts',
|
||||
view: 'scripts',
|
||||
labelKey: 'activity.scripts',
|
||||
activeStrategy: 'sidebar-owner',
|
||||
clickStrategy: 'sidebar-toggle',
|
||||
},
|
||||
tags: {
|
||||
id: 'tags',
|
||||
view: 'tags',
|
||||
|
||||
@@ -14,7 +14,8 @@ export type EditorRoute =
|
||||
| 'metadata-diff'
|
||||
| 'git-diff'
|
||||
| 'documentation'
|
||||
| 'site-validation';
|
||||
| 'site-validation'
|
||||
| 'scripts';
|
||||
|
||||
export const EDITOR_TAB_ROUTE_REGISTRY: Record<TabType, Exclude<EditorRoute, 'dashboard'>> = {
|
||||
post: 'post',
|
||||
@@ -29,6 +30,7 @@ export const EDITOR_TAB_ROUTE_REGISTRY: Record<TabType, Exclude<EditorRoute, 'da
|
||||
'git-diff': 'git-diff',
|
||||
documentation: 'documentation',
|
||||
'site-validation': 'site-validation',
|
||||
scripts: 'scripts',
|
||||
};
|
||||
|
||||
export interface EditorRouteResolution {
|
||||
|
||||
@@ -2,6 +2,7 @@ export const SIDEBAR_VIEW_REGISTRY = [
|
||||
'posts',
|
||||
'pages',
|
||||
'media',
|
||||
'scripts',
|
||||
'settings',
|
||||
'tags',
|
||||
'chat',
|
||||
|
||||
@@ -4,6 +4,7 @@ export type SingletonToolTabKey =
|
||||
| 'settings'
|
||||
| 'tags'
|
||||
| 'style'
|
||||
| 'scripts'
|
||||
| 'menu-editor'
|
||||
| 'documentation'
|
||||
| 'metadata-diff'
|
||||
@@ -17,12 +18,14 @@ export interface CanonicalTabSpec {
|
||||
|
||||
export type EntityTabType = 'post' | 'media';
|
||||
export type EntityTabOpenIntent = 'preview' | 'pin';
|
||||
export type ScriptTabOpenIntent = 'preview' | 'pin';
|
||||
export type GitDiffResourceOpenIntent = 'preview' | 'pin';
|
||||
|
||||
const SINGLETON_TOOL_TAB_REGISTRY: Record<SingletonToolTabKey, CanonicalTabSpec> = {
|
||||
settings: { type: 'settings', id: 'settings', isTransient: false },
|
||||
tags: { type: 'tags', id: 'tags', isTransient: false },
|
||||
style: { type: 'style', id: 'style', isTransient: false },
|
||||
scripts: { type: 'scripts', id: 'scripts', isTransient: false },
|
||||
'menu-editor': { type: 'menu-editor', id: 'menu-editor', isTransient: false },
|
||||
documentation: { type: 'documentation', id: 'documentation', isTransient: false },
|
||||
'metadata-diff': { type: 'metadata-diff', id: 'metadata-diff', isTransient: false },
|
||||
@@ -91,6 +94,22 @@ export function openImportTab(
|
||||
openTab(getImportTabSpec(definitionId));
|
||||
}
|
||||
|
||||
export function getScriptTabSpec(scriptId: string, intent: ScriptTabOpenIntent): CanonicalTabSpec {
|
||||
return {
|
||||
type: 'scripts',
|
||||
id: scriptId,
|
||||
isTransient: intent === 'preview',
|
||||
};
|
||||
}
|
||||
|
||||
export function openScriptTab(
|
||||
openTab: (tab: CanonicalTabSpec) => void,
|
||||
scriptId: string,
|
||||
intent: ScriptTabOpenIntent,
|
||||
): void {
|
||||
openTab(getScriptTabSpec(scriptId, intent));
|
||||
}
|
||||
|
||||
export function getGitDiffFileTabId(filePath: string): string {
|
||||
return `git-diff:${filePath}`;
|
||||
}
|
||||
|
||||
7
src/renderer/python/runtimeManagerInstance.ts
Normal file
7
src/renderer/python/runtimeManagerInstance.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PythonRuntimeManager } from './PythonRuntimeManager';
|
||||
|
||||
const runtimeManager = new PythonRuntimeManager();
|
||||
|
||||
export function getPythonRuntimeManager(): PythonRuntimeManager {
|
||||
return runtimeManager;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
const STORAGE_KEY = 'bds-app-state';
|
||||
|
||||
// Tab types
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'menu-editor' | 'metadata-diff' | 'git-diff' | 'documentation' | 'site-validation';
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'menu-editor' | 'metadata-diff' | 'git-diff' | 'documentation' | 'site-validation' | 'scripts';
|
||||
|
||||
export interface Tab {
|
||||
type: TabType;
|
||||
@@ -42,6 +42,13 @@ export type EditorMode = 'wysiwyg' | 'markdown' | 'preview';
|
||||
export type GitDiffViewStyle = 'inline' | 'side-by-side';
|
||||
export type PanelTab = 'tasks' | 'output' | 'post-links' | 'git-log';
|
||||
|
||||
export interface PanelOutputEntry {
|
||||
id: string;
|
||||
message: string;
|
||||
createdAt: string;
|
||||
kind: 'stdout' | 'result' | 'error';
|
||||
}
|
||||
|
||||
export interface GitDiffPreferences {
|
||||
wordWrap: boolean;
|
||||
viewStyle: GitDiffViewStyle;
|
||||
@@ -63,6 +70,7 @@ interface AppState {
|
||||
sidebarVisible: boolean;
|
||||
panelVisible: boolean;
|
||||
panelActiveTab: PanelTab;
|
||||
panelOutputEntries: PanelOutputEntry[];
|
||||
selectedPostId: string | null;
|
||||
selectedMediaId: string | null;
|
||||
preferredEditorMode: EditorMode;
|
||||
@@ -112,6 +120,8 @@ interface AppState {
|
||||
toggleSidebar: () => void;
|
||||
togglePanel: () => void;
|
||||
setPanelActiveTab: (tab: PanelTab) => void;
|
||||
appendPanelOutputEntry: (entry: PanelOutputEntry) => void;
|
||||
clearPanelOutputEntries: () => void;
|
||||
setSelectedPost: (id: string | null) => void;
|
||||
setSelectedMedia: (id: string | null) => void;
|
||||
setPreferredEditorMode: (mode: EditorMode) => void;
|
||||
@@ -166,6 +176,7 @@ export const useAppStore = create<AppState>()(
|
||||
sidebarVisible: true,
|
||||
panelVisible: false,
|
||||
panelActiveTab: 'tasks',
|
||||
panelOutputEntries: [],
|
||||
selectedPostId: null,
|
||||
selectedMediaId: null,
|
||||
preferredEditorMode: 'wysiwyg',
|
||||
@@ -290,6 +301,10 @@ export const useAppStore = create<AppState>()(
|
||||
toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })),
|
||||
togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })),
|
||||
setPanelActiveTab: (panelActiveTab) => set({ panelActiveTab }),
|
||||
appendPanelOutputEntry: (entry) => set((state) => ({
|
||||
panelOutputEntries: [...state.panelOutputEntries, entry],
|
||||
})),
|
||||
clearPanelOutputEntries: () => set({ panelOutputEntries: [] }),
|
||||
setSelectedPost: (id) => set({ selectedPostId: id }),
|
||||
setSelectedMedia: (id) => set({ selectedMediaId: id }),
|
||||
setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }),
|
||||
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
type TaskProgress,
|
||||
type EditorMode,
|
||||
type ErrorDetails,
|
||||
type PanelOutputEntry,
|
||||
type Tab,
|
||||
type TabType,
|
||||
type TabState
|
||||
|
||||
Reference in New Issue
Block a user