From fcdf869a7c2952c9c6e47a3fa9cf5e4aae1637ac Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 25 Feb 2026 19:51:58 +0100 Subject: [PATCH 01/16] wip: agui integration --- DOCUMENTATION.md | 160 +++++++ src/main/engine/ChatEngine.ts | 14 +- src/main/engine/OpenCodeManager.ts | 11 +- src/main/ipc/chatHandlers.ts | 19 +- src/main/ipc/handlers.ts | 6 +- src/main/main.ts | 25 +- src/main/preload.ts | 3 +- src/main/shared/electronApi.ts | 7 +- src/main/shared/i18n/locales/de.json | 1 + src/main/shared/i18n/locales/en.json | 1 + src/main/shared/i18n/locales/es.json | 1 + src/main/shared/i18n/locales/fr.json | 1 + src/main/shared/i18n/locales/it.json | 1 + src/main/shared/menuCommands.ts | 3 + src/renderer/App.tsx | 23 +- .../AssistantPanelControls.css | 173 ++++++++ .../AssistantPanelControls.tsx | 295 +++++++++++++ .../AssistantPanelControls/index.ts | 1 + .../AssistantSidebar/AssistantSidebar.css | 250 +++++++++++ .../AssistantSidebar/AssistantSidebar.tsx | 239 +++++++++++ .../components/AssistantSidebar/index.ts | 1 + .../components/ChatPanel/ChatPanel.tsx | 406 +++++++----------- .../components/ChatSurface/ChatTranscript.tsx | 154 +++++++ src/renderer/components/ChatSurface/index.ts | 1 + .../WindowTitleBar/WindowTitleBar.css | 35 ++ .../WindowTitleBar/WindowTitleBar.tsx | 16 +- src/renderer/components/index.ts | 2 + src/renderer/i18n/locales/de.json | 13 + src/renderer/i18n/locales/en.json | 13 + src/renderer/i18n/locales/es.json | 13 + src/renderer/i18n/locales/fr.json | 13 + src/renderer/i18n/locales/it.json | 13 + .../navigation/assistantActionDispatcher.ts | 129 ++++++ .../navigation/assistantConversation.ts | 30 ++ src/renderer/navigation/assistantPanelSpec.ts | 391 +++++++++++++++++ .../navigation/assistantPromptContext.ts | 67 +++ src/renderer/navigation/chatSession.ts | 73 ++++ src/renderer/navigation/chatSurfaceMode.ts | 24 ++ .../navigation/useChatMessageSender.ts | 56 +++ .../navigation/useChatSurfaceState.ts | 127 ++++++ src/renderer/store/appStore.ts | 4 + src/renderer/styles/chatSurface.css | 27 ++ tests/engine/ChatEngine.test.ts | 4 + tests/ipc/handlers.test.ts | 21 +- .../AssistantSidebar.styles.test.ts | 16 + .../ChatSurface.sharedStyles.test.ts | 41 ++ .../components/WindowTitleBar.test.tsx | 19 +- .../assistantActionDispatcher.test.ts | 195 +++++++++ .../navigation/assistantConversation.test.ts | 35 ++ .../navigation/assistantPanelSpec.test.ts | 195 +++++++++ .../navigation/assistantPromptContext.test.ts | 47 ++ .../navigation/assistantSidebarGuards.test.ts | 38 ++ tests/renderer/navigation/chatSession.test.ts | 98 +++++ .../navigation/chatSurfaceMode.test.ts | 25 ++ .../chatSurfaceModeUsageGuards.test.ts | 24 ++ .../navigation/chatSurfaceUsageGuards.test.ts | 25 ++ .../navigation/useChatMessageSender.test.tsx | 58 +++ .../navigation/useChatSurfaceState.test.tsx | 40 ++ tests/renderer/store/appStore.test.ts | 11 + 59 files changed, 3467 insertions(+), 267 deletions(-) create mode 100644 src/renderer/components/AssistantPanelControls/AssistantPanelControls.css create mode 100644 src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx create mode 100644 src/renderer/components/AssistantPanelControls/index.ts create mode 100644 src/renderer/components/AssistantSidebar/AssistantSidebar.css create mode 100644 src/renderer/components/AssistantSidebar/AssistantSidebar.tsx create mode 100644 src/renderer/components/AssistantSidebar/index.ts create mode 100644 src/renderer/components/ChatSurface/ChatTranscript.tsx create mode 100644 src/renderer/components/ChatSurface/index.ts create mode 100644 src/renderer/navigation/assistantActionDispatcher.ts create mode 100644 src/renderer/navigation/assistantConversation.ts create mode 100644 src/renderer/navigation/assistantPanelSpec.ts create mode 100644 src/renderer/navigation/assistantPromptContext.ts create mode 100644 src/renderer/navigation/chatSession.ts create mode 100644 src/renderer/navigation/chatSurfaceMode.ts create mode 100644 src/renderer/navigation/useChatMessageSender.ts create mode 100644 src/renderer/navigation/useChatSurfaceState.ts create mode 100644 src/renderer/styles/chatSurface.css create mode 100644 tests/renderer/components/AssistantSidebar.styles.test.ts create mode 100644 tests/renderer/components/ChatSurface.sharedStyles.test.ts create mode 100644 tests/renderer/navigation/assistantActionDispatcher.test.ts create mode 100644 tests/renderer/navigation/assistantConversation.test.ts create mode 100644 tests/renderer/navigation/assistantPanelSpec.test.ts create mode 100644 tests/renderer/navigation/assistantPromptContext.test.ts create mode 100644 tests/renderer/navigation/assistantSidebarGuards.test.ts create mode 100644 tests/renderer/navigation/chatSession.test.ts create mode 100644 tests/renderer/navigation/chatSurfaceMode.test.ts create mode 100644 tests/renderer/navigation/chatSurfaceModeUsageGuards.test.ts create mode 100644 tests/renderer/navigation/chatSurfaceUsageGuards.test.ts create mode 100644 tests/renderer/navigation/useChatMessageSender.test.tsx create mode 100644 tests/renderer/navigation/useChatSurfaceState.test.tsx diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4ffde4e..ffb939f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -11,6 +11,7 @@ - [Working with media](#working-with-media) - [Using macros](#using-macros) - [Using scripting (early access)](#using-scripting-early-access) +- [Using assistant panel widgets](#using-assistant-panel-widgets) - [Organizing with tags](#organizing-with-tags) - [Importing from WordPress (WXR)](#importing-from-wordpress-wxr) - [Using Git (Source Control)](#using-git-source-control) @@ -255,6 +256,165 @@ Notes: --- +## Using assistant panel widgets + +The assistant sidebar can render structured panel widgets when the AI response includes a valid JSON panel spec. This is useful when you want the assistant to return actionable UI instead of plain text only. + +Use this envelope: + +```json +{ + "specVersion": "1", + "elements": [] +} +``` + +### Supported widget types + +- `text` +- `metric` +- `list` +- `table` +- `action` +- `chart` +- `input` +- `form` +- `datePicker` +- `card` +- `image` +- `tabs` + +### Example snippets + +```json +{ "type": "text", "text": "Review complete." } +``` + +```json +{ "type": "metric", "label": "Draft posts", "value": "12" } +``` + +```json +{ "type": "list", "title": "Next steps", "items": ["Refine title", "Add tags"] } +``` + +```json +{ + "type": "table", + "columns": ["Post", "Status"], + "rows": [["Roadmap", "Draft"], ["Release", "Published"]] +} +``` + +```json +{ + "type": "action", + "label": "Open tags", + "action": "switchView", + "payload": { "view": "tags" } +} +``` + +```json +{ + "type": "chart", + "chartType": "bar", + "title": "Posts by month", + "series": [ + { "label": "Jan", "value": 10 }, + { "label": "Feb", "value": 14 } + ] +} +``` + +```json +{ + "type": "input", + "key": "query", + "label": "Search", + "inputType": "text", + "placeholder": "Find post...", + "submitLabel": "Run", + "action": "openChat" +} +``` + +```json +{ + "type": "form", + "formId": "meta-form", + "title": "Update metadata", + "submitLabel": "Apply", + "action": "openSettings", + "fields": [ + { "key": "title", "label": "Title", "inputType": "text" }, + { "key": "isDraft", "label": "Draft", "inputType": "checkbox" } + ] +} +``` + +```json +{ + "type": "datePicker", + "key": "publishDate", + "label": "Publish date", + "submitLabel": "Set", + "action": "openSettings" +} +``` + +```json +{ + "type": "card", + "title": "Suggestion", + "subtitle": "Editorial", + "body": "Add one category and two tags.", + "actions": [ + { "label": "Open tags", "action": "switchView", "payload": { "view": "tags" } } + ] +} +``` + +```json +{ + "type": "image", + "src": "https://example.com/preview.png", + "alt": "Generated preview", + "caption": "Current preview snapshot" +} +``` + +```json +{ + "type": "tabs", + "defaultTabId": "summary", + "tabs": [ + { + "id": "summary", + "label": "Summary", + "elements": [{ "type": "text", "text": "Short summary" }] + }, + { + "id": "actions", + "label": "Actions", + "elements": [ + { "type": "action", "label": "Open settings", "action": "openSettings" } + ] + } + ] +} +``` + +### Notes + +- `tabs` are panel-local UI tabs inside one assistant response; they are not editor tabs. +- Unknown or invalid widget payloads are ignored by the parser. +- Actions are restricted to supported safe action names in the app. + +[↑ Back to In this article](#in-this-article) + +--- + ## Organizing with tags Tags are your precision taxonomy tool. Over time, even well-managed projects accumulate near-duplicate tags, naming inconsistencies, and labels that no longer serve users. The Tags section exists to keep taxonomy useful and prevent search and filtering quality from degrading. diff --git a/src/main/engine/ChatEngine.ts b/src/main/engine/ChatEngine.ts index 0ca1b77..fc52638 100644 --- a/src/main/engine/ChatEngine.ts +++ b/src/main/engine/ChatEngine.ts @@ -326,7 +326,19 @@ When answering questions: 2. If asked about something outside your tools (weather, news, websites), explain that you can only access the user's local blog content. 3. Be concise and helpful. Format post information clearly when displaying it. 4. If a search returns no results, suggest alternative queries or filters. -5. When asked to describe or analyze an image, use the view_image tool to see the actual image content.`; +5. When asked to describe or analyze an image, use the view_image tool to see the actual image content. + +Agentic UI Contract: +- You may include structured UI payloads in your assistant response so the app can render interactive widgets. +- You DO have the ability to return interactive AGUI payloads (including bar charts) as JSON, even though you cannot draw bitmap images. +- When the user asks for a chart or guided workflow, prefer returning a valid AGUI payload over refusing. +- Use JSON with specVersion: "1" and an elements array. +- Prefer actionable widgets (cards, forms, tabs, inputs, metrics, tables, charts) when they reduce follow-up friction. +- Keep textual guidance and UI semantically consistent. +- Include only valid, supported action names. Supported actions include: openSettings, openPost, openMedia, openPanel, setActiveView, toggleSidebar, togglePanel, toggleAssistantSidebar. +- Supported element types include: text, metric, list, table, action, chart, form, input, datePicker, card, image, tabs. +- For tabs elements, include each tab with id, label, and nested elements. +- Never invent unsupported specVersion values or unsupported element/action names.`; } /** diff --git a/src/main/engine/OpenCodeManager.ts b/src/main/engine/OpenCodeManager.ts index f472a1f..a6f82a2 100644 --- a/src/main/engine/OpenCodeManager.ts +++ b/src/main/engine/OpenCodeManager.ts @@ -66,6 +66,9 @@ export interface ModelInfo { } export interface SendMessageOptions { + metadata?: { + surface?: 'tab' | 'sidebar'; + }; onDelta?: (delta: string) => void; onToolCall?: (toolCall: { name: string; args: unknown }) => void; onToolResult?: (result: { name: string; result: unknown }) => void; @@ -237,7 +240,7 @@ export class OpenCodeManager { userMessage: string, options: SendMessageOptions = {} ): Promise { - const { onDelta, onToolCall, onToolResult } = options; + const { metadata, onDelta, onToolCall, onToolResult } = options; try { const readyCheck = await this.checkReady(); @@ -272,11 +275,15 @@ export class OpenCodeManager { // Build message history from DB (excluding system messages) const dbMessages = conversation.messages.filter(m => m.role !== 'system'); + const surfaceHint = metadata?.surface + ? `\n\n[Client UI surface: ${metadata.surface}. Render response UI for this surface while keeping content functionally equivalent.]` + : ''; + const userMessageForModel = `${userMessage}${surfaceHint}`; // Add the new user message dbMessages.push({ conversationId, role: 'user', - content: userMessage, + content: userMessageForModel, createdAt: new Date(), }); diff --git a/src/main/ipc/chatHandlers.ts b/src/main/ipc/chatHandlers.ts index e3e4cb4..4f080c1 100644 --- a/src/main/ipc/chatHandlers.ts +++ b/src/main/ipc/chatHandlers.ts @@ -256,12 +256,13 @@ export function registerChatHandlers(): void { // ============ Chat Messaging ============ // Send a message - ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string) => { + ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string, metadata?: { surface?: 'tab' | 'sidebar' }) => { try { const manager = await getOpenCodeManager(); const mainWindow = mainWindowGetter?.(); const result = await manager.sendMessage(conversationId, message, { + metadata, onDelta: (delta) => { if (mainWindow) { mainWindow.webContents.send('chat-stream-delta', { conversationId, delta }); @@ -286,6 +287,22 @@ export function registerChatHandlers(): void { } }); + ipcMain.handle('chat:addSystemEvent', async (_, conversationId: string, content: string) => { + try { + const engine = getChatEngine(); + await engine.addMessage({ + conversationId, + role: 'system', + content, + createdAt: new Date(), + }); + return { success: true }; + } catch (error) { + console.error('[Chat IPC] Error adding system event:', error); + return { success: false, error: (error as Error).message }; + } + }); + // Abort a running message ipcMain.handle('chat:abortMessage', async (_, conversationId: string) => { try { diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 2dcb9e9..a67b900 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -84,7 +84,11 @@ function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean { sender.selectAll?.(); return true; case 'toggleDevTools': - sender.toggleDevTools?.(); + if (sender.isDevToolsOpened?.()) { + sender.closeDevTools?.(); + } else { + sender.openDevTools?.({ mode: 'detach' }); + } return true; case 'reload': sender.reload?.(); diff --git a/src/main/main.ts b/src/main/main.ts index 69023b1..95d695f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -49,6 +49,20 @@ interface Rectangle { // Check if dev server is likely running (only in development) const isDev = process.env.NODE_ENV === 'development'; +function toggleDetachedDevTools(targetWindow: BrowserWindow | null): void { + const webContents = targetWindow?.webContents; + if (!webContents) { + return; + } + + if (webContents.isDevToolsOpened()) { + webContents.closeDevTools(); + return; + } + + webContents.openDevTools({ mode: 'detach' }); +} + function getWindowStatePath(): string | null { if (typeof app.getPath !== 'function') { return null; @@ -246,7 +260,7 @@ function createWindow(): void { // F12 or Ctrl+Shift+I to toggle DevTools if (input.key === 'F12' || (input.control && input.shift && input.key.toLowerCase() === 'i')) { - mainWindow?.webContents.toggleDevTools(); + toggleDetachedDevTools(mainWindow); event.preventDefault(); } }); @@ -255,13 +269,13 @@ function createWindow(): void { const rendererPath = path.join(__dirname, '../renderer/index.html'); if (isDev) { mainWindow.loadURL('http://localhost:5173'); - mainWindow.webContents.openDevTools(); + mainWindow.webContents.openDevTools({ mode: 'detach' }); } else if (fs.existsSync(rendererPath)) { mainWindow.loadFile(rendererPath); } else { // Fallback to dev server if built files don't exist mainWindow.loadURL('http://localhost:5173'); - mainWindow.webContents.openDevTools(); + mainWindow.webContents.openDevTools({ mode: 'detach' }); } // Forward events to renderer @@ -571,6 +585,11 @@ function createApplicationMenu(): Menu { return; } + if (action === 'toggleDevTools') { + toggleDetachedDevTools(mainWindow); + return; + } + if (action === 'viewOnGitHub') { void shell.openExternal('https://github.com/rfc1437/bDS'); return; diff --git a/src/main/preload.ts b/src/main/preload.ts index 71ac84b..b3a3e2b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -299,7 +299,8 @@ export const electronAPI: ElectronAPI = { deleteConversation: (id: string) => ipcRenderer.invoke('chat:deleteConversation', id), // Messaging - sendMessage: (conversationId: string, message: string) => ipcRenderer.invoke('chat:sendMessage', conversationId, message), + sendMessage: (conversationId: string, message: string, metadata?: { surface?: 'tab' | 'sidebar' }) => ipcRenderer.invoke('chat:sendMessage', conversationId, message, metadata), + addSystemEvent: (conversationId: string, content: string) => ipcRenderer.invoke('chat:addSystemEvent', conversationId, content), abortMessage: (conversationId: string) => ipcRenderer.invoke('chat:abortMessage', conversationId), getHistory: (conversationId: string) => ipcRenderer.invoke('chat:getHistory', conversationId), clearMessages: (conversationId: string) => ipcRenderer.invoke('chat:clearMessages', conversationId), diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts index f29b855..59b59b7 100644 --- a/src/main/shared/electronApi.ts +++ b/src/main/shared/electronApi.ts @@ -431,6 +431,10 @@ export interface ChatTitleUpdate { title: string; } +export interface ChatSendMetadata { + surface?: 'tab' | 'sidebar'; +} + export interface SiteValidationReport { sitemapPath: string; sitemapChanged: boolean; @@ -726,7 +730,8 @@ export interface ElectronAPI { deleteConversation: (id: string) => Promise; // Messaging - sendMessage: (conversationId: string, message: string) => Promise<{ success: boolean; message?: string; error?: string }>; + sendMessage: (conversationId: string, message: string, metadata?: ChatSendMetadata) => Promise<{ success: boolean; message?: string; error?: string }>; + addSystemEvent: (conversationId: string, content: string) => Promise<{ success: boolean; error?: string }>; abortMessage: (conversationId: string) => Promise; getHistory: (conversationId: string) => Promise; clearMessages: (conversationId: string) => Promise; diff --git a/src/main/shared/i18n/locales/de.json b/src/main/shared/i18n/locales/de.json index 295e877..0a7fe1f 100644 --- a/src/main/shared/i18n/locales/de.json +++ b/src/main/shared/i18n/locales/de.json @@ -24,6 +24,7 @@ "menu.item.viewMedia": "Medien", "menu.item.toggleSidebar": "Seitenleiste umschalten", "menu.item.togglePanel": "Panel umschalten", + "menu.item.toggleAssistantSidebar": "Assistenz-Seitenleiste umschalten", "menu.item.toggleDevTools": "Entwicklerwerkzeuge umschalten", "menu.item.reload": "Neu laden", "menu.item.forceReload": "Erzwungen neu laden", diff --git a/src/main/shared/i18n/locales/en.json b/src/main/shared/i18n/locales/en.json index d801fc6..6a83c6c 100644 --- a/src/main/shared/i18n/locales/en.json +++ b/src/main/shared/i18n/locales/en.json @@ -24,6 +24,7 @@ "menu.item.viewMedia": "Media", "menu.item.toggleSidebar": "Toggle Sidebar", "menu.item.togglePanel": "Toggle Panel", + "menu.item.toggleAssistantSidebar": "Toggle Assistant Sidebar", "menu.item.toggleDevTools": "Toggle Developer Tools", "menu.item.reload": "Reload", "menu.item.forceReload": "Force Reload", diff --git a/src/main/shared/i18n/locales/es.json b/src/main/shared/i18n/locales/es.json index da940d5..6c32180 100644 --- a/src/main/shared/i18n/locales/es.json +++ b/src/main/shared/i18n/locales/es.json @@ -24,6 +24,7 @@ "menu.item.viewMedia": "Medios", "menu.item.toggleSidebar": "Alternar barra lateral", "menu.item.togglePanel": "Alternar panel", + "menu.item.toggleAssistantSidebar": "Alternar barra del asistente", "menu.item.toggleDevTools": "Alternar herramientas de desarrollo", "menu.item.reload": "Recargar", "menu.item.forceReload": "Forzar recarga", diff --git a/src/main/shared/i18n/locales/fr.json b/src/main/shared/i18n/locales/fr.json index 3badedc..17f1f93 100644 --- a/src/main/shared/i18n/locales/fr.json +++ b/src/main/shared/i18n/locales/fr.json @@ -24,6 +24,7 @@ "menu.item.viewMedia": "Médias", "menu.item.toggleSidebar": "Basculer la barre latérale", "menu.item.togglePanel": "Basculer le panneau", + "menu.item.toggleAssistantSidebar": "Basculer le panneau Assistant", "menu.item.toggleDevTools": "Basculer les outils de développement", "menu.item.reload": "Recharger", "menu.item.forceReload": "Forcer le rechargement", diff --git a/src/main/shared/i18n/locales/it.json b/src/main/shared/i18n/locales/it.json index 407b4fa..15d4e2b 100644 --- a/src/main/shared/i18n/locales/it.json +++ b/src/main/shared/i18n/locales/it.json @@ -24,6 +24,7 @@ "menu.item.viewMedia": "Contenuti media", "menu.item.toggleSidebar": "Attiva/disattiva barra laterale", "menu.item.togglePanel": "Attiva/disattiva pannello", + "menu.item.toggleAssistantSidebar": "Attiva/disattiva barra assistente", "menu.item.toggleDevTools": "Attiva/disattiva strumenti sviluppatore", "menu.item.reload": "Ricarica", "menu.item.forceReload": "Forza ricaricamento", diff --git a/src/main/shared/menuCommands.ts b/src/main/shared/menuCommands.ts index 8d240b1..4125d5a 100644 --- a/src/main/shared/menuCommands.ts +++ b/src/main/shared/menuCommands.ts @@ -19,6 +19,7 @@ export type AppMenuAction = | 'viewMedia' | 'toggleSidebar' | 'togglePanel' + | 'toggleAssistantSidebar' | 'toggleDevTools' | 'reload' | 'forceReload' @@ -103,6 +104,7 @@ export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [ { label: 'menu.item.viewMedia', action: 'viewMedia', accelerator: 'CmdOrCtrl+2' }, { label: 'menu.item.toggleSidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' }, { label: 'menu.item.togglePanel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' }, + { label: 'menu.item.toggleAssistantSidebar', action: 'toggleAssistantSidebar', accelerator: 'CmdOrCtrl+\\' }, { label: 'menu.item.toggleDevTools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' }, { label: '', action: 'view-separator-1', separator: true }, { label: 'menu.item.reload', action: 'reload' }, @@ -156,6 +158,7 @@ export const APP_MENU_ACTION_EVENT_MAP: Partial> = viewMedia: 'menu:viewMedia', toggleSidebar: 'menu:toggleSidebar', togglePanel: 'menu:togglePanel', + toggleAssistantSidebar: 'menu:toggleAssistantSidebar', toggleDevTools: 'menu:toggleDevTools', previewPost: 'menu:previewPost', publishSelected: 'menu:publishSelected', diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b0167fd..1515ec0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel, WindowTitleBar } from './components'; +import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel, WindowTitleBar, AssistantSidebar } from './components'; import { useAppStore, PostData, MediaData, TaskProgress } from './store'; import { loadTabsForProject, saveTabsForProject } from './utils'; import { openSingletonToolTab } from './navigation/tabPolicy'; @@ -33,6 +33,7 @@ const App: React.FC = () => { setLoading, toggleSidebar, togglePanel, + toggleAssistantSidebar, setActiveView, setSelectedPost, setActiveProject, @@ -307,6 +308,12 @@ const App: React.FC = () => { }) || (() => {}) ); + unsubscribers.push( + window.electronAPI?.on('menu:toggleAssistantSidebar', () => { + toggleAssistantSidebar(); + }) || (() => {}) + ); + unsubscribers.push( window.electronAPI?.on('menu:viewPosts', () => { const state = useAppStore.getState(); @@ -538,7 +545,7 @@ const App: React.FC = () => { }; }, []); - const { sidebarVisible } = useAppStore(); + const { sidebarVisible, assistantSidebarVisible } = useAppStore(); return (
@@ -562,6 +569,18 @@ const App: React.FC = () => {
+ {assistantSidebarVisible && ( + + + + )} diff --git a/src/renderer/components/AssistantPanelControls/AssistantPanelControls.css b/src/renderer/components/AssistantPanelControls/AssistantPanelControls.css new file mode 100644 index 0000000..cf3524f --- /dev/null +++ b/src/renderer/components/AssistantPanelControls/AssistantPanelControls.css @@ -0,0 +1,173 @@ +.assistant-panel-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.assistant-panel-metric { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 8px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; +} + +.assistant-panel-metric-label { + font-size: 12px; + opacity: 0.85; +} + +.assistant-panel-metric-value { + font-size: 14px; +} + +.assistant-panel-table { + width: 100%; + border-collapse: collapse; +} + +.assistant-panel-table th, +.assistant-panel-table td { + border: 1px solid var(--vscode-panel-border); + padding: 6px; + font-size: 12px; + text-align: left; +} + +.assistant-panel-widget-block { + display: flex; + flex-direction: column; + gap: 6px; +} + +.assistant-panel-widget-label { + font-size: 12px; + opacity: 0.9; +} + +.assistant-panel-widget-input { + width: 100%; + padding: 8px; +} + +.assistant-panel-checkbox { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; +} + +.assistant-panel-chart { + display: flex; + flex-direction: column; + gap: 6px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 8px; +} + +.assistant-panel-chart-title { + margin: 0; + font-weight: 600; +} + +.assistant-panel-chart-type { + font-size: 11px; + text-transform: uppercase; + opacity: 0.7; +} + +.assistant-panel-chart-item { + display: grid; + grid-template-columns: minmax(48px, auto) 1fr auto; + gap: 8px; + align-items: center; + font-size: 12px; +} + +.assistant-panel-chart-item progress { + width: 100%; +} + +.assistant-panel-form { + display: flex; + flex-direction: column; + gap: 8px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 8px; +} + +.assistant-panel-form-title { + margin: 0; + font-weight: 600; +} + +.assistant-panel-card { + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.assistant-panel-card h4, +.assistant-panel-card p { + margin: 0; +} + +.assistant-panel-card-subtitle { + font-size: 12px; + opacity: 0.8; +} + +.assistant-panel-card-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.assistant-panel-image { + margin: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.assistant-panel-image img { + max-width: 100%; + border-radius: 6px; + border: 1px solid var(--vscode-panel-border); +} + +.assistant-panel-image figcaption { + font-size: 12px; + opacity: 0.85; +} + +.assistant-panel-tabs { + display: flex; + flex-direction: column; + gap: 8px; +} + +.assistant-panel-tab-strip { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.assistant-panel-tab-button.active { + border-color: var(--vscode-focusBorder); +} + +.assistant-panel-tab-panel { + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} \ No newline at end of file diff --git a/src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx b/src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx new file mode 100644 index 0000000..d5da72b --- /dev/null +++ b/src/renderer/components/AssistantPanelControls/AssistantPanelControls.tsx @@ -0,0 +1,295 @@ +import React, { useState } from 'react'; +import type { AssistantPanelElement } from '../../navigation/assistantPanelSpec'; +import './AssistantPanelControls.css'; + +interface AssistantPanelControlsProps { + elements: AssistantPanelElement[]; + onAction: (action: string, payload?: Record) => void; +} + +export const AssistantPanelControls: React.FC = ({ elements, onAction }) => { + const [widgetValues, setWidgetValues] = useState>({}); + const [activeTabByWidget, setActiveTabByWidget] = useState>({}); + + const setWidgetValue = (key: string, value: unknown) => { + setWidgetValues((previous) => ({ + ...previous, + [key]: value, + })); + }; + + const getWidgetValue = (key: string, defaultValue?: unknown) => + Object.prototype.hasOwnProperty.call(widgetValues, key) ? widgetValues[key] : defaultValue; + + const renderInputControl = ( + key: string, + label: string, + inputType: 'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number', + options?: Array<{ label: string; value: string }>, + placeholder?: string, + defaultValue?: string | number | boolean, + ) => { + if (inputType === 'textarea') { + return ( +