mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
feat(desktop): wire project settings and shell chrome
This commit is contained in:
parent
62af32efe7
commit
b8d220f268
9 changed files with 116 additions and 35 deletions
|
|
@ -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({
|
|||
<label className="text-xs font-medium" htmlFor="new-profile-name">
|
||||
{p.nameLabel}
|
||||
</label>
|
||||
<Input
|
||||
<SanitizedInput
|
||||
aria-invalid={invalid}
|
||||
autoFocus
|
||||
id="new-profile-name"
|
||||
onChange={event => setName(event.target.value)}
|
||||
onValueChange={setName}
|
||||
placeholder="my-profile"
|
||||
sanitize={slug}
|
||||
value={name}
|
||||
/>
|
||||
<p className={cn('text-[0.66rem] leading-4', invalid ? 'text-destructive' : 'text-muted-foreground')}>
|
||||
|
|
@ -648,11 +650,12 @@ function RenameProfileDialog({
|
|||
<label className="text-xs font-medium" htmlFor="rename-profile-name">
|
||||
{p.newNameLabel}
|
||||
</label>
|
||||
<Input
|
||||
<SanitizedInput
|
||||
aria-invalid={invalid}
|
||||
autoFocus
|
||||
id="rename-profile-name"
|
||||
onChange={event => setName(event.target.value)}
|
||||
onValueChange={setName}
|
||||
sanitize={slug}
|
||||
value={name}
|
||||
/>
|
||||
<p className={cn('text-[0.66rem] leading-4', invalid ? 'text-destructive' : 'text-muted-foreground')}>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{statusTone === 'available' ? (
|
||||
<Sparkles className="mt-0.5 size-4 shrink-0 text-primary" />
|
||||
<Codicon className="mt-0.5 size-4 shrink-0 text-primary" name="cloud-download" size="1rem" />
|
||||
) : statusTone === 'error' ? null : (
|
||||
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<div className="ml-3.5 flex flex-col gap-0.5 pl-1.5">
|
||||
<OverlayNavItem
|
||||
active={providerView === 'accounts'}
|
||||
icon={Sparkles}
|
||||
icon={codiconIcon('account')}
|
||||
label={t.settings.nav.providerAccounts}
|
||||
nested
|
||||
onClick={() => openProviderView('accounts')}
|
||||
|
|
@ -186,7 +186,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang
|
|||
<div className="mt-auto flex items-center gap-1 pt-2">
|
||||
<Tip label={t.settings.exportConfig}>
|
||||
<OverlayIconButton onClick={() => void exportConfig()}>
|
||||
<IconDownload className="size-3.5" />
|
||||
<Download className="size-3.5" />
|
||||
</OverlayIconButton>
|
||||
</Tip>
|
||||
<Tip label={t.settings.importConfig}>
|
||||
|
|
@ -196,7 +196,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang
|
|||
importInputRef.current?.click()
|
||||
}}
|
||||
>
|
||||
<IconUpload className="size-3.5" />
|
||||
<Upload className="size-3.5" />
|
||||
</OverlayIconButton>
|
||||
</Tip>
|
||||
<Tip label={t.settings.resetToDefaults}>
|
||||
|
|
@ -207,7 +207,7 @@ export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChang
|
|||
void resetConfig()
|
||||
}}
|
||||
>
|
||||
<IconRefresh className="size-3.5" />
|
||||
<RefreshCw className="size-3.5" />
|
||||
</OverlayIconButton>
|
||||
</Tip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<IconLayoutDashboard />
|
||||
<LayoutDashboard />
|
||||
</Button>
|
||||
</Tip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="size-3" />
|
||||
<Codicon name="hubot" size="0.75rem" />
|
||||
),
|
||||
id: 'agents',
|
||||
label: copy.agents,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue