feat: phase 1 of python scripting

This commit is contained in:
2026-02-22 22:12:30 +01:00
parent ce050f98c3
commit 3ec8819d6d
43 changed files with 2329 additions and 14 deletions

View File

@@ -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')}

View File

@@ -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 />,

View File

@@ -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;

View File

@@ -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' && (

View 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;
}

View 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>
);
};

View File

@@ -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 />,

View File

@@ -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">

View File

@@ -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';

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 dimport",
"sidebar.import.createDefinition": "Créer une définition",
"sidebar.import.deleteDefinition": "Supprimer la définition",

View File

@@ -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",

View File

@@ -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',

View File

@@ -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 {

View File

@@ -2,6 +2,7 @@ export const SIDEBAR_VIEW_REGISTRY = [
'posts',
'pages',
'media',
'scripts',
'settings',
'tags',
'chat',

View File

@@ -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}`;
}

View File

@@ -0,0 +1,7 @@
import { PythonRuntimeManager } from './PythonRuntimeManager';
const runtimeManager = new PythonRuntimeManager();
export function getPythonRuntimeManager(): PythonRuntimeManager {
return runtimeManager;
}

View File

@@ -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 }),

View File

@@ -6,6 +6,7 @@ export {
type TaskProgress,
type EditorMode,
type ErrorDetails,
type PanelOutputEntry,
type Tab,
type TabType,
type TabState