diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 35784fb..4d87e3f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -10,7 +10,7 @@ import { NewSessionModal } from '@renderer/components/NewSessionModal'; import { ProjectSettingsPanel } from '@renderer/components/ProjectSettingsPanel'; import { BookmarksPanel } from '@renderer/components/BookmarksPanel'; import { SessionSearchPanel } from '@renderer/components/SessionSearchPanel'; -import { SettingsPanel } from '@renderer/components/SettingsPanel'; +import { SettingsPanel, type SettingsSection } from '@renderer/components/SettingsPanel'; import { Sidebar } from '@renderer/components/Sidebar'; import { TerminalPanel, DEFAULT_HEIGHT as DEFAULT_TERMINAL_HEIGHT, MIN_HEIGHT as MIN_TERMINAL_HEIGHT } from '@renderer/components/TerminalPanel'; import { resolveChatToolingSettings } from '@renderer/lib/chatTooling'; @@ -46,6 +46,7 @@ import { isScratchpadProject, SCRATCHPAD_PROJECT_ID } from '@shared/domain/proje import { applySessionModelConfig } from '@shared/domain/session'; import type { AppearanceTheme, LspProfileDefinition, McpServerDefinition } from '@shared/domain/tooling'; import type { WorkspaceState } from '@shared/domain/workspace'; +import type { UpdateStatus } from '@shared/contracts/ipc'; import { createId, nowIso } from '@shared/utils/ids'; function createDraftPattern(defaultModelId: string, defaultReasoningEffort: PatternDefinition['agents'][0]['reasoningEffort']): PatternDefinition { @@ -113,6 +114,8 @@ export default function App() { const [activeSubagents, setActiveSubagents] = useState({}); const [showSettings, setShowSettings] = useState(false); + const [settingsSection, setSettingsSection] = useState(); + const [updateStatus, setUpdateStatus] = useState({ state: 'idle' }); const [projectSettingsId, setProjectSettingsId] = useState(); const [newSessionProjectId, setNewSessionProjectId] = useState(); const [showDiscoveryModal, setShowDiscoveryModal] = useState(false); @@ -188,6 +191,12 @@ export default function App() { }; }, [api]); + // Subscribe to auto-update status pushes from the main process + useEffect(() => { + const off = api.onUpdateStatus((status) => setUpdateStatus(status)); + return off; + }, [api]); + // Apply theme to the document root const themeSetting: AppearanceTheme = workspace?.settings.theme ?? 'dark'; useTheme(themeSetting); @@ -494,6 +503,15 @@ export default function App() { } }, [api, workspace]); + const handleOpenSettingsAt = useCallback((section?: SettingsSection) => { + setSettingsSection(section); + setShowSettings(true); + }, []); + + const handleInstallUpdate = useCallback(() => { + void api.installUpdate(); + }, [api]); + // Listen for tray "Quick Scratchpad" action const scratchpadRef = useRef(handleCreateScratchpad); scratchpadRef.current = handleCreateScratchpad; @@ -637,8 +655,9 @@ export default function App() { const overlay = showSettings ? ( setShowSettings(false)} + onClose={() => { setShowSettings(false); setSettingsSection(undefined); }} onDeleteLspProfile={async (id) => { await api.deleteLspProfile(id); }} @@ -741,6 +760,9 @@ export default function App() { onRefreshGitContext={(projectId) => { void api.refreshProjectGitContext(projectId); }} + updateStatus={updateStatus} + onViewUpdateDetails={() => handleOpenSettingsAt('troubleshooting')} + onInstallUpdate={handleInstallUpdate} workspace={workspace} /> } diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx index b478e02..01d66de 100644 --- a/src/renderer/components/SettingsPanel.tsx +++ b/src/renderer/components/SettingsPanel.tsx @@ -29,6 +29,7 @@ interface SettingsPanelProps { toolingSettings: WorkspaceToolingSettings; discoveredUserTooling: DiscoveredToolingState; isRefreshingCapabilities: boolean; + initialSection?: SettingsSection; onRefreshCapabilities: () => void; onClose: () => void; onSavePattern: (pattern: PatternDefinition) => Promise; @@ -51,7 +52,7 @@ interface SettingsPanelProps { onGetQuota?: () => Promise>; } -type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting'; +export type SettingsSection = 'appearance' | 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting'; interface NavItem { id: SettingsSection; @@ -111,6 +112,7 @@ export function SettingsPanel({ toolingSettings, discoveredUserTooling, isRefreshingCapabilities, + initialSection, onRefreshCapabilities, onClose, onSavePattern, @@ -132,7 +134,7 @@ export function SettingsPanel({ onResolveUserDiscoveredTooling, onGetQuota, }: SettingsPanelProps) { - const [activeSection, setActiveSection] = useState('appearance'); + const [activeSection, setActiveSection] = useState(initialSection ?? 'appearance'); const [editingPattern, setEditingPattern] = useState(null); const [editingMcpServer, setEditingMcpServer] = useState(null); const [editingLspProfile, setEditingLspProfile] = useState(null); diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index 5f71204..8354e13 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -33,7 +33,9 @@ import { isScratchpadProject, type ProjectRecord, type ProjectGitContext } from import { listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling'; import type { SessionRecord } from '@shared/domain/session'; import { querySessions } from '@shared/domain/sessionLibrary'; +import type { UpdateStatus } from '@shared/contracts/ipc'; import type { WorkspaceState } from '@shared/domain/workspace'; +import { UpdateBanner } from '@renderer/components/ui'; interface SidebarProps { workspace: WorkspaceState; @@ -50,6 +52,9 @@ interface SidebarProps { onSetSessionArchived: (sessionId: string, isArchived: boolean) => void; onDeleteSession: (sessionId: string) => void; onRefreshGitContext: (projectId: string) => void; + updateStatus?: UpdateStatus; + onViewUpdateDetails?: () => void; + onInstallUpdate?: () => void; } /* ── Mode icon + accent colour mapping ─────────────────────── */ @@ -540,6 +545,9 @@ export function Sidebar({ onSetSessionArchived, onDeleteSession, onRefreshGitContext, + updateStatus, + onViewUpdateDetails, + onInstallUpdate, }: SidebarProps) { const scratchpadProject = workspace.projects.find((project) => isScratchpadProject(project)); const userProjects = workspace.projects.filter((project) => !isScratchpadProject(project)); @@ -752,6 +760,15 @@ export function Sidebar({ )} + {/* Update notification banner */} + {updateStatus && onViewUpdateDetails && onInstallUpdate && ( + + )} + {/* Footer */} {userProjects.length > 0 && (
diff --git a/src/renderer/components/ui/UpdateBanner.tsx b/src/renderer/components/ui/UpdateBanner.tsx new file mode 100644 index 0000000..835733d --- /dev/null +++ b/src/renderer/components/ui/UpdateBanner.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import { ArrowDownToLine, Download, RefreshCw, Sparkles, X } from 'lucide-react'; + +import type { UpdateStatus } from '@shared/contracts/ipc'; + +export interface UpdateBannerProps { + status: UpdateStatus; + onViewDetails: () => void; + onInstallUpdate: () => void; +} + +export function UpdateBanner({ status, onViewDetails, onInstallUpdate }: UpdateBannerProps) { + const [dismissed, setDismissed] = useState(false); + + const isActionable = + status.state === 'available' || + status.state === 'downloading' || + status.state === 'downloaded'; + + // Nothing to show + if (!isActionable) return null; + + // Allow dismissal for transient states, never for downloaded + if (dismissed && status.state !== 'downloaded') return null; + + const version = status.version ? `v${status.version}` : ''; + + if (status.state === 'downloaded') { + return ( +
+ +
+ ); + } + + // available / downloading + const isDownloading = status.state === 'downloading'; + const percent = status.downloadProgress ? Math.round(status.downloadProgress.percent) : 0; + + return ( +
+
+ + + + {/* Download progress bar */} + {isDownloading && percent > 0 && ( +
+
+
+ )} +
+
+ ); +} diff --git a/src/renderer/components/ui/index.ts b/src/renderer/components/ui/index.ts index c33b695..986155c 100644 --- a/src/renderer/components/ui/index.ts +++ b/src/renderer/components/ui/index.ts @@ -7,3 +7,5 @@ export { TextInput } from './TextInput'; export { TextareaInput } from './TextareaInput'; export { SelectInput } from './SelectInput'; export { InfoCallout } from './InfoCallout'; +export type { UpdateBannerProps } from './UpdateBanner'; +export { UpdateBanner } from './UpdateBanner'; diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 0b0d58c..57297c5 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -626,6 +626,19 @@ body { animation: banner-slide-in 0.25s cubic-bezier(0.16, 1, 0.3, 1); } +/* ── Update banner slide-up ──────────────────────────────────── */ + +@keyframes update-banner-in { + from { + opacity: 0; + transform: translateY(100%); + } +} + +.update-banner-enter { + animation: update-banner-in 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + /* ── Thinking process section ────────────────────────────────── */ @keyframes thinking-process-in { @@ -651,6 +664,7 @@ body { .msg-actions-enter, .session-item-enter, .banner-slide-enter, + .update-banner-enter, .thinking-process-enter { animation: none; }