hermes-bsd/apps/desktop/DESIGN.md
Sam & Claude 4a1073847e
Some checks failed
Typecheck / typecheck (apps/shared) (pull_request) Has been cancelled
Contributor Attribution Check / check-attribution (pull_request) Has been cancelled
History Check / check-common-ancestor (pull_request) Has been cancelled
Nix / nix (macos-latest) (pull_request) Has been cancelled
Nix / nix (ubuntu-latest) (pull_request) Has been cancelled
Typecheck / typecheck (ui-tui) (pull_request) Has been cancelled
Supply Chain Audit / changes (pull_request) Has been cancelled
Typecheck / typecheck (apps/bootstrap-installer) (pull_request) Has been cancelled
Typecheck / typecheck (apps/desktop) (pull_request) Has been cancelled
Typecheck / typecheck (web) (pull_request) Has been cancelled
Supply Chain Audit / Scan PR for critical supply chain risks (pull_request) Has been cancelled
Supply Chain Audit / Check PyPI dependency upper bounds (pull_request) Has been cancelled
docs: convert negative patterns to positive actionable instructions
Rewrite 'do not X' / 'never Y' / 'avoid Z' / 'cannot W' patterns across
documentation files into positive 'do ABC to achieve XYZ' instructions.

Files changed:
- AGENTS.md (180 lines): 30+ patterns converted including caching,
  profiles, known pitfalls, testing, change-detector tests
- CONTRIBUTING.md (50 lines): 14+ patterns including memory providers,
  cross-platform rules, skill authoring, security
- README-FreeBSD.md: operator-user instructions
- apps/desktop/DESIGN.md (49 lines): 12 design constraint patterns
- docs/observability/README.md: 4 observer contract patterns

Hard safety invariants preserved:
- Secrets never in logs → 'Keep secrets out of logs. Redact from log output'
- Tests never write to ~/.hermes → 'Use _isolate_hermes_home fixture'
- Prompt cache never broken → 'Past context stays immutable mid-conversation'
2026-06-21 13:24:20 +02:00

8.2 KiB

Desktop Design System

Conventions for the Electron desktop app (apps/desktop). Read this before adding a component, overlay, or style. The rule of thumb: one source per concern, tokens over literals, flat over boxed. If you reach for a raw color, a one-off shadow, a bespoke button, or a hardcoded px-* on a control — stop, there's already a primitive for it.

Principles

  1. Flat, not boxed. Use flat grouping with whitespace and a single hairline; avoid card-in-card and divider borders inside a panel.
  2. Borderless + shadow for elevation. Overlays float on shadow-nous + a --stroke-nous hairline, not hard borders.
  3. One primitive per concern. One Button, one set of control variants, one SearchField, one Loader, one ErrorState. Migrate onto them; keep each concern using one primitive instead of forking.
  4. Tokens, not literals. Reference CSS vars (--ui-*, --shadow-nous, --theme-*) for all colors and shadows — avoid raw hex and ad-hoc rgba in components.
  5. Style lives in the primitive. Variants and sizes own padding, radius, color, chrome. Call sites pass a variant/size, not className overrides that re-specify those.

Surfaces & elevation

Every overlay / dialog / toast (boot-failure, install, notifications, model-picker, onboarding, prompt-overlays, updates, base Dialog) uses:

shadow-nous           /* downward-weighted, layered contact→ambient falloff */
border-(--stroke-nous) /* currentColor hairline, theme-adaptive */

Both are CSS vars in src/styles.css — tune in one place, everything inherits. Keep overlays uniform: use the shared tokens for all elevation; if a change is needed, update the token rather than adding per-overlay custom shadows or borders.

Stroke & color tokens

Token Use
--ui-stroke-primary…quaternary hairlines, in descending strength
--ui-stroke-tertiary the default in-panel divider / list hairline
--stroke-nous the overlay hairline (pairs with shadow-nous)
--ui-text-primary / -secondary / -tertiary text hierarchy
--ui-bg-quaternary soft control fill (secondary button)
--chrome-action-hover hover fill for quiet controls
--theme-primary, --ui-accent brand/accent

Reference CSS vars (--ui-text-primary, --ui-stroke-tertiary, etc.) for all text and border colors. The white tile in BrandMark is the one sanctioned literal (the mark needs a fixed backdrop).

Buttons — one component

src/components/ui/button.tsx is the single source. Pick a variant + size; do not pass h-*, px-*, py-*, or icon-size overrides.

Variants: default (primary), destructive, secondary (soft fill — the default non-primary look), outline (transparent + 1px inset ring, no fill/shadow), ghost, link, text (boxless quiet inline — "Cancel", "Clear"), textStrong (bold underlined inline affordance — "Change", "Open logs").

Sizes: default, xs, sm, lg, inline (flush, zero box — for buttons that sit inside a heading/sentence; replaces h-auto px-0 py-0), and the icon family icon / icon-xs / icon-sm / icon-lg / icon-titlebar.

Notes:

  • Text buttons are square (no radius) and sized by padding + line-height (no fixed heights). Only icon buttons carry the shared 4px radius.
  • SVGs inherit size-3.5 (size-3 at xs). Let the button component control icon sizing.
  • Polymorph with asChild when the button must render as a link/Slot.

Form controls

  • controlVariants (src/components/ui/control.ts) is the shared shape for Input / Textarea / SelectTrigger. New text-entry controls compose it.
  • SearchField — borderless, underline-on-focus, auto-width. The only search input. Use this single search component instead of building boxed search bars or wrapping it in a bordered tile. Empty lists hide their search field.
  • SegmentedControl — the choice control for small mutually-exclusive sets (color mode, tool-call display, usage period). Replaces radio piles and pill rows.
  • Switch (size="xs") — bare, with aria-label. No bordered text wrapper.

Layout

  • Gutters: PAGE_INSET_X (src/app/layout-constants.ts) for page side padding; PAGE_INSET_NEG_X to bleed a child to the edge. Use these constants instead of hardcoding px-6/px-8 on pages.
  • Master/detail overlays: OverlaySplitLayout + OverlaySidebar / OverlayMain. Cron, profiles, etc. ride this — reuse the existing shell instead of rebuilding a titlebar for each overlay.
  • Rows: ListRow (settings primitives.tsx) for label/description/action rows. Flat, flush-left; no per-row indentation that fights flush headers.
  • No dividers between rows unless the list genuinely needs them; prefer spacing. When you do need one, it's a single --ui-stroke-tertiary hairline.

Feedback & empty/error/loading states

  • Loading: Loader (src/components/ui/loader.tsx) — animated math/ascii curves (lemniscate-bloom for long ops). Use this instead of the literal text "Loading…".
  • Errors: ErrorState + the canonical ErrorIcon (no bg chip). One look for the React boundary, in-dialog errors, and the boot-failure banner. Pass nodes for title/description so Radix DialogTitle/Description can flow through for a11y.
  • Logs: LogView — no bg, hairline border, tight padding, small mono. Every place we surface raw logs uses it.
  • Empty: EmptyState / EmptyPanel — use the shared components for all empty states rather than hand-rolling centered empties.

Iconography & brand

  • Codicon is the icon set. No mixing icon libraries inline.
  • BrandMark (src/components/brand-mark.tsx) is the brand glyph — the nous-girl mark on a white tile, softly rounded, identical in light/dark. It replaced scattered Sparkles glyphs in updates / onboarding / about. Use it for hero/brand moments; keep the brand mark consistent by avoiding decorative star/sparkle icons alongside it.

Motion

  • Quick, functional transitions (~100ms on controls). Respect prefers-reduced-motion for anything beyond a fade.
  • Choreographed exits (e.g. onboarding's "matrix" fade-down) stagger per-element then settle the surface — the outer container's fade is delayed so it doesn't swallow the inner animation. Keep the global fade synchronized with inner animation timing to avoid racing the detail.

i18n

  • Every user-facing string goes through useI18n() (src/i18n/context.tsx). No literals in JSX.
  • Update all locales togetheren, ja, zh, zh-hant. A string change in en.ts that skips the others is a regression (drifted punctuation, stale labels). Keep trailing-punctuation and tone consistent across all four.

State (TypeScript)

Mirrors the repo TS style (see root AGENTS.md):

  • Shared/cross-component state → small nanostores, not prop-drilling. Each feature owns its atoms; shared atoms live in src/store.
  • Rendering components subscribe with useStore; non-render actions read with $atom.get().
  • Colocated action modules over god hooks. A hook owns one narrow job.
  • Keep persistence beside the atom that owns it. Route roots stay thin.
  • Prefer interface for public props; extend React primitives (React.ComponentProps<'button'>, Omit<…>).

Affordances

  • cursor-pointer at the primitive level (Button, dropdown/select) — let the primitive own cursor behavior rather than hardcoding it per call site.
  • Global focus-ring reset; titlebar actions have no active-background state.
  • Esc closes every dismissable overlay/dialog (install/onboarding excluded); close is an x-icon, not the word "Close".

Before you add something — checklist

  • Reuse a primitive (Button, SearchField, SegmentedControl, ListRow, Loader, ErrorState, LogView) instead of forking one?
  • Tokens (--ui-*, shadow-nous, --stroke-nous) — zero raw colors / one-off shadows?
  • No className overriding a primitive's padding / size / radius / chrome?
  • Overlay uses shadow-nous + border-(--stroke-nous), no hard border?
  • Flat — no card-in-card, no gratuitous row dividers?
  • All four locales updated for any new/changed string?
  • cursor-pointer, focus ring, and Esc-to-close behave?