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

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