From b8d220f2684c8627b3fab33aa7bc10a129838353 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 25 Jun 2026 16:40:27 -0500 Subject: [PATCH] feat(desktop): wire project settings and shell chrome --- apps/desktop/src/app/profiles/index.tsx | 13 ++- .../src/app/settings/about-settings.tsx | 5 +- apps/desktop/src/app/settings/constants.ts | 4 +- apps/desktop/src/app/settings/index.tsx | 12 +-- .../src/app/settings/sessions-settings.tsx | 5 +- .../src/app/shell/gateway-menu-panel.tsx | 6 +- .../app/shell/hooks/use-statusbar-items.tsx | 4 +- apps/desktop/src/styles.css | 94 ++++++++++++++++--- nix/desktop.nix | 8 ++ 9 files changed, 116 insertions(+), 35 deletions(-) diff --git a/apps/desktop/src/app/profiles/index.tsx b/apps/desktop/src/app/profiles/index.tsx index 32249c47906..a9a0c5e70c2 100644 --- a/apps/desktop/src/app/profiles/index.tsx +++ b/apps/desktop/src/app/profiles/index.tsx @@ -11,7 +11,7 @@ import { DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' +import { SanitizedInput } from '@/components/ui/sanitized-input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { @@ -26,6 +26,7 @@ import { } from '@/hermes' import { useI18n } from '@/i18n' import { AlertTriangle, Pencil, Save, Terminal, Trash2, Users } from '@/lib/icons' +import { slug } from '@/lib/sanitize' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' @@ -519,12 +520,13 @@ function CreateProfileDialog({ - setName(event.target.value)} + onValueChange={setName} placeholder="my-profile" + sanitize={slug} value={name} />

@@ -648,11 +650,12 @@ function RenameProfileDialog({ - setName(event.target.value)} + onValueChange={setName} + sanitize={slug} value={name} />

diff --git a/apps/desktop/src/app/settings/about-settings.tsx b/apps/desktop/src/app/settings/about-settings.tsx index c1d56115d6c..fa0ca76a2c3 100644 --- a/apps/desktop/src/app/settings/about-settings.tsx +++ b/apps/desktop/src/app/settings/about-settings.tsx @@ -3,8 +3,9 @@ import { useEffect, useState } from 'react' import { BrandMark } from '@/components/brand-mark' import { Button } from '@/components/ui/button' +import { Codicon } from '@/components/ui/codicon' import { type Translations, useI18n } from '@/i18n' -import { CheckCircle2, ExternalLink, Loader2, RefreshCw, Sparkles } from '@/lib/icons' +import { CheckCircle2, ExternalLink, Loader2, RefreshCw } from '@/lib/icons' import { cn } from '@/lib/utils' import { $desktopVersion, @@ -117,7 +118,7 @@ export function AboutSettings() { >

{statusTone === 'available' ? ( - + ) : statusTone === 'error' ? null : ( )} diff --git a/apps/desktop/src/app/settings/constants.ts b/apps/desktop/src/app/settings/constants.ts index 5295cd6866f..8f1e466d1b6 100644 --- a/apps/desktop/src/app/settings/constants.ts +++ b/apps/desktop/src/app/settings/constants.ts @@ -1,3 +1,4 @@ +import { codiconIcon } from '@/components/ui/codicon' import { Brain, type IconComponent, @@ -7,7 +8,6 @@ import { Monitor, Moon, Palette, - Sparkles, Sun, Wrench } from '@/lib/icons' @@ -501,7 +501,7 @@ export const SECTIONS: DesktopConfigSection[] = [ { id: 'model', label: 'Model', - icon: Sparkles, + icon: codiconIcon('hubot'), keys: ['model_context_length', 'fallback_providers'] }, { diff --git a/apps/desktop/src/app/settings/index.tsx b/apps/desktop/src/app/settings/index.tsx index ecf0f29377d..6edb00979fb 100644 --- a/apps/desktop/src/app/settings/index.tsx +++ b/apps/desktop/src/app/settings/index.tsx @@ -1,11 +1,11 @@ -import { IconDownload, IconRefresh, IconUpload } from '@tabler/icons-react' import { useRef } from 'react' import { Tip } from '@/components/ui/tooltip' import { getHermesConfigDefaults, getHermesConfigRecord, saveHermesConfig } from '@/hermes' import { useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' -import { Archive, Bell, Globe, Info, KeyRound, Settings2, Sparkles, Wrench, Zap } from '@/lib/icons' +import { codiconIcon } from '@/components/ui/codicon' +import { Archive, Bell, Download, Globe, Info, KeyRound, RefreshCw, Settings2, Upload, Wrench, Zap } from '@/lib/icons' import { notifyError } from '@/store/notifications' import { useRouteEnumParam } from '../hooks/use-route-enum-param' @@ -120,7 +120,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang
openProviderView('accounts')} @@ -186,7 +186,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang
void exportConfig()}> - + @@ -196,7 +196,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang importInputRef.current?.click() }} > - + @@ -207,7 +207,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang void resetConfig() }} > - +
diff --git a/apps/desktop/src/app/settings/sessions-settings.tsx b/apps/desktop/src/app/settings/sessions-settings.tsx index f644ded929c..ca93c547776 100644 --- a/apps/desktop/src/app/settings/sessions-settings.tsx +++ b/apps/desktop/src/app/settings/sessions-settings.tsx @@ -8,6 +8,7 @@ import { sessionTitle } from '@/lib/chat-runtime' import { triggerHaptic } from '@/lib/haptics' import { Archive, ArchiveOff, FolderOpen, Loader2, Trash2 } from '@/lib/icons' import { notify, notifyError } from '@/store/notifications' +import { untombstoneSessions } from '@/store/projects' import { applyConfiguredDefaultProjectDir, ensureDefaultWorkspaceCwd, setSessions } from '@/store/session' import type { SessionInfo } from '@/types/hermes' @@ -62,7 +63,9 @@ export function SessionsSettings() { try { await setSessionArchived(session.id, false, session.profile) setLocalSessions(prev => prev.filter(s => s.id !== session.id)) - // Surface it again in the sidebar without waiting for a full refresh. + // Surface it again in the sidebar without waiting for a full refresh, and + // lift any optimistic eviction so the grouped tree shows it again too. + untombstoneSessions([session.id, session._lineage_root_id]) setSessions(prev => [{ ...session, archived: false }, ...prev.filter(s => s.id !== session.id)]) triggerHaptic('selection') notify({ durationMs: 2_000, kind: 'success', message: s.restored }) diff --git a/apps/desktop/src/app/shell/gateway-menu-panel.tsx b/apps/desktop/src/app/shell/gateway-menu-panel.tsx index 04624787854..72a26d33e54 100644 --- a/apps/desktop/src/app/shell/gateway-menu-panel.tsx +++ b/apps/desktop/src/app/shell/gateway-menu-panel.tsx @@ -1,10 +1,8 @@ -import { IconLayoutDashboard } from '@tabler/icons-react' - import { StatusDot, type StatusTone } from '@/components/status-dot' import { Button } from '@/components/ui/button' import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' -import { Activity, AlertCircle } from '@/lib/icons' +import { Activity, AlertCircle, LayoutDashboard } from '@/lib/icons' import type { RuntimeReadinessResult } from '@/lib/runtime-readiness' import { cn } from '@/lib/utils' import type { StatusResponse } from '@/types/hermes' @@ -88,7 +86,7 @@ export function GatewayMenuPanel({ size="icon-sm" variant="ghost" > - +
diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index aad85f11c5f..3d1f27eb205 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react' import type { CommandCenterSection } from '@/app/command-center' import { $terminalTakeover, setTerminalTakeover } from '@/app/right-sidebar/store' import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel' +import { Codicon } from '@/components/ui/codicon' import { GlyphSpinner } from '@/components/ui/glyph-spinner' import { useI18n } from '@/i18n' import { @@ -13,7 +14,6 @@ import { Command, Hash, Loader2, - Sparkles, Terminal, Zap, ZapFilled @@ -322,7 +322,7 @@ export function useStatusbarItems({ ) : subagentsRunning > 0 ? ( ) : ( - + ), id: 'agents', label: copy.agents, diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index f4026ba5e29..e87b5bf9639 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -471,6 +471,31 @@ background: repeating-conic-gradient(currentColor 0% 25%, transparent 0% 50%) 0 0 / 0.125rem 0.125rem; } +/* Hover-reveal suppression — the shared, declarative escape hatch. + A collapsed pane slides in when the pointer dwells on its thin edge trigger. + Controls that sit over that edge gutter would drag the panel in by accident. + Mark any such region and, while it's hovered, the matching edge trigger(s) go + pointer-transparent. Auto-resets on mouse-out, no JS. + + - data-suppress-pane-reveal → kills BOTH edges (use for small regions + like the thread timeline). + - data-suppress-pane-reveal-side → kills only the hovered region's OWN side + (side read from its [data-pane-side] + pane ancestor), so the opposite sidebar + stays summonable while you work a pane. */ +[data-pane-shell]:has([data-suppress-pane-reveal]:hover) [data-pane-reveal-trigger] { + pointer-events: none; +} + +[data-pane-shell]:has([data-pane-side='left'] [data-suppress-pane-reveal-side]:hover) + [data-pane-side='left'] + [data-pane-reveal-trigger], +[data-pane-shell]:has([data-pane-side='right'] [data-suppress-pane-reveal-side]:hover) + [data-pane-side='right'] + [data-pane-reveal-trigger] { + pointer-events: none; +} + :root:not([style*='--theme-asset-bg:']) .theme-default-filler { display: block; } @@ -709,6 +734,28 @@ canvas { -webkit-user-drag: none; } +/* Tabs render 2-wide on every HTML code surface — the source preview, inline + diffs, markdown code blocks, tool output — so indentation matches the editor + and the terminal instead of the browser default (8). */ +pre, +code { + tab-size: 2; + -moz-tab-size: 2; +} + +/* Arc-style multicolor action surface (static, not animated). Reusable on any + Button via className. Unlayered so it beats Tailwind's bg-*/text-* variant + utilities. */ +.btn-arc { + background-image: linear-gradient(110deg, #5b6cff 0%, #8b5cf6 28%, #d946ef 58%, #fb7185 82%, #fb923c 100%); + color: #fff; + border-color: transparent; +} + +.btn-arc:hover:not(:disabled) { + filter: brightness(1.08) saturate(1.06); +} + /* Shared input chrome — mirrors composer hover/focus FX. Unlayered to beat Tailwind utilities. */ .desktop-input-chrome { --ring-pct: 18%; @@ -1064,12 +1111,6 @@ canvas { border-color: var(--ui-stroke-secondary) !important; } -/* On focus we don't change the fill — just shift the border ~15% toward the - foreground, which darkens it in light mode and lightens it in dark mode. */ -[data-slot='composer-surface']:focus-within { - border-color: color-mix(in srgb, var(--ui-stroke-secondary) 85%, var(--dt-foreground)) !important; -} - [data-slot='composer-fade'] { min-height: 2.375rem; } @@ -1235,21 +1276,48 @@ canvas { opacity: 1; } -/* Syntax-highlighted inline diff (Shiki): strip the theme's own surface + - default margins so context lines stay transparent and each changed line owns - its tint. `display: grid` on the code puts one `.line` per row and drops the - whitespace-only `\n` nodes between them — without it, full-width block lines - double up with the literal newlines (phantom blank rows). */ +/* Shiki surfaces in the inline file diff + source preview: strip the theme's + own background/margins, and lay each `.line` on its own grid row so the + inter-line `\n` text nodes can't double-space full-width rows. Empty lines + carry a non-breaking space so blank rows keep their height. */ [data-slot='file-diff-panel'] .shiki, -[data-slot='file-diff-panel'] .shiki code { +[data-slot='file-diff-panel'] .shiki code, +.preview-source-code .shiki, +.preview-source-code .shiki code { margin: 0; background: transparent !important; } -[data-slot='file-diff-panel'] .shiki code { +[data-slot='file-diff-panel'] .shiki code, +.preview-source-code .shiki code { display: grid; } +[data-slot='file-diff-panel'] .shiki code .line:empty::before, +.preview-source-code .shiki code .line:empty::before { + content: '\00a0'; +} + +/* Inline diff rows keep their utility-class padding; just floor the height. */ +[data-slot='file-diff-panel'] .shiki code .line { + min-height: 1.25rem; + white-space: pre; +} + +/* Source rows are a fixed editor-height box so source⇄diff toggling never + shifts and the gutter stays aligned. */ +.preview-source-code .shiki code { + min-width: max-content; +} + +.preview-source-code .shiki code .line { + display: block; + height: 1.25rem; + padding: 0 0.625rem; + line-height: 1.25rem; + white-space: pre; +} + /* The github-dark token palette reads candy-bright at our small code size. `github-dark-dimmed` only dims the *background* (which we strip), so soften the token *foregrounds* directly — a small saturation + brightness pullback, diff --git a/nix/desktop.nix b/nix/desktop.nix index d1c312b9b2d..544895096c0 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -54,6 +54,14 @@ let npm exec tsc -b npm exec vite build + + # Bundle the electron main into a single self-contained file so + # the nix output doesn't need node_modules/. simple-git (the only + # external runtime dep of the electron main) gets inlined; electron + # and node-pty are external (provided by the runtime / native-deps). + # preload.cjs stays separate — Electron loads it via __dirname, not + # require(), so it must remain a standalone file. + node scripts/bundle-electron-main.mjs popd runHook postBuild