From 7de07263fc6d55b0626479749acfdc75fc1d3706 Mon Sep 17 00:00:00 2001 From: Aleel101 Date: Sun, 26 Apr 2026 07:24:29 -0400 Subject: [PATCH 1/7] fix: update package dependencies and enhance window management - Downgraded `@tailwindcss/postcss` and `tailwindcss` to version 4.0.0. - Downgraded `electron` from 39.8.9 to 39.8.8. - Added IPC handlers for window management (minimize, maximize, close). - Updated window creation to remove the native title bar and manage menu visibility based on platform. - Enhanced state management in onboarding and design generation processes. - Introduced support for fast model configuration in onboarding IPC. - Improved design generation state handling to support multiple active generations. - Refactored various components to utilize the new state management logic. --- apps/desktop/package.json | 6 +- apps/desktop/src/main/index.ts | 32 +- apps/desktop/src/main/onboarding-ipc.ts | 20 + apps/desktop/src/preload/index.ts | 5 + apps/desktop/src/renderer/src/App.tsx | 2 +- .../renderer/src/components/ConsolePanel.tsx | 135 +++++ .../renderer/src/components/FilesPanel.tsx | 5 +- .../renderer/src/components/FilesTabView.tsx | 8 +- .../renderer/src/components/HistoryPanel.tsx | 160 ++++++ .../src/components/InlineCommentComposer.tsx | 2 +- .../src/components/ModelSwitcher.test.ts | 18 +- .../renderer/src/components/ModelSwitcher.tsx | 523 +++++++++++++----- .../renderer/src/components/PageTabBar.tsx | 43 ++ .../renderer/src/components/PreviewPane.tsx | 176 +++++- .../src/components/PreviewToolbar.tsx | 43 +- .../src/renderer/src/components/Settings.tsx | 305 ++++++++++ .../src/renderer/src/components/Sidebar.tsx | 32 +- .../src/renderer/src/components/TopBar.tsx | 227 ++++---- .../src/components/WindowControls.tsx | 89 +++ .../src/components/chat/AssistantText.tsx | 32 +- .../src/components/chat/ChatMessageList.tsx | 290 +++++++--- .../src/components/chat/CommentChipBar.tsx | 2 +- .../components/chat/GenerationStatusBar.tsx | 89 +++ .../src/components/chat/PromptInput.tsx | 63 --- .../src/components/chat/StickyTodoHeader.tsx | 86 +++ .../src/components/chat/UserMessage.tsx | 20 +- .../src/components/chat/WorkingCard.tsx | 119 +++- .../src/renderer/src/hooks/useAgentStream.ts | 148 +++-- apps/desktop/src/renderer/src/store.ts | 208 +++++-- .../src/renderer/src/views/hub/DesignGrid.tsx | 8 + .../src/renderer/src/views/hub/RecentTab.tsx | 2 +- packages/core/package.json | 4 +- packages/core/src/agent.ts | 35 +- packages/i18n/src/locales/en.json | 15 + packages/providers/package.json | 2 +- packages/runtime/src/iframe-errors.ts | 20 + packages/runtime/src/index.ts | 33 +- packages/runtime/src/overlay.ts | 32 ++ packages/shared/src/config.ts | 5 + packages/ui/package.json | 4 +- packages/ui/src/tokens.css | 2 +- website/package.json | 4 +- 42 files changed, 2452 insertions(+), 602 deletions(-) create mode 100644 apps/desktop/src/renderer/src/components/ConsolePanel.tsx create mode 100644 apps/desktop/src/renderer/src/components/HistoryPanel.tsx create mode 100644 apps/desktop/src/renderer/src/components/PageTabBar.tsx create mode 100644 apps/desktop/src/renderer/src/components/WindowControls.tsx create mode 100644 apps/desktop/src/renderer/src/components/chat/GenerationStatusBar.tsx create mode 100644 apps/desktop/src/renderer/src/components/chat/StickyTodoHeader.tsx diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ff8b37cc..3f2ce1ba 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -38,20 +38,20 @@ "zustand": "^5.0.2" }, "devDependencies": { - "@tailwindcss/postcss": "^4.2.4", + "@tailwindcss/postcss": "^4.0.0", "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.10.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", - "electron": "^39.8.9", + "electron": "^39.8.8", "electron-builder": "^26.8.1", "electron-builder-squirrel-windows": "26.8.1", "electron-vite": "^2.3.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", - "tailwindcss": "^4.2.4", + "tailwindcss": "^4.0.0", "typescript": "^5.7.2", "vite": "^6.0.5", "vitest": "^2.1.8" diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index d06d8e43..51888985 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -129,14 +129,27 @@ const USE_AGENT_RUNTIME = (() => { const IS_VITEST = process.env['VITEST'] === 'true'; +/** No OS title bar; renderer draws TopBar with -webkit-app-region. Register once (not in createWindow — macOS can call createWindow again on activate). */ +function registerWindowChromeIpc(): void { + ipcMain.on('window:minimize', () => mainWindow?.minimize()); + ipcMain.on('window:maximize', () => { + if (!mainWindow) return; + if (mainWindow.isMaximized()) mainWindow.unmaximize(); + else mainWindow.maximize(); + }); + ipcMain.on('window:close', () => mainWindow?.close()); +} + function createWindow(): void { + // frame: false removes the native title bar and window controls; we provide + // a custom top bar in the renderer (see TopBar, WindowControls). mainWindow = new BrowserWindow({ width: 1280, height: 820, minWidth: 960, minHeight: 640, - autoHideMenuBar: process.platform !== 'darwin', - titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', + frame: false, + autoHideMenuBar: true, backgroundColor: BRAND.backgroundColor, icon: join(__dirname, '../../resources/icon.png'), show: false, @@ -148,7 +161,12 @@ function createWindow(): void { }, }); - mainWindow.on('ready-to-show', () => mainWindow?.show()); + mainWindow.on('ready-to-show', () => { + if (process.platform === 'win32' || process.platform === 'linux') { + mainWindow?.setMenuBarVisibility(false); + } + mainWindow?.show(); + }); // Null the reference on close so stale IPC sends from async emitters // (autoUpdater, long-running generate runs) become clean no-ops rather // than throwing "Object has been destroyed" on a discarded webContents. @@ -1084,7 +1102,12 @@ function registerIpcHandlers(db: Database | null): void { // Inline-comment edits don't need to be tied to whatever provider was // pinned in the original generate; resolve fresh against the canonical // active provider so a switch in Settings takes effect immediately. - const hint = payload.model ?? { provider: cfg.provider, modelId: cfg.modelPrimary }; + // Prefer modelFast when configured — it's cheaper for quick edits. + const fastModelId = cfg.modelFast ?? null; + const hint = payload.model ?? { + provider: cfg.provider, + modelId: fastModelId ?? cfg.modelPrimary, + }; const active = resolveActiveModel(cfg, hint); const allowKeyless = active.allowKeyless; const apiKey = await resolveApiKeyForActive(active.model.provider, allowKeyless); @@ -1359,6 +1382,7 @@ if (!IS_VITEST) { registerDiagnosticsIpc(diagnosticsDb); setupAutoUpdater(); registerAppMenu(); + registerWindowChromeIpc(); createWindow(); void scheduleStartupUpdateCheck(); diff --git a/apps/desktop/src/main/onboarding-ipc.ts b/apps/desktop/src/main/onboarding-ipc.ts index c1803768..22a7c03b 100644 --- a/apps/desktop/src/main/onboarding-ipc.ts +++ b/apps/desktop/src/main/onboarding-ipc.ts @@ -180,6 +180,7 @@ function toState(cfg: Config | null): OnboardingState { hasKey: false, provider: null, modelPrimary: null, + modelFast: null, baseUrl: null, designSystem: null, }; @@ -191,6 +192,7 @@ function toState(cfg: Config | null): OnboardingState { hasKey: false, provider: active, modelPrimary: null, + modelFast: null, baseUrl: null, designSystem: cfg.designSystem ?? null, }; @@ -199,6 +201,7 @@ function toState(cfg: Config | null): OnboardingState { hasKey: true, provider: active, modelPrimary: cfg.activeModel, + modelFast: cfg.modelFast ?? null, baseUrl: cfg.providers[active]?.baseUrl ?? null, designSystem: cfg.designSystem ?? null, }; @@ -1153,6 +1156,23 @@ export function registerOnboardingIpc(): void { }, ); + ipcMain.handle( + 'config:v1:set-fast-model', + async (_e, raw: unknown): Promise => { + const input = raw as { modelFast: string | null }; + if (typeof input !== 'object' || input === null || !('modelFast' in input)) { + throw new CodesignError( + 'config:v1:set-fast-model expects { modelFast: string | null }', + ERROR_CODES.IPC_BAD_INPUT, + ); + } + const modelFast = input.modelFast === '' ? null : input.modelFast; + cachedConfig = hydrateConfig({ ...cachedConfig, modelFast: modelFast ?? undefined }); + await writeConfig(cachedConfig); + return toState(cachedConfig); + }, + ); + ipcMain.handle( 'config:v1:detect-external-configs', async (): Promise => { diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 9cc4fe11..96330cff 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -314,6 +314,8 @@ const api = { 'config:v1:set-active-provider-and-model', input, ) as Promise, + setFastModel: (modelFast: string | null) => + ipcRenderer.invoke('config:v1:set-fast-model', { modelFast }) as Promise, testEndpoint: (input: { wire: WireApi; baseUrl: string; @@ -572,6 +574,9 @@ const api = { }, openExternal: (url: string) => ipcRenderer.invoke('codesign:v1:open-external', url) as Promise, + minimize: () => ipcRenderer.send('window:minimize'), + maximize: () => ipcRenderer.send('window:maximize'), + close: () => ipcRenderer.send('window:close'), }; contextBridge.exposeInMainWorld('codesign', api); diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 3d2e9283..0829812e 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -29,7 +29,7 @@ export function App() { const switchDesign = useCodesignStore((s) => s.switchDesign); const sendPrompt = useCodesignStore((s) => s.sendPrompt); const isGenerating = useCodesignStore( - (s) => s.isGenerating && s.generatingDesignId === s.currentDesignId, + (s) => s.currentDesignId !== null && s.activeGenerations.has(s.currentDesignId), ); const setView = useCodesignStore((s) => s.setView); const view = useCodesignStore((s) => s.view); diff --git a/apps/desktop/src/renderer/src/components/ConsolePanel.tsx b/apps/desktop/src/renderer/src/components/ConsolePanel.tsx new file mode 100644 index 00000000..2b8aa779 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/ConsolePanel.tsx @@ -0,0 +1,135 @@ +import type { ConsoleLevel } from '@open-codesign/runtime'; +import { ChevronDown, ChevronUp, Terminal, Trash2 } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import type { IframeConsoleLogEntry } from '../store'; +import { useCodesignStore } from '../store'; + +const LEVEL_STYLE: Record = { + log: 'text-[var(--color-text-primary)]', + info: 'text-[#3b82f6]', + warn: 'text-[#f59e0b]', + error: 'text-[var(--color-error)]', + debug: 'text-[var(--color-text-muted)]', +}; + +const LEVEL_BG: Record = { + log: '', + info: '', + warn: 'bg-[color-mix(in_srgb,#f59e0b_6%,transparent)]', + error: 'bg-[color-mix(in_srgb,var(--color-error)_6%,transparent)]', + debug: '', +}; + +function ConsoleRow({ entry }: { entry: IframeConsoleLogEntry }) { + const time = new Date(entry.timestamp).toTimeString().slice(0, 8); + const text = entry.args.join(' '); + return ( +
+ {time} + + {entry.level} + + {text} +
+ ); +} + +export function ConsolePanel() { + const logs = useCodesignStore((s) => s.consoleLogs); + const clearConsoleLogs = useCodesignStore((s) => s.clearConsoleLogs); + const [open, setOpen] = useState(false); + const bottomRef = useRef(null); + const scrollRef = useRef(null); + const stickyRef = useRef(true); + + const errorCount = logs.filter((l) => l.level === 'error').length; + const warnCount = logs.filter((l) => l.level === 'warn').length; + + useEffect(() => { + if (!open || !stickyRef.current) return; + bottomRef.current?.scrollIntoView({ block: 'end' }); + }, [logs.length, open]); + + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + function onScroll() { + if (!el) return; + stickyRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 32; + } + el.addEventListener('scroll', onScroll, { passive: true }); + return () => el.removeEventListener('scroll', onScroll); + }, []); + + if (logs.length === 0 && !open) return null; + + const badge = + errorCount > 0 ? ( + + {errorCount} + + ) : warnCount > 0 ? ( + + {warnCount} + + ) : logs.length > 0 ? ( + + {logs.length} + + ) : null; + + return ( +
+ {/* Header */} +
+ + {logs.length > 0 && ( + + )} +
+ + {open && ( +
+ {logs.length === 0 ? ( +
+ No output yet. +
+ ) : ( + logs.map((entry, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: console logs are append-only, index is stable within a session + + )) + )} +
+
+ )} +
+ ); +} diff --git a/apps/desktop/src/renderer/src/components/FilesPanel.tsx b/apps/desktop/src/renderer/src/components/FilesPanel.tsx index 5b4627e3..4351eda9 100644 --- a/apps/desktop/src/renderer/src/components/FilesPanel.tsx +++ b/apps/desktop/src/renderer/src/components/FilesPanel.tsx @@ -23,8 +23,7 @@ export function FilesPanel() { const t = useT(); const currentDesignId = useCodesignStore((s) => s.currentDesignId); const designs = useCodesignStore((s) => s.designs); - const isGenerating = useCodesignStore((s) => s.isGenerating); - const generatingDesignId = useCodesignStore((s) => s.generatingDesignId); + const activeGenerations = useCodesignStore((s) => s.activeGenerations); const openFileTab = useCodesignStore((s) => s.openCanvasFileTab); const requestWorkspaceRebind = useCodesignStore((s) => s.requestWorkspaceRebind); const { files, loading } = useDesignFiles(currentDesignId); @@ -33,7 +32,7 @@ export function FilesPanel() { const currentDesign = designs.find((d) => d.id === currentDesignId); const workspacePath = currentDesign?.workspacePath ?? null; - const isCurrentDesignGenerating = isGenerating && generatingDesignId === currentDesignId; + const isCurrentDesignGenerating = currentDesignId !== null && activeGenerations.has(currentDesignId); useEffect(() => { if (!workspacePath || !currentDesignId) { diff --git a/apps/desktop/src/renderer/src/components/FilesTabView.tsx b/apps/desktop/src/renderer/src/components/FilesTabView.tsx index 9aa1a1b4..5fd21aed 100644 --- a/apps/desktop/src/renderer/src/components/FilesTabView.tsx +++ b/apps/desktop/src/renderer/src/components/FilesTabView.tsx @@ -17,15 +17,15 @@ function WorkspaceSection() { const t = useT(); const currentDesignId = useCodesignStore((s) => s.currentDesignId); const designs = useCodesignStore((s) => s.designs); - const isGenerating = useCodesignStore((s) => s.isGenerating); - const generatingDesignId = useCodesignStore((s) => s.generatingDesignId); + const activeGenerations = useCodesignStore((s) => s.activeGenerations); const requestWorkspaceRebind = useCodesignStore((s) => s.requestWorkspaceRebind); const [picking, setPicking] = useState(false); const [folderExists, setFolderExists] = useState(null); const currentDesign = designs.find((d) => d.id === currentDesignId); const workspacePath = currentDesign?.workspacePath ?? null; - const isCurrentDesignGenerating = isGenerating && generatingDesignId === currentDesignId; + const isCurrentDesignGenerating = + currentDesignId !== null && activeGenerations.has(currentDesignId); const disabled = picking || isCurrentDesignGenerating; useEffect(() => { @@ -317,7 +317,7 @@ export function FilesTabView() { pushIframeError(`SET_MODE postMessage failed: ${reason}`); } }} - className="w-full h-full bg-white border-0 block" + className="w-full h-full bg-transparent border-0 block" /> ) : (
diff --git a/apps/desktop/src/renderer/src/components/HistoryPanel.tsx b/apps/desktop/src/renderer/src/components/HistoryPanel.tsx new file mode 100644 index 00000000..5ec4e0b5 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/HistoryPanel.tsx @@ -0,0 +1,160 @@ +import type { DesignSnapshot } from '@open-codesign/shared'; +import { buildSrcdoc } from '@open-codesign/runtime'; +import { Clock, RotateCcw, X } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { useCodesignStore } from '../store'; + +interface HistoryPanelProps { + onClose: () => void; +} + +function formatDate(iso: string): string { + const d = new Date(iso); + return d.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +export function HistoryPanel({ onClose }: HistoryPanelProps) { + const currentDesignId = useCodesignStore((s) => s.currentDesignId); + const setPreviewHtml = useCodesignStore((s) => s.setPreviewHtml); + const pushToast = useCodesignStore((s) => s.pushToast); + + const [snapshots, setSnapshots] = useState([]); + const [loading, setLoading] = useState(true); + const [selected, setSelected] = useState(null); + const [restoring, setRestoring] = useState(false); + + useEffect(() => { + if (!currentDesignId || !window.codesign) return; + setLoading(true); + window.codesign.snapshots + .list(currentDesignId) + .then((rows) => { + setSnapshots(rows); + setSelected(rows[0] ?? null); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [currentDesignId]); + + const previewSrc = useMemo( + () => (selected ? buildSrcdoc(selected.artifactSource) : null), + [selected], + ); + + async function handleRestore() { + if (!selected || !currentDesignId || !window.codesign) return; + setRestoring(true); + try { + await window.codesign.snapshots.create({ + designId: currentDesignId, + parentId: selected.id, + type: 'fork', + prompt: selected.prompt, + artifactType: selected.artifactType, + artifactSource: selected.artifactSource, + }); + setPreviewHtml(selected.artifactSource); + pushToast({ variant: 'success', title: 'Restored to selected version' }); + onClose(); + } catch (err) { + pushToast({ + variant: 'error', + title: 'Restore failed', + description: err instanceof Error ? err.message : String(err), + }); + } finally { + setRestoring(false); + } + } + + return ( +
+ {/* Header */} +
+ + + Version History + + +
+ +
+ {/* Snapshot list */} +
+ {loading ? ( +
Loading…
+ ) : snapshots.length === 0 ? ( +
No history yet.
+ ) : ( + snapshots.map((snap, i) => ( + + )) + )} +
+ + {/* Preview area */} +
+ {previewSrc ? ( +