diff --git a/apps/dashboard/src/components/ChatSidebar.tsx b/apps/dashboard/src/components/ChatSidebar.tsx index 38f1cf80abd..87af497237a 100644 --- a/apps/dashboard/src/components/ChatSidebar.tsx +++ b/apps/dashboard/src/components/ChatSidebar.tsx @@ -323,7 +323,7 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) { ) : undefined } - className="self-start min-w-0 px-0 py-0 normal-case tracking-normal text-sm font-medium hover:underline disabled:no-underline" + className="self-start min-w-0 px-0 py-0 normal-case tracking-normal text-sm font-medium underline-offset-4 decoration-current/40 hover:underline disabled:no-underline" title={info.model ?? "switch model"} > {modelLabel} diff --git a/apps/dashboard/src/components/Markdown.tsx b/apps/dashboard/src/components/Markdown.tsx index bef0804e7c4..c80ea08d11e 100644 --- a/apps/dashboard/src/components/Markdown.tsx +++ b/apps/dashboard/src/components/Markdown.tsx @@ -331,7 +331,7 @@ function InlineContent({ href={node.href} target="_blank" rel="noreferrer" - className="text-primary underline underline-offset-2 decoration-primary/30 hover:decoration-primary/60 transition-colors" + className="text-primary underline underline-offset-4 decoration-current/40 transition-colors" > {node.text} diff --git a/apps/dashboard/src/pages/AnalyticsPage.tsx b/apps/dashboard/src/pages/AnalyticsPage.tsx index 4896e760636..e46a1806e6d 100644 --- a/apps/dashboard/src/pages/AnalyticsPage.tsx +++ b/apps/dashboard/src/pages/AnalyticsPage.tsx @@ -510,7 +510,7 @@ export default function AnalyticsPage() { dashboard.show_token_analytics: true {" "} - in Config. + in Config.

diff --git a/apps/dashboard/src/pages/EnvPage.tsx b/apps/dashboard/src/pages/EnvPage.tsx index 1c457da0583..a06df1a5e15 100644 --- a/apps/dashboard/src/pages/EnvPage.tsx +++ b/apps/dashboard/src/pages/EnvPage.tsx @@ -148,7 +148,7 @@ function EnvVarRow({ href={info.url} target="_blank" rel="noreferrer" - className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline" + className="inline-flex items-center gap-1 text-[0.65rem] text-primary underline-offset-4 decoration-current/40 hover:underline" > {t.env.getKey} @@ -184,7 +184,7 @@ function EnvVarRow({ href={info.url} target="_blank" rel="noreferrer" - className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline" + className="inline-flex items-center gap-1 text-[0.65rem] text-primary underline-offset-4 decoration-current/40 hover:underline" > {t.env.getKey} @@ -217,7 +217,7 @@ function EnvVarRow({ href={info.url} target="_blank" rel="noreferrer" - className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline" + className="inline-flex items-center gap-1 text-[0.65rem] text-primary underline-offset-4 decoration-current/40 hover:underline" > {t.env.getKey} @@ -407,7 +407,7 @@ function ProviderGroupCard({ href={keyUrl} target="_blank" rel="noreferrer" - className="inline-flex items-center gap-1 text-[0.65rem] text-primary hover:underline" + className="inline-flex items-center gap-1 text-[0.65rem] text-primary underline-offset-4 decoration-current/40 hover:underline" onClick={(e) => e.stopPropagation()} > {t.env.getKey} diff --git a/apps/dashboard/src/pages/ModelsPage.tsx b/apps/dashboard/src/pages/ModelsPage.tsx index f09104d4241..98c5be1a617 100644 --- a/apps/dashboard/src/pages/ModelsPage.tsx +++ b/apps/dashboard/src/pages/ModelsPage.tsx @@ -927,7 +927,7 @@ export default function ModelsPage() { …) and provider retries, so they diverge from your provider bill. Enable{" "} dashboard.show_token_analytics{" "} - in Config to + in Config to show the local debug estimate anyway.

)} diff --git a/apps/dashboard/src/pages/PluginsPage.tsx b/apps/dashboard/src/pages/PluginsPage.tsx index 290e5e04f0f..6696f3580c1 100644 --- a/apps/dashboard/src/pages/PluginsPage.tsx +++ b/apps/dashboard/src/pages/PluginsPage.tsx @@ -346,7 +346,7 @@ export default function PluginsPage() { {!m.tab?.hidden ? ( - + diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 24dabd72940..0d1c77d514c 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -617,11 +617,10 @@ function findSystemPython() { if (pyExe) { for (const version of SUPPORTED_VERSIONS) { try { - const out = execFileSync( - pyExe, - [`-${version}`, '-c', 'import sys; print(sys.executable)'], - { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] } - ) + const out = execFileSync(pyExe, [`-${version}`, '-c', 'import sys; print(sys.executable)'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }) const candidate = out.trim() if (candidate && fileExists(candidate)) return candidate } catch { diff --git a/apps/desktop/src/app/agents/index.tsx b/apps/desktop/src/app/agents/index.tsx index cc5afa17f42..4bcf76e46e6 100644 --- a/apps/desktop/src/app/agents/index.tsx +++ b/apps/desktop/src/app/agents/index.tsx @@ -36,12 +36,7 @@ function statusGlyph(status: SubagentStatus): ReactNode { return } - return ( - - ) + return } const STREAM_TONE: Record = { @@ -65,7 +60,11 @@ function streamGlyph(entry: SubagentStreamEntry): ReactNode { } if (entry.kind === 'thinking') { - return + return ( + + … + + ) } return @@ -103,8 +102,13 @@ export function AgentsView({ onClose }: AgentsViewProps) { } const fmtDuration = (seconds?: number) => { - if (!seconds || seconds <= 0) return '' - if (seconds < 60) return `${seconds.toFixed(1)}s` + if (!seconds || seconds <= 0) { + return '' + } + + if (seconds < 60) { + return `${seconds.toFixed(1)}s` + } const m = Math.floor(seconds / 60) const s = Math.round(seconds % 60) @@ -113,18 +117,29 @@ const fmtDuration = (seconds?: number) => { } const fmtTokens = (value?: number) => { - if (!value) return '' + if (!value) { + return '' + } return value >= 1000 ? `${(value / 1000).toFixed(1)}k tok` : `${value} tok` } const fmtAge = (updatedAt: number, nowMs: number) => { const s = Math.max(0, Math.round((nowMs - updatedAt) / 1000)) - if (s < 2) return 'now' - if (s < 60) return `${s}s ago` + + if (s < 2) { + return 'now' + } + + if (s < 60) { + return `${s}s ago` + } const m = Math.floor(s / 60) - if (m < 60) return `${m}m ago` + + if (m < 60) { + return `${m}m ago` + } return `${Math.floor(m / 60)}h ago` } @@ -152,12 +167,14 @@ function groupDelegations(roots: readonly SubagentNode[]): RootGroup[] { if (prev && sameShape && closeInTime && uniqueStep) { prev.nodes.push(node) + continue } if (node.taskCount > 1) { n += 1 groups.push({ id: `delegation-${n}`, label: `Delegation ${n}`, nodes: [node], taskCount: node.taskCount }) + continue } @@ -180,7 +197,9 @@ function SubagentTree({ tree }: { tree: SubagentNode[] }) { const cost = flat.reduce((sum, n) => sum + (n.costUsd ?? 0), 0) useEffect(() => { - if (active <= 0 || typeof window === 'undefined') return + if (active <= 0 || typeof window === 'undefined') { + return + } const id = window.setInterval(() => setNowMs(Date.now()), 500) @@ -261,10 +280,7 @@ function StreamLine({ const tone = entry.isError ? 'text-destructive' : STREAM_TONE[entry.kind] return ( -
+
{streamGlyph(entry)} {entry.text} @@ -283,13 +299,17 @@ function StreamLine({ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: number; nowMs: number }) { const running = node.status === 'running' || node.status === 'queued' const elapsed = useElapsedSeconds(running, `subagent:${node.id}`) + const durationSeconds = typeof node.durationSeconds === 'number' ? Math.max(0, Math.round(node.durationSeconds)) : elapsed + const [open, setOpen] = useState(() => running || depth < 2) const enterRef = useEnterAnimation(true, `subagent-row:${node.id}`) useEffect(() => { - if (running) setOpen(true) + if (running) { + setOpen(true) + } }, [running]) const visibleRows = open ? node.stream.slice(-10) : node.stream.slice(-2) @@ -304,11 +324,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n ].filter(Boolean) return ( -
0 && 'pl-4')} - data-slot="tool-block" - ref={enterRef} - > +
0 && 'pl-4')} data-slot="tool-block" ref={enterRef}> } + searchValue={query} > {!artifacts ? ( @@ -602,7 +588,7 @@ export function ArtifactsView({ total={visibleFileArtifacts.length} />
-
+
@@ -665,34 +651,6 @@ function ArtifactsPagination({ className, itemLabel, onPageChange, page, pageSiz ) } -function FilterButton({ - active, - icon: Icon, - label, - onClick -}: { - active: boolean - icon: typeof Layers3 - label: string - onClick: () => void -}) { - return ( - - ) -} - interface ArtifactImageCardProps { artifact: ArtifactRecord failedImage: boolean @@ -704,7 +662,7 @@ function ArtifactImageCard({ artifact, failedImage, onImageError, onOpenChat }: return (
{artifact.kind}
-
{artifact.label}
+
+ {artifact.label} +
{artifact.value}
@@ -769,7 +729,7 @@ function ArtifactCellAction({ if (href) { return (
{value} @@ -902,7 +865,7 @@ function ArtifactTable({ {artifacts.map(artifact => ( - + {ARTIFACT_COLUMNS.map(col => { const Cell = col.Cell diff --git a/apps/desktop/src/app/chat/composer/context-menu.tsx b/apps/desktop/src/app/chat/composer/context-menu.tsx index a356403c059..74de7b3b70a 100644 --- a/apps/desktop/src/app/chat/composer/context-menu.tsx +++ b/apps/desktop/src/app/chat/composer/context-menu.tsx @@ -11,7 +11,7 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { Clipboard, FileText, FolderOpen, ImageIcon, Link, type IconComponent, MessageSquareText } from '@/lib/icons' +import { Clipboard, FileText, FolderOpen, type IconComponent, ImageIcon, Link, MessageSquareText } from '@/lib/icons' import { cn } from '@/lib/utils' import { GHOST_ICON_BTN } from './controls' @@ -39,7 +39,10 @@ export function ContextMenu({ )} diff --git a/apps/desktop/src/app/chat/composer/queue-panel.tsx b/apps/desktop/src/app/chat/composer/queue-panel.tsx index 1d51d656562..18e95d0444e 100644 --- a/apps/desktop/src/app/chat/composer/queue-panel.tsx +++ b/apps/desktop/src/app/chat/composer/queue-panel.tsx @@ -21,7 +21,9 @@ const entryPreview = (entry: QueuedPromptEntry) => export function QueuePanel({ busy, editingId, entries, onDelete, onEdit, onSendNow }: QueuePanelProps) { const [collapsed, setCollapsed] = useState(false) - if (entries.length === 0) return null + if (entries.length === 0) { + return null + } return (
diff --git a/apps/desktop/src/app/chat/composer/trigger-popover.tsx b/apps/desktop/src/app/chat/composer/trigger-popover.tsx index bea7bdabcb8..7cc6a3b2237 100644 --- a/apps/desktop/src/app/chat/composer/trigger-popover.tsx +++ b/apps/desktop/src/app/chat/composer/trigger-popover.tsx @@ -89,10 +89,7 @@ export function ComposerTriggerPopover({ return ( -
- ) - })} + {active && ( +
+ ) + })} +
+
{ - const requestId = usageRequestRef.current + 1 - usageRequestRef.current = requestId - setUsageLoading(true) - setUsageError('') + const refreshUsage = useCallback(async (days: UsagePeriod) => { + const requestId = usageRequestRef.current + 1 + usageRequestRef.current = requestId + setUsageLoading(true) + setUsageError('') - try { - const response = await getUsageAnalytics(days) + try { + const response = await getUsageAnalytics(days) - if (usageRequestRef.current === requestId) { - setUsage(response) - } - } catch (error) { - if (usageRequestRef.current === requestId) { - setUsageError(error instanceof Error ? error.message : String(error)) - } - } finally { - if (usageRequestRef.current === requestId) { - setUsageLoading(false) - } + if (usageRequestRef.current === requestId) { + setUsage(response) } - }, - [] - ) + } catch (error) { + if (usageRequestRef.current === requestId) { + setUsageError(error instanceof Error ? error.message : String(error)) + } + } finally { + if (usageRequestRef.current === requestId) { + setUsageLoading(false) + } + } + }, []) useEffect(() => { if (!debouncedQuery) { @@ -583,7 +580,10 @@ export function CommandCenterView({ const beginAuxiliaryEdit = useCallback( (task: string) => { const current = auxiliary?.tasks.find(entry => entry.task === task) - const initialProvider = current?.provider && current.provider !== 'auto' ? current.provider : mainModel?.provider ?? '' + + const initialProvider = + current?.provider && current.provider !== 'auto' ? current.provider : (mainModel?.provider ?? '') + const initialModel = current?.model || mainModel?.model || '' setAuxDraft({ provider: initialProvider, model: initialModel }) setEditingAuxTask(task) @@ -658,15 +658,7 @@ export function CommandCenterView({ {SECTIONS.map(value => ( setSection(value)} @@ -1168,7 +1160,7 @@ function UsagePanel({ error, loading, onPeriodChange, onRefresh, period, usage } ) : (
No usage in the last {period} days.{' '} -
diff --git a/apps/desktop/src/app/cron/index.tsx b/apps/desktop/src/app/cron/index.tsx index f79ee39b602..b5c1deb083b 100644 --- a/apps/desktop/src/app/cron/index.tsx +++ b/apps/desktop/src/app/cron/index.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' +import { Codicon } from '@/components/ui/codicon' import { Dialog, DialogContent, @@ -24,7 +25,6 @@ import { triggerCronJob, updateCronJob } from '@/hermes' -import { Codicon } from '@/components/ui/codicon' import { AlertTriangle, Clock, Pause, Pencil, Play, Trash2, Zap } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' @@ -216,11 +216,23 @@ function scheduleOptionForExpr(expr: string): ScheduleOption { return SCHEDULE_OPTIONS.find(option => option.value === 'weekdays') ?? SCHEDULE_OPTIONS[0] } - if (dayOfMonth === '*' && month === '*' && isIntegerToken(dayOfWeek) && isIntegerToken(minute) && isIntegerToken(hour)) { + if ( + dayOfMonth === '*' && + month === '*' && + isIntegerToken(dayOfWeek) && + isIntegerToken(minute) && + isIntegerToken(hour) + ) { return SCHEDULE_OPTIONS.find(option => option.value === 'weekly') ?? SCHEDULE_OPTIONS[0] } - if (month === '*' && dayOfWeek === '*' && isIntegerToken(dayOfMonth) && isIntegerToken(minute) && isIntegerToken(hour)) { + if ( + month === '*' && + dayOfWeek === '*' && + isIntegerToken(dayOfMonth) && + isIntegerToken(minute) && + isIntegerToken(hour) + ) { return SCHEDULE_OPTIONS.find(option => option.value === 'monthly') ?? SCHEDULE_OPTIONS[0] } @@ -297,10 +309,7 @@ interface CronViewProps extends React.ComponentProps<'section'> { setStatusbarItemGroup?: SetStatusbarItemGroup } -export function CronView({ - setStatusbarItemGroup: _setStatusbarItemGroup, - ...props -}: CronViewProps) { +export function CronView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: CronViewProps) { const [jobs, setJobs] = useState(null) const [query, setQuery] = useState('') const [refreshing, setRefreshing] = useState(false) @@ -475,9 +484,7 @@ export function CronView({
)} -
- {totalCount === 0 ? 'No scheduled jobs' : `${enabledCount}/${totalCount} active`} -
+
{totalCount === 0 ? 'No scheduled jobs' : `${enabledCount}/${totalCount} active`}
setEditor({ mode: 'closed' })} onSave={handleEditorSave} /> @@ -488,7 +495,8 @@ export function CronView({ {pendingDelete ? ( <> - This will remove {truncate(jobTitle(pendingDelete), 60)}{' '} + This will remove{' '} + {truncate(jobTitle(pendingDelete), 60)}{' '} permanently. It will stop firing immediately. ) : null} @@ -586,13 +594,14 @@ function CronJobRow({ ) } -function IconAction({ - children, - className, - ...props -}: Omit, 'size' | 'variant'>) { +function IconAction({ children, className, ...props }: Omit, 'size' | 'variant'>) { return ( - ) @@ -600,7 +609,9 @@ function IconAction({ function StatePill({ children, tone }: { children: string; tone: keyof typeof PILL_TONE }) { return ( - + {children} ) diff --git a/apps/desktop/src/app/messaging/index.tsx b/apps/desktop/src/app/messaging/index.tsx index f4194b13903..9d0904a70ba 100644 --- a/apps/desktop/src/app/messaging/index.tsx +++ b/apps/desktop/src/app/messaging/index.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { PageLoader } from '@/components/page-loader' import { StatusDot, type StatusTone } from '@/components/status-dot' import { Button } from '@/components/ui/button' +import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { @@ -12,7 +13,6 @@ import { type MessagingPlatformInfo, updateMessagingPlatform } from '@/hermes' -import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { AlertTriangle, ExternalLink, Save, Trash2 } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' @@ -206,10 +206,7 @@ function fieldCopy(field: MessagingEnvVarInfo) { } } -export function MessagingView({ - setStatusbarItemGroup: _setStatusbarItemGroup, - ...props -}: MessagingViewProps) { +export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: MessagingViewProps) { const [platforms, setPlatforms] = useState(null) const [edits, setEdits] = useState({}) const [query, setQuery] = useState('') @@ -375,42 +372,42 @@ export function MessagingView({ ) : (
- + -
- {selected && ( - void handleClear(selected, key)} - onEdit={(key, value) => - setEdits(current => ({ - ...current, - [selected.id]: { - ...(current[selected.id] || {}), - [key]: value - } - })) - } - onSave={() => void handleSave(selected)} - onToggle={enabled => void handleToggle(selected, enabled)} - platform={selected} - saving={saving} - /> - )} -
-
+
+ {selected && ( + void handleClear(selected, key)} + onEdit={(key, value) => + setEdits(current => ({ + ...current, + [selected.id]: { + ...(current[selected.id] || {}), + [key]: value + } + })) + } + onSave={() => void handleSave(selected)} + onToggle={enabled => void handleToggle(selected, enabled)} + platform={selected} + saving={saving} + /> + )} +
+ )} ) @@ -429,7 +426,9 @@ function PlatformRow({
- ))} +
+ {branch && ( - + {branch} @@ -208,7 +210,7 @@ function FilesystemTab({ } export function RightSidebarSectionHeader({ children }: { children: ReactNode }) { - return
{children}
+ return
{children}
} interface FileTreeBodyProps { diff --git a/apps/desktop/src/app/right-sidebar/terminal/index.tsx b/apps/desktop/src/app/right-sidebar/terminal/index.tsx index 61d9d84e581..a9f2f4995a8 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/index.tsx +++ b/apps/desktop/src/app/right-sidebar/terminal/index.tsx @@ -27,7 +27,12 @@ export function TerminalTab({ cwd, onAddSelectionToChat }: TerminalTabProps) {
{status === 'starting' && (
- +
)} {selection.trim() && ( diff --git a/apps/desktop/src/app/settings/appearance-settings.tsx b/apps/desktop/src/app/settings/appearance-settings.tsx index 025ec7b7550..c35ec3417e5 100644 --- a/apps/desktop/src/app/settings/appearance-settings.tsx +++ b/apps/desktop/src/app/settings/appearance-settings.tsx @@ -67,7 +67,7 @@ export function AppearanceSettings() {

-
+
Color Mode
@@ -105,14 +105,16 @@ export function AppearanceSettings() { )}
{label}
-
{description}
+
+ {description} +
) })}
-
+
Tool Call Display
@@ -160,14 +162,16 @@ export function AppearanceSettings() { )}
-
{option.description}
+
+ {option.description} +
) })}
-
+
Theme
@@ -197,7 +201,9 @@ export function AppearanceSettings() {
-
{theme.label}
+
+ {theme.label} +
{theme.description}
diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index b9c11b67572..a150bcd6a32 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -1,7 +1,7 @@ import { Brain, - Lock, type IconComponent, + Lock, MessageCircle, Mic, Monitor, diff --git a/apps/desktop/src/app/settings/gateway-settings.tsx b/apps/desktop/src/app/settings/gateway-settings.tsx index ef6b22373c6..c09f2db47f3 100644 --- a/apps/desktop/src/app/settings/gateway-settings.tsx +++ b/apps/desktop/src/app/settings/gateway-settings.tsx @@ -60,7 +60,9 @@ function ModeCard({ {title} {active ? : null}
-

{description}

+

+ {description} +

) } diff --git a/apps/desktop/src/app/settings/keys-settings.tsx b/apps/desktop/src/app/settings/keys-settings.tsx index 6e233a6475d..242485e5c8b 100644 --- a/apps/desktop/src/app/settings/keys-settings.tsx +++ b/apps/desktop/src/app/settings/keys-settings.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { Button } from '@/components/ui/button' +import { Codicon } from '@/components/ui/codicon' import { Input } from '@/components/ui/input' import { deleteEnvVar, getEnvVars, revealEnvVar, setEnvVar } from '@/hermes' -import { Codicon } from '@/components/ui/codicon' import { Check, Eye, EyeOff, Save, Settings2, Trash2, Zap } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' diff --git a/apps/desktop/src/app/settings/mcp-settings.tsx b/apps/desktop/src/app/settings/mcp-settings.tsx index 428b76bbd64..794ea3c4695 100644 --- a/apps/desktop/src/app/settings/mcp-settings.tsx +++ b/apps/desktop/src/app/settings/mcp-settings.tsx @@ -1,13 +1,13 @@ +import { useStore } from '@nanostores/react' import { useEffect, useMemo, useState } from 'react' import { OverlayActionButton, OverlayCard } from '@/app/overlays/overlay-chrome' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' -import { getHermesConfigRecord, saveHermesConfig, type HermesGateway } from '@/hermes' +import { getHermesConfigRecord, type HermesGateway, saveHermesConfig } from '@/hermes' import { Package, Wrench } from '@/lib/icons' import { notify, notifyError } from '@/store/notifications' import { $activeSessionId } from '@/store/session' -import { useStore } from '@nanostores/react' import type { HermesConfigRecord } from '@/types/hermes' import { includesQuery } from './helpers' @@ -64,7 +64,10 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps) getHermesConfigRecord() .then(next => { - if (cancelled) return + if (cancelled) { + return + } + setConfig(next) const first = Object.keys(getServers(next)).sort()[0] ?? null setSelected(first) @@ -76,6 +79,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps) const servers = useMemo(() => getServers(config), [config]) const names = useMemo(() => Object.keys(servers).sort(), [servers]) + const filtered = useMemo( () => names.filter(serverName => serverMatches(serverName, servers[serverName], query.trim().toLowerCase())), [names, query, servers] @@ -97,6 +101,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps) if (!nextName) { notify({ kind: 'error', title: 'Name required', message: 'Give this MCP server a config key.' }) + return } @@ -112,6 +117,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps) parsed = raw as Record } catch (err) { notifyError(err, 'Invalid MCP JSON') + return } @@ -161,6 +167,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps) const reloadMcp = async () => { if (!gateway) { notify({ kind: 'warning', title: 'Gateway unavailable', message: 'Reconnect the gateway before reloading MCP.' }) + return } diff --git a/apps/desktop/src/app/settings/primitives.tsx b/apps/desktop/src/app/settings/primitives.tsx index 2a54daa595e..d883a5b31dc 100644 --- a/apps/desktop/src/app/settings/primitives.tsx +++ b/apps/desktop/src/app/settings/primitives.tsx @@ -53,7 +53,9 @@ export function NavLink({ - ) -} - -function CategoryButton({ - active, - count, - label, - onClick -}: { - active: boolean - count: number - label: string - onClick: () => void -}) { - return ( - - ) -} - function StatusPill({ active, children }: { active: boolean; children: string }) { return ( {children} diff --git a/apps/desktop/src/app/types.ts b/apps/desktop/src/app/types.ts index 6b81d93c0af..246076bd36b 100644 --- a/apps/desktop/src/app/types.ts +++ b/apps/desktop/src/app/types.ts @@ -52,13 +52,7 @@ export type CommandDispatchResponse = | SkillCommandDispatchResponse | SendCommandDispatchResponse -export type SidebarNavId = - | 'artifacts' - | 'command-center' - | 'messaging' - | 'new-session' - | 'settings' - | 'skills' +export type SidebarNavId = 'artifacts' | 'command-center' | 'messaging' | 'new-session' | 'settings' | 'skills' export interface SidebarNavItem { id: SidebarNavId diff --git a/apps/desktop/src/components/Backdrop.tsx b/apps/desktop/src/components/Backdrop.tsx index 9621cb12fcc..1f69167c6cd 100644 --- a/apps/desktop/src/components/Backdrop.tsx +++ b/apps/desktop/src/components/Backdrop.tsx @@ -1,8 +1,5 @@ -import { useGpuTier } from '@nous-research/ui/hooks/use-gpu-tier' import { Leva, useControls } from 'leva' -import { type CSSProperties, useEffect, useMemo, useState } from 'react' - -import { ThemeControls } from './ThemeControls' +import { type CSSProperties, useEffect, useState } from 'react' const BLEND_MODES = [ 'normal', @@ -25,43 +22,7 @@ const BLEND_MODES = [ type BlendMode = (typeof BLEND_MODES)[number] -function binaryNoiseDataUrl(tile: number, density: number, size: number, color: string): string { - if (typeof document === 'undefined') { - return '' - } - - // Cap at 1.5x to match the design-language overlay perf work (PR #14): - // with `image-rendering: pixelated` there's no visible win above 1.5x, and - // a full retina (2x) PNG is ~78% larger to keep resident in compositor memory. - const dpr = Math.min(window.devicePixelRatio || 1, 1.5) - const physTile = Math.round(tile * dpr) - const block = Math.max(1, Math.round(size * dpr)) - - const canvas = document.createElement('canvas') - canvas.width = physTile - canvas.height = physTile - - const ctx = canvas.getContext('2d') - - if (!ctx) { - return '' - } - - ctx.fillStyle = color - - for (let y = 0; y < physTile; y += block) { - for (let x = 0; x < physTile; x += block) { - if (Math.random() < density) { - ctx.fillRect(x, y, block, block) - } - } - } - - return `url("${canvas.toDataURL('image/png')}")` -} - export function Backdrop() { - const gpuTier = useGpuTier() const [controlsOpen, setControlsOpen] = useState(false) useEffect(() => { @@ -94,9 +55,7 @@ export function Backdrop() { const shape = useControls( 'UI / Shape', - { - radiusScalar: { value: 0.2, min: 0, max: 2, step: 0.1, label: 'radius scalar' } - }, + { radiusScalar: { value: 0.2, min: 0, max: 2, step: 0.1, label: 'radius scalar' } }, { collapsed: true } ) @@ -108,7 +67,7 @@ export function Backdrop() { 'Backdrop / Statue', { enabled: { value: true, label: 'on' }, - opacity: { value: 0.04, min: 0, max: 1, step: 0.005 }, + opacity: { value: 0.025, min: 0, max: 1, step: 0.005 }, blendMode: { value: 'difference' as BlendMode, options: BLEND_MODES, label: 'blend' }, invert: { value: true, label: 'invert color' }, saturate: { value: 1, min: 0, max: 3, step: 0.05, label: 'saturate' }, @@ -123,55 +82,14 @@ export function Backdrop() { { collapsed: true } ) - const vignette = useControls( - 'Backdrop / Vignette', - { - enabled: { value: true, label: 'on' }, - opacity: { value: 0.22, min: 0, max: 1, step: 0.01 }, - blendMode: { value: 'lighten' as BlendMode, options: BLEND_MODES, label: 'blend' }, - useTheme: { value: true, label: 'use --warm-glow' }, - color: { value: '#ffbd38', label: 'color (override)' }, - origin: { - value: '0% 0%', - options: ['0% 0%', '100% 0%', '50% 0%', '0% 100%', '100% 100%', '50% 50%'], - label: 'corner' - }, - transparentStop: { value: 60, min: 0, max: 100, step: 1, label: 'fade start %' } - }, - { collapsed: true } - ) - - const noise = useControls( - 'Backdrop / Noise', - { - enabled: { value: false, label: 'on' }, - opacity: { value: 0.21, min: 0, max: 1.5, step: 0.01, label: 'opacity (× mul)' }, - blendMode: { value: 'color-dodge' as BlendMode, options: BLEND_MODES, label: 'blend' }, - color: { value: '#eaeaea', label: 'dot color' }, - density: { value: 0.11, min: 0, max: 1, step: 0.005, label: 'density' }, - size: { value: 1, min: 1, max: 10, step: 1, label: 'block px' }, - tile: { value: 256, min: 64, max: 1024, step: 32, label: 'tile px' }, - reroll: { value: 0, min: 0, max: 100, step: 1, label: 'reroll' } - }, - { collapsed: true } - ) - - const noiseUrl = useMemo( - () => binaryNoiseDataUrl(noise.tile, noise.density, noise.size, noise.color), - // eslint-disable-next-line react-hooks/exhaustive-deps - [noise.tile, noise.density, noise.size, noise.color, noise.reroll] - ) - return ( <>
- )} - - {noise.enabled && gpuTier > 0 && noiseUrl && ( -
- )} ) } diff --git a/apps/desktop/src/components/ThemeControls.tsx b/apps/desktop/src/components/ThemeControls.tsx deleted file mode 100644 index f960c377e57..00000000000 --- a/apps/desktop/src/components/ThemeControls.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Leva-driven palette fine-tuning, dev-mode only. - * - * Two folders (`Theme / Light` and `Theme / Dark`) expose the seed colors - * and mix percentages that drive the glass token derivation. Edits are live - * only; use them to tune values before copying them back into presets/CSS. - */ - -import { button, useControls } from 'leva' -import { useEffect, useMemo } from 'react' - -import { getBaseColors, useTheme } from '@/themes/context' - -interface ThemeTuningValues { - accentFill: number - accentSoft: string - backgroundSeed: string - bubbleMix: number - bubbleSeed: string - cardMix: number - cardSeed: string - chromeMix: number - elevatedMix: number - elevatedSeed: string - foreground: string - midground: string - primary: string - primaryFill: number - primaryStroke: number - quaternaryFill: number - quaternaryStroke: number - quinaryFill: number - secondary: string - secondaryFill: number - secondaryStroke: number - sidebarMix: number - sidebarSeed: string - tertiaryFill: number - tertiaryStroke: number - warm: string -} - -const HEX_RE = /^#[0-9a-f]{6}$/i - -const swatch = (value: string | undefined) => - typeof value === 'string' && HEX_RE.test(value.trim()) ? value : '#444444' - -const pct = (value: number) => `${value}%` - -const defaultsFor = (mode: 'light' | 'dark') => ({ - bubbleMix: mode === 'dark' ? 48 : 30, - cardMix: mode === 'dark' ? 38 : 22, - chromeMix: mode === 'dark' ? 36 : 44, - elevatedMix: mode === 'dark' ? 46 : 28, - primaryFill: 16, - primaryStroke: 24, - quaternaryFill: 5, - quaternaryStroke: 6, - quinaryFill: 3, - secondaryFill: 11, - secondaryStroke: 16, - sidebarMix: mode === 'dark' ? 42 : 36, - tertiaryFill: 8, - tertiaryStroke: 10 -}) - -const setCss = (name: string, value: string) => document.documentElement.style.setProperty(name, value) - -function applyTuning(values: ThemeTuningValues) { - setCss('--theme-foreground', values.foreground) - setCss('--theme-primary', values.primary) - setCss('--theme-secondary', values.secondary) - setCss('--theme-accent-soft', values.accentSoft) - setCss('--theme-midground', values.midground) - setCss('--theme-warm', values.warm) - setCss('--theme-background-seed', values.backgroundSeed) - setCss('--theme-sidebar-seed', values.sidebarSeed) - setCss('--theme-card-seed', values.cardSeed) - setCss('--theme-elevated-seed', values.elevatedSeed) - setCss('--theme-bubble-seed', values.bubbleSeed) - setCss('--theme-mix-chrome', pct(values.chromeMix)) - setCss('--theme-mix-sidebar', pct(values.sidebarMix)) - setCss('--theme-mix-card', pct(values.cardMix)) - setCss('--theme-mix-elevated', pct(values.elevatedMix)) - setCss('--theme-mix-bubble', pct(values.bubbleMix)) - setCss('--theme-fill-primary-accent-mix', pct(values.primaryFill)) - setCss('--theme-fill-secondary-accent-mix', pct(values.secondaryFill)) - setCss('--theme-fill-tertiary-accent-mix', pct(values.tertiaryFill)) - setCss('--theme-fill-quaternary-accent-mix', pct(values.quaternaryFill)) - setCss('--theme-fill-quinary-accent-mix', pct(values.quinaryFill)) - setCss('--theme-stroke-primary-accent-mix', pct(values.primaryStroke)) - setCss('--theme-stroke-secondary-accent-mix', pct(values.secondaryStroke)) - setCss('--theme-stroke-tertiary-accent-mix', pct(values.tertiaryStroke)) - setCss('--theme-stroke-quaternary-accent-mix', pct(values.quaternaryStroke)) -} - -function buildSchema(skinName: string, mode: 'light' | 'dark') { - const base = getBaseColors(skinName, mode) - const mix = defaultsFor(mode) - - const schema = { - foreground: { value: swatch(base.foreground), label: 'text base' }, - primary: { value: swatch(base.primary), label: 'primary' }, - secondary: { value: swatch(base.secondary), label: 'secondary' }, - accentSoft: { value: swatch(base.accent), label: 'accent soft' }, - midground: { value: swatch(base.midground ?? base.ring), label: 'midground' }, - warm: { value: swatch(base.primary), label: 'warm glow' }, - backgroundSeed: { value: swatch(base.background), label: 'chrome seed' }, - sidebarSeed: { value: swatch(base.sidebarBackground ?? base.background), label: 'sidebar seed' }, - cardSeed: { value: swatch(base.card), label: 'card seed' }, - elevatedSeed: { value: swatch(base.popover), label: 'elevated seed' }, - bubbleSeed: { value: swatch(base.userBubble ?? base.popover), label: 'bubble seed' }, - chromeMix: { value: mix.chromeMix, min: 0, max: 100, step: 1, label: 'chrome mix %' }, - sidebarMix: { value: mix.sidebarMix, min: 0, max: 100, step: 1, label: 'sidebar mix %' }, - cardMix: { value: mix.cardMix, min: 0, max: 100, step: 1, label: 'card mix %' }, - elevatedMix: { value: mix.elevatedMix, min: 0, max: 100, step: 1, label: 'elevated mix %' }, - bubbleMix: { value: mix.bubbleMix, min: 0, max: 100, step: 1, label: 'bubble mix %' }, - primaryFill: { value: mix.primaryFill, min: 0, max: 40, step: 1, label: 'fill primary %' }, - secondaryFill: { value: mix.secondaryFill, min: 0, max: 40, step: 1, label: 'fill secondary %' }, - tertiaryFill: { value: mix.tertiaryFill, min: 0, max: 40, step: 1, label: 'fill tertiary %' }, - quaternaryFill: { value: mix.quaternaryFill, min: 0, max: 40, step: 1, label: 'fill quaternary %' }, - quinaryFill: { value: mix.quinaryFill, min: 0, max: 40, step: 1, label: 'fill quinary %' }, - primaryStroke: { value: mix.primaryStroke, min: 0, max: 50, step: 1, label: 'stroke primary %' }, - secondaryStroke: { value: mix.secondaryStroke, min: 0, max: 50, step: 1, label: 'stroke secondary %' }, - tertiaryStroke: { value: mix.tertiaryStroke, min: 0, max: 50, step: 1, label: 'stroke tertiary %' }, - quaternaryStroke: { value: mix.quaternaryStroke, min: 0, max: 50, step: 1, label: 'stroke quaternary %' } - } - - return { - ...schema, - 'apply defaults': button(() => applyTuning(valuesFromSchema(schema))) - } as Parameters[1] -} - -function valuesFromSchema(schema: Record): ThemeTuningValues { - return Object.fromEntries(Object.entries(schema).map(([key, field]) => [key, field.value])) as unknown as ThemeTuningValues -} - -/** Renders nothing — Leva's UI is a portal driven by `useControls`. */ -export function ThemeControls() { - const { resolvedMode, themeName } = useTheme() - const light = useMemo(() => buildSchema(themeName, 'light'), [themeName]) - const dark = useMemo(() => buildSchema(themeName, 'dark'), [themeName]) - const lightValues = useControls('Theme / Light', light, { collapsed: resolvedMode !== 'light' }, [themeName]) - const darkValues = useControls('Theme / Dark', dark, { collapsed: resolvedMode !== 'dark' }, [themeName]) - - useEffect(() => { - applyTuning((resolvedMode === 'light' ? lightValues : darkValues) as ThemeTuningValues) - }, [darkValues, lightValues, resolvedMode]) - - return null -} diff --git a/apps/desktop/src/components/assistant-ui/clarify-tool.tsx b/apps/desktop/src/components/assistant-ui/clarify-tool.tsx index 2ea23507c43..266e554ad11 100644 --- a/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +++ b/apps/desktop/src/components/assistant-ui/clarify-tool.tsx @@ -273,7 +273,7 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
1–{choices.length} to pick - {trailing && {trailing}} + {trailing && ( + {trailing} + )}
) } diff --git a/apps/desktop/src/components/desktop-onboarding-overlay.tsx b/apps/desktop/src/components/desktop-onboarding-overlay.tsx index 265d0107f04..d9e39861c49 100644 --- a/apps/desktop/src/components/desktop-onboarding-overlay.tsx +++ b/apps/desktop/src/components/desktop-onboarding-overlay.tsx @@ -144,8 +144,8 @@ export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway const showPicker = flow.status === 'idle' || flow.status === 'success' return ( -
-
+
+
{reason ? : null} @@ -209,7 +209,7 @@ function Preparing({ boot }: { boot: DesktopBootState }) { function Header() { return ( -
+
@@ -252,7 +252,7 @@ function FooterLink({ children, onClick }: { children: React.ReactNode; onClick: return (
)} {children} diff --git a/apps/desktop/src/components/ui/button.tsx b/apps/desktop/src/components/ui/button.tsx index 4b0aff4a9aa..467f4c0a5df 100644 --- a/apps/desktop/src/components/ui/button.tsx +++ b/apps/desktop/src/components/ui/button.tsx @@ -16,7 +16,7 @@ const buttonVariants = cva( 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', - link: 'text-primary underline-offset-4 hover:underline' + link: 'text-primary underline-offset-4 decoration-current/20 hover:underline' }, size: { default: 'h-9 px-4 py-2 has-[>svg]:px-3', diff --git a/apps/desktop/src/components/ui/dialog.tsx b/apps/desktop/src/components/ui/dialog.tsx index 47edc314a2e..4f732954a2e 100644 --- a/apps/desktop/src/components/ui/dialog.tsx +++ b/apps/desktop/src/components/ui/dialog.tsx @@ -46,7 +46,7 @@ function DialogContent({ ) { return ( diff --git a/apps/desktop/src/components/ui/dropdown-menu.tsx b/apps/desktop/src/components/ui/dropdown-menu.tsx index abe99a6cae4..ee180726b96 100644 --- a/apps/desktop/src/components/ui/dropdown-menu.tsx +++ b/apps/desktop/src/components/ui/dropdown-menu.tsx @@ -52,7 +52,7 @@ function DropdownMenuItem({ return ( ) { return ( diff --git a/apps/desktop/src/components/ui/text-tab.tsx b/apps/desktop/src/components/ui/text-tab.tsx new file mode 100644 index 00000000000..4e85966883b --- /dev/null +++ b/apps/desktop/src/components/ui/text-tab.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +function TextTabMeta({ className, ...props }: React.ComponentProps<'span'>) { + return +} + +interface TextTabProps extends React.ComponentProps<'button'> { + active?: boolean +} + +function TextTab({ active = false, children, className, type = 'button', ...props }: TextTabProps) { + return ( + + ) +} + +export { TextTab, TextTabMeta } diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index 8992359c58f..a8d32e48bef 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -394,9 +394,7 @@ export function getProfiles(): Promise { }) } -export function createProfile( - body: ProfileCreatePayload -): Promise<{ name: string; ok: boolean; path: string }> { +export function createProfile(body: ProfileCreatePayload): Promise<{ name: string; ok: boolean; path: string }> { return window.hermesDesktop.api<{ name: string; ok: boolean; path: string }>({ path: '/api/profiles', method: 'POST', @@ -404,10 +402,7 @@ export function createProfile( }) } -export function renameProfile( - name: string, - newName: string -): Promise<{ name: string; ok: boolean; path: string }> { +export function renameProfile(name: string, newName: string): Promise<{ name: string; ok: boolean; path: string }> { return window.hermesDesktop.api<{ name: string; ok: boolean; path: string }>({ path: `/api/profiles/${encodeURIComponent(name)}`, method: 'PATCH', diff --git a/apps/desktop/src/lib/chat-runtime.ts b/apps/desktop/src/lib/chat-runtime.ts index 1999b647558..915869a4fd1 100644 --- a/apps/desktop/src/lib/chat-runtime.ts +++ b/apps/desktop/src/lib/chat-runtime.ts @@ -318,7 +318,11 @@ export function toRuntimeMessage(message: ChatMessage): ThreadMessage { role, content: message.parts as Extract['content'], createdAt, - status: message.pending ? { type: 'running' } : { type: 'complete', reason: 'stop' }, + status: message.error + ? { type: 'incomplete', reason: 'error', error: message.error } + : message.pending + ? { type: 'running' } + : { type: 'complete', reason: 'stop' }, metadata: { unstable_state: null, unstable_annotations: [], diff --git a/apps/desktop/src/lib/external-link.test.tsx b/apps/desktop/src/lib/external-link.test.tsx index 3023d79d0d2..4e529576548 100644 --- a/apps/desktop/src/lib/external-link.test.tsx +++ b/apps/desktop/src/lib/external-link.test.tsx @@ -143,6 +143,7 @@ describe('external link helpers', () => { it('ignores error-like fetched titles and falls back to slug label', async () => { const bridge = vi.fn().mockResolvedValue('GetYourGuide – Error') installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['hermesDesktop']['fetchLinkTitle'] }) + const url = 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/' diff --git a/apps/desktop/src/lib/external-link.tsx b/apps/desktop/src/lib/external-link.tsx index cd3912cf995..05f1ec02f66 100644 --- a/apps/desktop/src/lib/external-link.tsx +++ b/apps/desktop/src/lib/external-link.tsx @@ -216,7 +216,7 @@ export function ExternalLink({ return ( { event.stopPropagation() diff --git a/apps/desktop/src/lib/katex-memo.ts b/apps/desktop/src/lib/katex-memo.ts index 2f7b07ffb2b..7143fbff905 100644 --- a/apps/desktop/src/lib/katex-memo.ts +++ b/apps/desktop/src/lib/katex-memo.ts @@ -180,9 +180,7 @@ function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable { return () => function transform(tree: Root, file: VFile): undefined { visitParents(tree, 'element', (element, parents) => { - const classes = Array.isArray(element.properties?.className) - ? (element.properties.className as string[]) - : [] + const classes = Array.isArray(element.properties?.className) ? (element.properties.className as string[]) : [] // Match the same class set rehype-katex looks for. `language-math` // is the markdown ` ```math ` form, `math-inline` is what @@ -201,12 +199,7 @@ function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable { // For ` ```math ` the scope walks up to the wrapping
 and
         // we treat it as display math. Same logic rehype-katex uses.
-        if (
-          languageMath &&
-          parent &&
-          parent.type === 'element' &&
-          (parent as Element).tagName === 'pre'
-        ) {
+        if (languageMath && parent && parent.type === 'element' && (parent as Element).tagName === 'pre') {
           scope = parent as Element
           parent = parents[parents.length - 2]
           displayMode = true
@@ -253,10 +246,7 @@ function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable {
  * wrapper. Drop-in for `@streamdown/math`'s `createMathPlugin`.
  */
 export function createMemoizedMathPlugin(config: MathPluginConfig = {}) {
-  const remarkPlugin: Pluggable = [
-    remarkMath,
-    { singleDollarTextMath: config.singleDollarTextMath ?? false }
-  ]
+  const remarkPlugin: Pluggable = [remarkMath, { singleDollarTextMath: config.singleDollarTextMath ?? false }]
 
   const rehypePlugin = createMemoizedRehypeKatex({ errorColor: config.errorColor })
 
diff --git a/apps/desktop/src/lib/markdown-preprocess.ts b/apps/desktop/src/lib/markdown-preprocess.ts
index bb85b22b0a2..c4d4637befa 100644
--- a/apps/desktop/src/lib/markdown-preprocess.ts
+++ b/apps/desktop/src/lib/markdown-preprocess.ts
@@ -310,7 +310,9 @@ const LATEX_INLINE_RE = /\\\(([^\n]+?)\\\)/g
 const LATEX_DISPLAY_RE = /\\\[([\s\S]+?)\\\]/g
 
 function rewriteLatexBracketDelimiters(text: string): string {
-  return text.replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`).replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`)
+  return text
+    .replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`)
+    .replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`)
 }
 
 // Escape `$` patterns so they don't get eaten as math delimiters.
@@ -340,14 +342,19 @@ export function preprocessMarkdown(text: string): string {
     .split(CODE_FENCE_SPLIT_RE)
     .map(part => {
       // Fence blocks pass through untouched.
-      if (/^(?:```|~~~)/.test(part)) {return part}
+      if (/^(?:```|~~~)/.test(part)) {
+        return part
+      }
 
       // Whitespace-only segments (e.g. the `\n\n` between two adjacent
       // fences) must NOT go through stripPreviewTargets — its internal
       // .trim() would collapse them to '' and glue the surrounding
       // fences together, producing things like ``````math which the
       // markdown parser then reads as a single 6-backtick block.
-      if (!part.trim()) {return part}
+      if (!part.trim()) {
+        return part
+      }
+
       // Preserve leading/trailing whitespace around the prose body so
       // that fence-prose-fence sequences keep their blank-line gaps.
       // stripPreviewTargets internally calls .trim() on its result for
diff --git a/apps/desktop/src/lib/provider-setup-errors.test.ts b/apps/desktop/src/lib/provider-setup-errors.test.ts
index 62f99e3a2df..e5a9dc65089 100644
--- a/apps/desktop/src/lib/provider-setup-errors.test.ts
+++ b/apps/desktop/src/lib/provider-setup-errors.test.ts
@@ -4,16 +4,16 @@ import { isProviderSetupErrorMessage } from './provider-setup-errors'
 
 describe('isProviderSetupErrorMessage', () => {
   it('matches generic missing-provider copy', () => {
-    expect(isProviderSetupErrorMessage('No inference provider configured. Run `hermes model` to choose one.')).toBe(true)
+    expect(isProviderSetupErrorMessage('No inference provider configured. Run `hermes model` to choose one.')).toBe(
+      true
+    )
     expect(isProviderSetupErrorMessage('No inference provider is configured.')).toBe(true)
     expect(isProviderSetupErrorMessage('set an API key (OPENROUTER_API_KEY) in ~/.hermes/.env')).toBe(true)
   })
 
   it('does not match non-provider runtime failures', () => {
     expect(
-      isProviderSetupErrorMessage(
-        'Selected runtime is not available. setup.status reports configured credentials.'
-      )
+      isProviderSetupErrorMessage('Selected runtime is not available. setup.status reports configured credentials.')
     ).toBe(false)
   })
 
diff --git a/apps/desktop/src/lib/todos.ts b/apps/desktop/src/lib/todos.ts
index 01071a3f0e9..56f36b45c27 100644
--- a/apps/desktop/src/lib/todos.ts
+++ b/apps/desktop/src/lib/todos.ts
@@ -16,6 +16,7 @@ function parseArray(value: unknown[]): TodoItem[] {
     if (!isRecord(item) || !isStatus(item.status)) {
       return []
     }
+
     const id = String(item.id ?? '').trim()
     const content = String(item.content ?? '').trim()
 
diff --git a/apps/desktop/src/lib/tool-result-summary.ts b/apps/desktop/src/lib/tool-result-summary.ts
index 615473568c3..22af5644452 100644
--- a/apps/desktop/src/lib/tool-result-summary.ts
+++ b/apps/desktop/src/lib/tool-result-summary.ts
@@ -2,6 +2,7 @@
 // mode still gets the raw JSON section.
 
 const WRAPPER_KEYS = ['data', 'result', 'output', 'response', 'payload'] as const
+
 const PRIORITY_KEYS = [
   'title',
   'name',
@@ -17,6 +18,7 @@ const PRIORITY_KEYS = [
   'summary',
   'description'
 ] as const
+
 const ERROR_KEYS = ['error', 'errors', 'failure', 'exception'] as const
 const ERROR_MSG_KEYS = ['message', 'reason', 'detail', 'stderr'] as const
 const NON_ERROR_TEXT = new Set(['', '0', 'false', 'none', 'null', 'nil', 'ok', 'success', 'n/a', 'na'])
@@ -66,6 +68,7 @@ function clipBlock(value: string, maxChars = 1800, maxLines = 18): string {
   if (!t) {
     return ''
   }
+
   const lines = t.split('\n')
   let text = lines.slice(0, maxLines).join('\n')
   const clipped = lines.length > maxLines || text.length > maxChars
@@ -187,11 +190,13 @@ function formatFieldValue(value: unknown, depth: number): string {
     if (!v.length) {
       return ''
     }
+
     const scalars = v.map(summarizeScalar).filter(Boolean)
 
     if (scalars.length === v.length && v.length <= 4) {
       return clipInline(scalars.join(', '))
     }
+
     const first = summarizeListItem(v[0], depth + 1)
 
     return first ? `${pluralize(v.length, 'item')} (${first})` : pluralize(v.length, 'item')
@@ -207,16 +212,21 @@ function formatFieldValue(value: unknown, depth: number): string {
 // "Returned N items" / "0 items" / "Returned an empty object" are all
 // noise — better to render nothing and let the title carry the signal.
 function formatArraySummary(value: unknown[], depth: number): string {
-  if (!value.length) return ''
+  if (!value.length) {
+    return ''
+  }
 
   const max = 6
+
   const lines = value
     .slice(0, max)
     .map(item => summarizeListItem(item, depth + 1))
     .filter(Boolean)
     .map(l => `- ${l}`)
 
-  if (!lines.length) return ''
+  if (!lines.length) {
+    return ''
+  }
 
   if (value.length > max) {
     const remaining = value.length - max
@@ -228,7 +238,10 @@ function formatArraySummary(value: unknown[], depth: number): string {
 
 function formatRecordSummary(record: Json, depth: number): string {
   const keys = Object.keys(record)
-  if (!keys.length) return ''
+
+  if (!keys.length) {
+    return ''
+  }
 
   if (depth <= 2) {
     const direct = firstString(record, ['message', 'summary', 'description', 'preview', 'text', 'content'])
@@ -249,6 +262,7 @@ function formatRecordSummary(record: Json, depth: number): string {
     if (!v) {
       continue
     }
+
     lines.push(`- ${titleCase(k)}: ${v}`)
 
     if (lines.length >= max) {
@@ -256,7 +270,9 @@ function formatRecordSummary(record: Json, depth: number): string {
     }
   }
 
-  if (!lines.length) return ''
+  if (!lines.length) {
+    return ''
+  }
 
   if (candidates.length > lines.length) {
     const remaining = candidates.length - lines.length
@@ -270,6 +286,7 @@ function formatSummaryValue(value: unknown, depth: number): string {
   if (depth > 4) {
     return ''
   }
+
   const v = norm(value)
 
   if (typeof v === 'string') {
@@ -383,11 +400,13 @@ function findNestedError(value: unknown, depth: number, seen: Set): str
   if (depth > 5) {
     return ''
   }
+
   const v = norm(value)
 
   if (!v || typeof v !== 'object' || seen.has(v)) {
     return ''
   }
+
   seen.add(v)
 
   if (Array.isArray(v)) {
@@ -408,6 +427,7 @@ function findNestedError(value: unknown, depth: number, seen: Set): str
     if (!hasMeaningfulErrorValue(record[k])) {
       continue
     }
+
     const text = valueErrorText(record[k])
 
     if (text) {
diff --git a/apps/desktop/src/store/composer-queue.ts b/apps/desktop/src/store/composer-queue.ts
index d2a3f228ff1..3f231fb7b2a 100644
--- a/apps/desktop/src/store/composer-queue.ts
+++ b/apps/desktop/src/store/composer-queue.ts
@@ -14,7 +14,10 @@ type QueueState = Record
 const STORAGE_KEY = 'hermes.desktop.composerQueue.v1'
 
 const load = (): QueueState => {
-  if (typeof window === 'undefined') return {}
+  if (typeof window === 'undefined') {
+    return {}
+  }
+
   try {
     const raw = window.localStorage.getItem(STORAGE_KEY)
     const parsed = raw ? JSON.parse(raw) : null
@@ -26,10 +29,16 @@ const load = (): QueueState => {
 }
 
 const save = (state: QueueState) => {
-  if (typeof window === 'undefined') return
+  if (typeof window === 'undefined') {
+    return
+  }
+
   try {
-    if (Object.keys(state).length === 0) window.localStorage.removeItem(STORAGE_KEY)
-    else window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
+    if (Object.keys(state).length === 0) {
+      window.localStorage.removeItem(STORAGE_KEY)
+    } else {
+      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
+    }
   } catch {
     // best-effort: storage may be unavailable, queue still works in-memory
   }
@@ -41,8 +50,11 @@ const writeSession = (sid: string, queue: QueuedPromptEntry[]) => {
   const current = $queuedPromptsBySession.get()
   const next = { ...current }
 
-  if (queue.length === 0) delete next[sid]
-  else next[sid] = queue
+  if (queue.length === 0) {
+    delete next[sid]
+  } else {
+    next[sid] = queue
+  }
 
   $queuedPromptsBySession.set(next)
   save(next)
@@ -72,7 +84,9 @@ export const enqueueQueuedPrompt = (
 ): null | QueuedPromptEntry => {
   const sid = sidOf(key)
 
-  if (!sid) return null
+  if (!sid) {
+    return null
+  }
 
   const entry: QueuedPromptEntry = {
     id: nextId(),
@@ -89,11 +103,15 @@ export const enqueueQueuedPrompt = (
 export const dequeueQueuedPrompt = (key: string | null | undefined): null | QueuedPromptEntry => {
   const sid = sidOf(key)
 
-  if (!sid) return null
+  if (!sid) {
+    return null
+  }
 
   const [head, ...rest] = queueFor(sid)
 
-  if (!head) return null
+  if (!head) {
+    return null
+  }
 
   writeSession(sid, rest)
 
@@ -103,12 +121,16 @@ export const dequeueQueuedPrompt = (key: string | null | undefined): null | Queu
 export const removeQueuedPrompt = (key: string | null | undefined, id: string): boolean => {
   const sid = sidOf(key)
 
-  if (!sid) return false
+  if (!sid) {
+    return false
+  }
 
   const queue = queueFor(sid)
   const next = queue.filter(e => e.id !== id)
 
-  if (next.length === queue.length) return false
+  if (next.length === queue.length) {
+    return false
+  }
 
   writeSession(sid, next)
 
@@ -122,24 +144,32 @@ export const updateQueuedPrompt = (
 ): boolean => {
   const sid = sidOf(key)
 
-  if (!sid) return false
+  if (!sid) {
+    return false
+  }
 
   const queue = queueFor(sid)
   let changed = false
 
   const next = queue.map(entry => {
-    if (entry.id !== id) return entry
+    if (entry.id !== id) {
+      return entry
+    }
 
     const attachments = update.attachments ? cloneAttachments(update.attachments) : entry.attachments
 
-    if (entry.text === update.text && !update.attachments) return entry
+    if (entry.text === update.text && !update.attachments) {
+      return entry
+    }
 
     changed = true
 
     return { ...entry, text: update.text, attachments }
   })
 
-  if (!changed) return false
+  if (!changed) {
+    return false
+  }
 
   writeSession(sid, next)
 
@@ -152,7 +182,9 @@ export const updateQueuedPromptText = (key: string | null | undefined, id: strin
 export const clearQueuedPrompts = (key: string | null | undefined) => {
   const sid = sidOf(key)
 
-  if (!sid || !(sid in $queuedPromptsBySession.get())) return
+  if (!sid || !(sid in $queuedPromptsBySession.get())) {
+    return
+  }
 
   writeSession(sid, [])
 }
diff --git a/apps/desktop/src/store/layout.ts b/apps/desktop/src/store/layout.ts
index 74f4c38e679..a01e22961c3 100644
--- a/apps/desktop/src/store/layout.ts
+++ b/apps/desktop/src/store/layout.ts
@@ -1,6 +1,13 @@
 import { atom, computed, type ReadableAtom } from 'nanostores'
 
-import { arraysEqual, insertUniqueId, persistBoolean, persistStringArray, storedBoolean, storedStringArray } from '@/lib/storage'
+import {
+  arraysEqual,
+  insertUniqueId,
+  persistBoolean,
+  persistStringArray,
+  storedBoolean,
+  storedStringArray
+} from '@/lib/storage'
 
 import { $paneStates, ensurePaneRegistered, setPaneOpen, setPaneWidthOverride, togglePane } from './panes'
 
diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts
index ae2b7e7a8cd..642ab13ab1d 100644
--- a/apps/desktop/src/store/onboarding.ts
+++ b/apps/desktop/src/store/onboarding.ts
@@ -164,7 +164,9 @@ async function fetchProviderDefaultModel(
   // returned (model.options orders by recency / authenticated state, so
   // the just-authenticated provider is usually first anyway).
   const lower = preferredSlugs.map(s => s.toLowerCase())
-  const matched = providers.find((p: ModelOptionProvider) => lower.includes(String(p.slug).toLowerCase())) ?? providers[0]
+
+  const matched =
+    providers.find((p: ModelOptionProvider) => lower.includes(String(p.slug).toLowerCase())) ?? providers[0]
 
   const models = matched.models ?? []
 
diff --git a/apps/desktop/src/store/preview.ts b/apps/desktop/src/store/preview.ts
index 461cbc9a14d..3fff6a24086 100644
--- a/apps/desktop/src/store/preview.ts
+++ b/apps/desktop/src/store/preview.ts
@@ -400,6 +400,15 @@ export function closeRightRailTab(tabId: RightRailTabId) {
 
 export const closeActiveRightRailTab = () => closeRightRailTab($rightRailActiveTabId.get())
 
+/** Dismisses the active preview + every file tab so the rail pane unmounts. */
+export function closeRightRail() {
+  if ($previewTarget.get()) {
+    dismissPreviewTarget()
+  }
+
+  $filePreviewTabs.set([])
+}
+
 export function clearSessionPreviewRegistry() {
   $sessionPreviewRegistry.set({})
   setPreviewTarget(null)
diff --git a/apps/desktop/src/store/subagents.test.ts b/apps/desktop/src/store/subagents.test.ts
index 4d4d079aebe..6dee494e2ef 100644
--- a/apps/desktop/src/store/subagents.test.ts
+++ b/apps/desktop/src/store/subagents.test.ts
@@ -54,7 +54,13 @@ describe('subagent store', () => {
     )
     upsertSubagent(
       's1',
-      { status: 'running', subagent_id: 'a1', task_index: 0, tool_name: 'search_files', tool_preview: 'pattern=hermes' },
+      {
+        status: 'running',
+        subagent_id: 'a1',
+        task_index: 0,
+        tool_name: 'search_files',
+        tool_preview: 'pattern=hermes'
+      },
       false,
       'subagent.tool'
     )
diff --git a/apps/desktop/src/store/subagents.ts b/apps/desktop/src/store/subagents.ts
index db01e2db35d..bc94794c0e0 100644
--- a/apps/desktop/src/store/subagents.ts
+++ b/apps/desktop/src/store/subagents.ts
@@ -56,15 +56,24 @@ const asStatus = (v: unknown): SubagentStatus =>
 
 const compact = (text: string, max = PREVIEW_MAX) => {
   const line = text.replace(/\s+/g, ' ').trim()
-  if (!line) return ''
+
+  if (!line) {
+    return ''
+  }
+
   return line.length > max ? `${line.slice(0, max - 1)}…` : line
 }
 
 const toolLabel = (name: string) =>
-  name.split('_').filter(Boolean).map(p => p[0]!.toUpperCase() + p.slice(1)).join(' ') || name
+  name
+    .split('_')
+    .filter(Boolean)
+    .map(p => p[0]!.toUpperCase() + p.slice(1))
+    .join(' ') || name
 
 const formatTool = (name: string, preview = '') => {
   const snippet = compact(preview, TOOL_PREVIEW_MAX)
+
   return snippet ? `${toolLabel(name)}("${snippet}")` : toolLabel(name)
 }
 
@@ -90,7 +99,10 @@ const idOf = (p: SubagentPayload) =>
 
 const appendStream = (stream: SubagentStreamEntry[], entry: SubagentStreamEntry) => {
   const last = stream.at(-1)
-  if (last?.kind === entry.kind && last.text === entry.text && last.isError === entry.isError) return stream
+
+  if (last?.kind === entry.kind && last.text === entry.text && last.isError === entry.isError) {
+    return stream
+  }
 
   return [...stream, entry].slice(-MAX_STREAM)
 }
@@ -108,19 +120,29 @@ function streamFromPayload(
 
   for (const tail of asTail(payload.output_tail)) {
     const line = tail.tool ? formatTool(tail.tool, tail.preview ?? '') : compact(tail.preview ?? '')
-    if (line) out.push({ at, isError: tail.isError, kind: tail.tool ? 'tool' : 'progress', text: line })
+
+    if (line) {
+      out.push({ at, isError: tail.isError, kind: tail.tool ? 'tool' : 'progress', text: line })
+    }
   }
 
-  if (tool) out.push({ at, isError: !!payload.error, kind: 'tool', text: formatTool(tool, preview) })
+  if (tool) {
+    out.push({ at, isError: !!payload.error, kind: 'tool', text: formatTool(tool, preview) })
+  }
 
-  if (eventType === 'subagent.progress' && text)
+  if (eventType === 'subagent.progress' && text) {
     out.push({ at, isError: !!payload.error, kind: 'progress', text })
+  }
 
-  if (eventType === 'subagent.thinking' && text) out.push({ at, kind: 'thinking', text })
+  if (eventType === 'subagent.thinking' && text) {
+    out.push({ at, kind: 'thinking', text })
+  }
 
   const summary = compact(str(payload.summary) || str(payload.text))
-  if (TERMINAL.has(status) && summary)
+
+  if (TERMINAL.has(status) && summary) {
     out.push({ at, isError: status === 'failed', kind: 'summary', text: summary })
+  }
 
   return out
 }
@@ -158,7 +180,10 @@ function toProgress(payload: SubagentPayload, prev: SubagentProgress | undefined
 
 export function clearSessionSubagents(sid: string) {
   const map = $subagentsBySession.get()
-  if (!(sid in map)) return
+
+  if (!(sid in map)) {
+    return
+  }
 
   const { [sid]: _drop, ...rest } = map
   $subagentsBySession.set(rest)
@@ -167,10 +192,16 @@ export function clearSessionSubagents(sid: string) {
 export function pruneDelegateFallbackSubagents(sid: string) {
   const map = $subagentsBySession.get()
   const list = map[sid]
-  if (!list?.length) return
+
+  if (!list?.length) {
+    return
+  }
 
   const next = list.filter(item => !item.id.startsWith('delegate-tool:'))
-  if (next.length === list.length) return
+
+  if (next.length === list.length) {
+    return
+  }
 
   $subagentsBySession.set({ ...map, [sid]: next })
 }
@@ -180,10 +211,16 @@ export function upsertSubagent(sid: string, payload: SubagentPayload, createIfMi
   const list = map[sid] ?? []
   const id = idOf(payload)
   const idx = list.findIndex(item => item.id === id)
-  if (idx < 0 && !createIfMissing) return
+
+  if (idx < 0 && !createIfMissing) {
+    return
+  }
 
   const prev = idx >= 0 ? list[idx] : undefined
-  if (prev && TERMINAL.has(prev.status)) return
+
+  if (prev && TERMINAL.has(prev.status)) {
+    return
+  }
 
   const next = toProgress(payload, prev, eventType)
   const nextList = idx >= 0 ? list.map(item => (item.id === id ? next : item)) : [...list, next]
@@ -193,17 +230,26 @@ export function upsertSubagent(sid: string, payload: SubagentPayload, createIfMi
 
 export function buildSubagentTree(items: readonly SubagentProgress[]): SubagentNode[] {
   const nodes = new Map()
-  for (const item of items) nodes.set(item.id, { ...item, children: [] })
+
+  for (const item of items) {
+    nodes.set(item.id, { ...item, children: [] })
+  }
 
   const roots: SubagentNode[] = []
+
   for (const node of nodes.values()) {
     const parent = node.parentId ? nodes.get(node.parentId) : null
-    if (parent) parent.children.push(node)
-    else roots.push(node)
+
+    if (parent) {
+      parent.children.push(node)
+    } else {
+      roots.push(node)
+    }
   }
 
   const sort = (a: SubagentNode, b: SubagentNode) =>
     a.startedAt - b.startedAt || a.taskIndex - b.taskIndex || a.goal.localeCompare(b.goal)
+
   const walk = (node: SubagentNode) => node.children.sort(sort).forEach(walk)
   roots.sort(sort).forEach(walk)
 
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index fb9a8df2c01..33090db769d 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -103,11 +103,11 @@
     --theme-neutral-chrome: #f3f3f3;
     --theme-neutral-sidebar: #f3f3f3;
     --theme-neutral-card: #fcfcfc;
-    --theme-mix-chrome: 44%;
-    --theme-mix-sidebar: 36%;
+    --theme-mix-chrome: 92%;
+    --theme-mix-sidebar: 100%;
     --theme-mix-card: 22%;
     --theme-mix-elevated: 28%;
-    --theme-mix-bubble: 30%;
+    --theme-mix-bubble: 0%;
     --theme-fill-primary-accent-mix: 16%;
     --theme-fill-secondary-accent-mix: 11%;
     --theme-fill-tertiary-accent-mix: 8%;
@@ -117,6 +117,10 @@
     --theme-stroke-secondary-accent-mix: 16%;
     --theme-stroke-tertiary-accent-mix: 10%;
     --theme-stroke-quaternary-accent-mix: 6%;
+    --theme-row-hover-accent-mix: 4%;
+    --theme-row-active-accent-mix: 8%;
+    --theme-control-hover-accent-mix: 6%;
+    --theme-control-active-accent-mix: 8%;
 
     --ui-base: var(--theme-foreground);
     --ui-accent: var(--theme-midground);
@@ -164,6 +168,26 @@
       var(--ui-accent) var(--theme-fill-quinary-accent-mix),
       color-mix(in srgb, var(--ui-base) 3%, transparent)
     );
+    --ui-row-hover-background: color-mix(
+      in srgb,
+      var(--ui-accent) var(--theme-row-hover-accent-mix),
+      color-mix(in srgb, var(--ui-base) 3%, transparent)
+    );
+    --ui-row-active-background: color-mix(
+      in srgb,
+      var(--ui-accent) var(--theme-row-active-accent-mix),
+      color-mix(in srgb, var(--ui-base) 5%, transparent)
+    );
+    --ui-control-hover-background: color-mix(
+      in srgb,
+      var(--ui-accent) var(--theme-control-hover-accent-mix),
+      color-mix(in srgb, var(--ui-base) 4%, transparent)
+    );
+    --ui-control-active-background: color-mix(
+      in srgb,
+      var(--ui-accent) var(--theme-control-active-accent-mix),
+      color-mix(in srgb, var(--ui-base) 5%, transparent)
+    );
     --ui-text-primary: color-mix(in srgb, var(--ui-base) 94%, transparent);
     --ui-text-secondary: color-mix(in srgb, var(--ui-base) 74%, transparent);
     --ui-text-tertiary: color-mix(in srgb, var(--ui-base) 54%, transparent);
@@ -188,17 +212,18 @@
       var(--ui-accent) var(--theme-stroke-quaternary-accent-mix),
       color-mix(in srgb, var(--ui-base) 3%, transparent)
     );
-    --glass-sash-hover-border: color-mix(in srgb, var(--ui-accent) 18%, var(--ui-stroke-tertiary));
-    --glass-sash-hover-background: color-mix(in srgb, var(--ui-accent) 6%, transparent);
-    --glass-surface-background: var(--ui-bg-editor);
-    --glass-sidebar-surface-background: var(--ui-bg-sidebar);
-    --glass-chat-surface-background: var(--ui-bg-chrome);
-    --glass-editor-surface-background: var(--ui-bg-chrome);
-    --glass-chat-bubble-background: color-mix(in srgb, var(--theme-bubble-seed) var(--theme-mix-bubble), var(--theme-neutral-card));
-    --glass-chat-bubble-opaque-background: var(--ui-bg-editor);
-    --glass-inline-code-background: color-mix(in srgb, #141414 5%, transparent);
-    --glass-inline-code-border: color-mix(in srgb, #141414 8%, transparent);
-    --glass-inline-code-foreground: color-mix(in srgb, #141414 88%, transparent);
+    --ui-sash-hover-border: color-mix(in srgb, var(--ui-accent) 18%, var(--ui-stroke-tertiary));
+    --ui-sash-hover-background: color-mix(in srgb, var(--ui-accent) 6%, transparent);
+    --ui-surface-background: var(--ui-bg-editor);
+    --ui-sidebar-surface-background: var(--ui-bg-sidebar);
+    --ui-chat-surface-background: var(--ui-bg-chrome);
+    --ui-editor-surface-background: var(--ui-bg-chrome);
+    --ui-chat-bubble-background: color-mix(in srgb, var(--theme-bubble-seed) var(--theme-mix-bubble), var(--theme-neutral-card));
+    --ui-chat-bubble-opaque-background: var(--ui-bg-editor);
+    --ui-inline-code-background: color-mix(in srgb, #141414 5%, transparent);
+    --ui-inline-code-border: color-mix(in srgb, #141414 8%, transparent);
+    --ui-inline-code-foreground: color-mix(in srgb, #141414 88%, transparent);
+    --ui-selection-background: color-mix(in srgb, #ffd24a 55%, transparent);
 
     --dt-background: var(--ui-bg-chrome);
     --dt-foreground: var(--ui-text-primary);
@@ -217,12 +242,13 @@
     --dt-border: var(--ui-stroke-secondary);
     --dt-input: var(--ui-stroke-primary);
     --dt-ring: var(--ui-stroke-primary);
+    --dt-midground: var(--theme-midground);
     --dt-composer-ring: var(--ui-base);
     --dt-destructive: #cf2d56;
     --dt-destructive-foreground: #ffffff;
     --dt-sidebar-bg: var(--ui-bg-sidebar);
     --dt-sidebar-border: var(--ui-stroke-secondary);
-    --dt-user-bubble: var(--ui-bg-editor);
+    --dt-user-bubble: var(--ui-chat-bubble-background);
     --dt-user-bubble-border: var(--ui-stroke-tertiary);
 
     --dt-font-sans: 'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
@@ -280,12 +306,12 @@
     --sidebar-foreground: var(--dt-foreground);
     --sidebar-primary: var(--dt-primary);
     --sidebar-primary-foreground: var(--dt-primary-foreground);
-    --sidebar-accent: var(--dt-accent);
+    --sidebar-accent: var(--ui-control-active-background);
     --sidebar-accent-foreground: var(--dt-accent-foreground);
     --sidebar-border: var(--dt-sidebar-border);
     --sidebar-ring: var(--dt-ring);
     --sidebar-edge-border: color-mix(in srgb, var(--ui-base) 7.5%, transparent);
-    --chrome-action-hover: var(--ui-bg-tertiary);
+    --chrome-action-hover: var(--ui-control-hover-background);
 
     --midground: var(--dt-midground);
     --background: var(--dt-background);
@@ -298,50 +324,28 @@
   }
 
   :root.dark {
-    --ui-base: #ffe6cb;
-    --ui-accent: #0053fd;
-    --ui-accent-secondary: #ffe6cb;
-    --ui-warm: #ffe6cb;
+    /* Per-mode mix knobs — overridden inline by `applyTheme()` per skin. */
+    --theme-mix-chrome: 74%;
+    --theme-mix-card: 38%;
+    --theme-mix-elevated: 46%;
+    --theme-mix-bubble: 46%;
+    --theme-neutral-chrome: #0d0d0e;
+    --theme-neutral-sidebar: #0a0a0b;
+    --theme-neutral-card: #161618;
+
+    /* Dark-only accent palette overrides. */
     --ui-red: #e75e78;
-    --ui-orange: #db704b;
-    --ui-yellow: #c08532;
     --ui-green: #55a583;
     --ui-cyan: #6f9ba6;
-    --ui-blue: #0053fd;
-    --ui-purple: #9e94d5;
-    --ui-bg-chrome: #0d1d3a;
-    --ui-bg-sidebar: #0a1833;
-    --ui-bg-editor: #101827;
-    --ui-bg-elevated: #121d32;
-    --ui-bg-input: #131316;
-    --dt-background: var(--ui-bg-chrome);
-    --dt-foreground: var(--ui-text-primary);
-    --dt-card: var(--ui-bg-elevated);
-    --dt-card-foreground: var(--ui-text-primary);
-    --dt-muted: var(--ui-bg-tertiary);
-    --dt-muted-foreground: var(--ui-text-tertiary);
-    --dt-popover: color-mix(in srgb, var(--ui-bg-elevated) 96%, transparent);
-    --dt-popover-foreground: var(--ui-text-primary);
-    --dt-primary: var(--ui-accent);
-    --dt-primary-foreground: #0b0b0c;
-    --dt-secondary: var(--ui-bg-secondary);
-    --dt-secondary-foreground: var(--ui-text-secondary);
-    --dt-accent: var(--ui-bg-tertiary);
-    --dt-accent-foreground: var(--ui-text-primary);
-    --dt-border: var(--ui-stroke-secondary);
-    --dt-input: var(--ui-stroke-primary);
-    --dt-ring: var(--ui-stroke-primary);
-    --dt-composer-ring: var(--ui-base);
-    --dt-sidebar-bg: var(--ui-bg-sidebar);
-    --dt-sidebar-border: var(--ui-stroke-secondary);
-    --dt-user-bubble: var(--ui-bg-elevated);
-    --dt-user-bubble-border: var(--ui-stroke-tertiary);
+
     --sidebar-edge-border: color-mix(in srgb, var(--ui-base) 12%, transparent);
     --composer-ring-strength: 1.3;
     --backdrop-invert-mul: 0;
-    --glass-inline-code-background: color-mix(in srgb, #ffffff 7%, transparent);
-    --glass-inline-code-border: color-mix(in srgb, #ffffff 10%, transparent);
-    --glass-inline-code-foreground: color-mix(in srgb, #ffffff 88%, transparent);
+
+    --ui-inline-code-background: color-mix(in srgb, #ffffff 7%, transparent);
+    --ui-inline-code-border: color-mix(in srgb, #ffffff 10%, transparent);
+    --ui-inline-code-foreground: color-mix(in srgb, #ffffff 88%, transparent);
+    --ui-selection-background: color-mix(in srgb, #ffd24a 38%, transparent);
   }
 
   * {
@@ -361,7 +365,7 @@
 
   body {
     margin: 0;
-    background: var(--glass-chat-surface-background);
+    background: var(--ui-chat-surface-background);
     color: var(--dt-foreground);
     font-family: var(--dt-font-sans);
     font-size: 0.8125rem;
@@ -378,9 +382,22 @@
     font: inherit;
   }
 
+  :where(
+    a,
+    .underline,
+    [class~='hover:underline'],
+    [class~='focus:underline'],
+    [class~='focus-visible:underline'],
+    [class~='group-hover:underline'],
+    [class~='peer-hover:underline']
+  ) {
+    text-decoration-color: color-mix(in srgb, currentColor 20%, transparent);
+    text-underline-offset: 0.25rem;
+  }
+
   *::selection {
-    background: var(--dt-midground);
-    color: var(--dt-midground-foreground);
+    background: var(--ui-selection-background);
+    color: inherit;
   }
 }
 
@@ -742,7 +759,7 @@ canvas {
   background: linear-gradient(
     to bottom,
     transparent,
-    color-mix(in srgb, var(--glass-chat-surface-background) 88%, transparent)
+    color-mix(in srgb, var(--ui-chat-surface-background) 88%, transparent)
   ) !important;
 }
 
@@ -751,12 +768,6 @@ canvas {
   box-shadow: var(--shadow-composer) !important;
 }
 
-[data-slot='composer-surface'] > [aria-hidden='true'] {
-  background: var(--glass-chat-bubble-background) !important;
-  backdrop-filter: blur(0.75rem) saturate(1.08);
-  -webkit-backdrop-filter: blur(0.75rem) saturate(1.08);
-}
-
 [data-slot='composer-fade'] {
   min-height: 2.375rem;
 }
@@ -771,7 +782,7 @@ canvas {
 }
 
 [data-slot='composer-root']:focus-within [data-slot='composer-surface'] > [aria-hidden='true'] {
-  background: var(--glass-chat-bubble-background) !important;
+  background: var(--ui-chat-bubble-background) !important;
 }
 
 /* Tool/thinking blocks now live at message-text alignment (no leading
@@ -818,9 +829,9 @@ canvas {
 }
 
 [data-slot='aui_assistant-message-content'] .aui-md :not(pre) > code {
-  border: 0.0625rem solid var(--glass-inline-code-border);
-  background: var(--glass-inline-code-background);
-  color: var(--glass-inline-code-foreground);
+  border: 0.0625rem solid var(--ui-inline-code-border);
+  background: var(--ui-inline-code-background);
+  color: var(--ui-inline-code-foreground);
 }
 
 [data-slot='aui_assistant-message-content'] .aui-md :where(.aui-shiki, .aui-shiki > pre) {
@@ -843,18 +854,18 @@ canvas {
 }
 
 /* Tool / thinking blocks are scaffolding around the model's reply, so we
-   fade them slightly. The reading column (prose) stays at full strength;
-   scaffolding recedes and lifts back to full opacity on hover/focus so it
-   stays legible when the user actually wants to read it. */
-[data-slot='aui_assistant-message-content']
-  > :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure']) {
+   keep them transparent and fade them slightly. The reading column (prose)
+   stays at full strength; scaffolding lifts back to full opacity on
+   hover/focus so it stays legible when the user actually wants to read it. */
+[data-slot='tool-block'],
+[data-slot='aui_thinking-disclosure'] {
   background: transparent !important;
-  opacity: 0.67;
-  transition: opacity 120ms ease-out;
 }
 
-[data-slot='tool-block'] {
-  background: transparent !important;
+[data-slot='aui_assistant-message-content']
+  > :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure']) {
+  opacity: 0.67;
+  transition: opacity 120ms ease-out;
 }
 
 [data-slot='aui_assistant-message-content']
@@ -909,10 +920,15 @@ canvas {
   min-height: 0;
   min-width: 0;
   flex-shrink: 0;
+  cursor: pointer;
   color: var(--color-muted-foreground);
   opacity: 0.5;
 }
 
+[data-slot='aui_msg-actions'] button:disabled {
+  cursor: default;
+}
+
 [data-slot='aui_msg-actions'] button:hover {
   background: transparent;
   color: var(--color-foreground);
diff --git a/apps/desktop/src/themes/context.tsx b/apps/desktop/src/themes/context.tsx
index a198402a95b..4ee3282450f 100644
--- a/apps/desktop/src/themes/context.tsx
+++ b/apps/desktop/src/themes/context.tsx
@@ -148,6 +148,17 @@ function renderedModeFor(colors: DesktopThemeColors, mode: 'light' | 'dark'): 'l
 
 // ─── CSS application ────────────────────────────────────────────────────────
 
+// Per-mode mix knobs. Light/dark fallbacks live in styles.css `:root` /
+// `:root.dark`; setting them inline keeps active-skin overrides surviving
+// the boot-time paint.
+const mixesFor = (isDark: boolean): Record => ({
+  '--theme-mix-chrome': isDark ? '74%' : '92%',
+  '--theme-mix-sidebar': '100%',
+  '--theme-mix-card': isDark ? '38%' : '22%',
+  '--theme-mix-elevated': isDark ? '46%' : '28%',
+  '--theme-mix-bubble': isDark ? '46%' : '0%'
+})
+
 function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
   if (typeof document === 'undefined') {
     return
@@ -157,95 +168,54 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
   const c = theme.colors
   const typo = { ...DEFAULT_TYPOGRAPHY, ...nousTheme.typography, ...theme.typography }
   const rendered = renderedModeFor(c, mode)
+  const isDark = rendered === 'dark'
   const midground = c.midground ?? c.ring
   const skinName = theme.name.endsWith(`-${mode}`) ? theme.name.slice(0, -mode.length - 1) : theme.name
-  const neutralChrome = rendered === 'dark' ? '#0d0d0e' : '#f3f3f3'
-  const neutralSidebar = rendered === 'dark' ? '#0a0a0b' : '#f3f3f3'
-  const neutralCard = rendered === 'dark' ? '#161618' : '#fcfcfc'
-  const chromeMix = rendered === 'dark' ? 36 : 44
-  const sidebarMix = rendered === 'dark' ? 42 : 36
-  const cardMix = rendered === 'dark' ? 38 : 22
-  const elevatedMix = rendered === 'dark' ? 46 : 28
-  const bubbleMix = rendered === 'dark' ? 48 : 30
 
   root.style.setProperty('color-scheme', rendered)
   root.dataset.hermesTheme = skinName
   root.dataset.hermesMode = rendered
-  root.classList.toggle('dark', rendered === 'dark')
+  root.classList.toggle('dark', isDark)
+
+  // Brand seeds feed every glass + shadcn token via `color-mix()` in styles.css.
+  const seeds: Record = {
+    '--theme-foreground': c.foreground,
+    '--theme-primary': c.primary,
+    '--theme-secondary': c.secondary,
+    '--theme-accent-soft': c.accent,
+    '--theme-midground': midground,
+    '--theme-warm': c.primary,
+    '--theme-background-seed': c.background,
+    '--theme-sidebar-seed': c.sidebarBackground ?? c.background,
+    '--theme-card-seed': c.card,
+    '--theme-elevated-seed': c.popover,
+    '--theme-bubble-seed': c.userBubble ?? c.popover
+  }
+
+  // shadcn/Tailwind tokens that aren't derived from the seed chain.
+  const palette: Record = {
+    '--dt-primary-foreground': c.primaryForeground,
+    '--dt-secondary-foreground': c.secondaryForeground,
+    '--dt-accent-foreground': c.accentForeground,
+    '--dt-border': c.border,
+    '--dt-input': c.input,
+    '--dt-ring': c.ring,
+    '--dt-muted': c.muted,
+    '--dt-midground-foreground': c.midgroundForeground ?? readableOn(midground),
+    '--dt-composer-ring': c.composerRing ?? midground,
+    '--dt-destructive': c.destructive,
+    '--dt-destructive-foreground': c.destructiveForeground,
+    '--dt-sidebar-border': c.sidebarBorder ?? c.border,
+    '--dt-user-bubble-border': c.userBubbleBorder ?? c.border,
+    '--dt-font-sans': typo.fontSans,
+    '--dt-font-mono': typo.fontMono,
+    '--noise-opacity-mul': isDark ? 'calc(0.04 / 0.21)' : 'calc(0.34 / 0.21)'
+  }
+
+  for (const [k, v] of Object.entries({ ...seeds, ...mixesFor(isDark), ...palette })) {
+    root.style.setProperty(k, v)
+  }
 
-  const set = (k: string, v: string) => root.style.setProperty(k, v)
-  set('--theme-foreground', c.foreground)
-  set('--theme-primary', c.primary)
-  set('--theme-secondary', c.secondary)
-  set('--theme-accent-soft', c.accent)
-  set('--theme-midground', midground)
-  set('--theme-warm', c.primary)
-  set('--theme-background-seed', c.background)
-  set('--theme-sidebar-seed', c.sidebarBackground ?? c.background)
-  set('--theme-card-seed', c.card)
-  set('--theme-elevated-seed', c.popover)
-  set('--theme-bubble-seed', c.userBubble ?? c.popover)
-  set('--theme-neutral-chrome', neutralChrome)
-  set('--theme-neutral-sidebar', neutralSidebar)
-  set('--theme-neutral-card', neutralCard)
-  set('--theme-mix-chrome', `${chromeMix}%`)
-  set('--theme-mix-sidebar', `${sidebarMix}%`)
-  set('--theme-mix-card', `${cardMix}%`)
-  set('--theme-mix-elevated', `${elevatedMix}%`)
-  set('--theme-mix-bubble', `${bubbleMix}%`)
-  set('--theme-fill-primary-accent-mix', '16%')
-  set('--theme-fill-secondary-accent-mix', '11%')
-  set('--theme-fill-tertiary-accent-mix', '8%')
-  set('--theme-fill-quaternary-accent-mix', '5%')
-  set('--theme-fill-quinary-accent-mix', '3%')
-  set('--theme-stroke-primary-accent-mix', '24%')
-  set('--theme-stroke-secondary-accent-mix', '16%')
-  set('--theme-stroke-tertiary-accent-mix', '10%')
-  set('--theme-stroke-quaternary-accent-mix', '6%')
-  set('--ui-base', 'var(--theme-foreground)')
-  set('--ui-accent', 'var(--theme-midground)')
-  set('--ui-accent-secondary', 'var(--theme-primary)')
-  set('--ui-warm', 'var(--theme-warm)')
-  set('--ui-bg-chrome', 'color-mix(in srgb, var(--theme-background-seed) var(--theme-mix-chrome), var(--theme-neutral-chrome))')
-  set('--ui-bg-sidebar', 'color-mix(in srgb, var(--theme-sidebar-seed) var(--theme-mix-sidebar), var(--theme-neutral-sidebar))')
-  set('--ui-bg-editor', 'color-mix(in srgb, var(--theme-card-seed) var(--theme-mix-card), var(--theme-neutral-card))')
-  set('--ui-bg-elevated', 'color-mix(in srgb, var(--theme-elevated-seed) var(--theme-mix-elevated), var(--theme-neutral-card))')
-  set('--ui-bg-input', 'var(--ui-bg-editor)')
-  set('--glass-surface-background', 'var(--ui-bg-editor)')
-  set('--glass-sidebar-surface-background', 'var(--ui-bg-sidebar)')
-  set('--glass-chat-surface-background', 'var(--ui-bg-chrome)')
-  set('--glass-editor-surface-background', 'var(--ui-bg-chrome)')
-  set('--glass-chat-bubble-background', 'color-mix(in srgb, var(--theme-bubble-seed) var(--theme-mix-bubble), var(--theme-neutral-card))')
-  set('--glass-chat-bubble-opaque-background', 'var(--ui-bg-editor)')
-  set('--dt-background', 'var(--ui-bg-chrome)')
-  set('--dt-foreground', 'var(--ui-text-primary)')
-  set('--dt-card', 'var(--ui-bg-editor)')
-  set('--dt-card-foreground', 'var(--ui-text-primary)')
-  set('--dt-muted', c.muted)
-  set('--dt-muted-foreground', 'var(--ui-text-tertiary)')
-  set('--dt-popover', 'var(--ui-bg-elevated)')
-  set('--dt-popover-foreground', 'var(--ui-text-primary)')
-  set('--dt-primary', 'var(--theme-primary)')
-  set('--dt-primary-foreground', c.primaryForeground)
-  set('--dt-secondary', 'var(--theme-secondary)')
-  set('--dt-secondary-foreground', c.secondaryForeground)
-  set('--dt-accent', 'var(--theme-accent-soft)')
-  set('--dt-accent-foreground', c.accentForeground)
-  set('--dt-border', c.border)
-  set('--dt-input', c.input)
-  set('--dt-ring', c.ring)
-  set('--dt-midground', 'var(--theme-midground)')
-  set('--dt-midground-foreground', c.midgroundForeground ?? readableOn(midground))
-  set('--dt-composer-ring', c.composerRing ?? midground)
-  set('--dt-destructive', c.destructive)
-  set('--dt-destructive-foreground', c.destructiveForeground)
-  set('--dt-sidebar-bg', 'var(--ui-bg-sidebar)')
-  set('--dt-sidebar-border', c.sidebarBorder ?? c.border)
-  set('--dt-user-bubble', 'var(--glass-chat-bubble-background)')
-  set('--dt-user-bubble-border', c.userBubbleBorder ?? c.border)
-  set('--dt-font-sans', typo.fontSans)
-  set('--dt-font-mono', typo.fontMono)
-  set('--noise-opacity-mul', rendered === 'dark' ? 'calc(0.04 / 0.21)' : 'calc(0.34 / 0.21)')
   window.hermesDesktop?.setTitleBarTheme?.({
     background: c.background,
     foreground: c.foreground
diff --git a/website/src/components/UserStoriesCollage/styles.module.css b/website/src/components/UserStoriesCollage/styles.module.css
index bc365e47b20..42b9e6c7f93 100644
--- a/website/src/components/UserStoriesCollage/styles.module.css
+++ b/website/src/components/UserStoriesCollage/styles.module.css
@@ -242,7 +242,11 @@
   text-decoration: none;
   font-weight: 600;
 }
-.footer a:hover { text-decoration: underline; }
+.footer a:hover {
+  text-decoration: underline;
+  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
+  text-underline-offset: 4px;
+}
 
 .empty {
   padding: 3rem 1rem;