Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lat.md/lat.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This directory defines the high-level concepts, business logic, and architecture
- [[chat-performance]] — how chat rendering stays responsive through contained transcript rows, batched textarea resizing, and fixed-row slash-command virtualization.
- [[model-context]] — the per-model context-window override that drives the context gauge and the agent's auto-compaction.
- [[model-selection]] — the session-scoped in-chat model override that switches the model (and provider) for one conversation without touching the global default.
- [[reasoning-effort]] — the composer's Faster⟷Smarter effort control: a draggable `role="slider"` over six ordered levels that stays open until dismissed and commits one `onChange` per real change.
- [[web-preview]] — the in-app split-screen webview and the `partition`-based gate that lets only it load remote HTTPS while staying sandboxed.
- [[code-blocks]] — collapsible long code blocks, and why expansion state is keyed on source position to survive react-markdown's streaming remounts.
- [[window-chrome]] — the browser-style title bar where open-conversation tabs sit on top of the window drag region, clickable while empty space still drags.
Expand All @@ -23,3 +24,4 @@ This directory defines the high-level concepts, business logic, and architecture
- [[office-3d-traffic]] — the Office tab's backdrop traffic: car-following and junction-yielding simulation, per-model nose orientation, and instanced fleet rendering in a dozen draw calls.
- [[office-3d-interiors]] — enterable office/bank/showroom interiors: per-location conditional mounting (city unmounts while indoors), camera fly-in rig, interactable objects (ATM → wallet, desk → agent, car → spec card), and idle-agent walking trips between buildings.
- [[office-interactions]] — space representatives: interactive bank tellers whose menu runs account status, balances, and account creation against the hermes-one backend for a chosen agent; the extensible pattern for future spaces (showroom sales, building space).
- [[mcp-servers]] — add / edit / remove / enable / test MCP servers from the Capabilities → MCP tab; the shared add+edit modal and the in-place atomic `updateMcpServer` upsert (config.yaml locally, gateway REST in Remote/SSH).
20 changes: 20 additions & 0 deletions lat.md/mcp-servers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# MCP server management

Add, edit, remove, enable, and test Model Context Protocol (MCP) servers from the Capabilities → MCP tab. Each server is an `http` or `stdio` entry stored in the profile's `config.yaml`, or on the remote gateway in Remote/SSH mode.

## Operations

The MCP tab in [[src/renderer/src/screens/Tools/Tools.tsx]] lists servers as a table and drives every mutation through IPC into [[src/main/mcp-servers.ts]]; local mode edits `config.yaml` directly, Remote/SSH mode proxies to the gateway's `/api/mcp/servers` REST endpoints.

The columns are Server · Transport · Command/URL · Enabled.

- **Add** — [[src/main/mcp-servers.ts#addMcpServer]] validates the input and rejects a duplicate name.
- **Edit** — a per-row pencil opens the shared modal pre-filled with the server's values; saving calls [[src/main/mcp-servers.ts#updateMcpServer]], which upserts the entry **in place** (no delete-then-add gap so a mid-write failure can't lose the server) and only removes the old entry when the name changed — guarding first against colliding with another server. The modal has **Visual** and **JSON** modes (a toggle): Visual is the field form; JSON is a raw `Server JSON` editor (`command`/`args`/`env`/`url`/`auth`/`enabled`). The two stay in sync on toggle; on save the active mode is the source of truth (JSON is parsed + validated inline, and an `enabled` change there is applied via `setMcpServerEnabled` after the config write). Name stays a separate field in both modes.
- **Remove** — [[src/main/mcp-servers.ts#removeMcpServer]].
- **Enable/disable** and **Test** are separate IPC calls; the enable toggle is the row's trailing control (with the row dimming when off — the two together stand in for a status column, so there isn't one), and the edit/test/remove actions reveal on row hover. Test surfaces its result (`connected · <tool count>` or an error) as the transient banner above the table, since `listMcpServers` carries no live connection state or tool count.

The add and edit flows share one modal: `editingMcpName` in `Tools.tsx` (null while adding) selects `updateMcpServer` vs `addMcpServer` and swaps the title and save-button copy. Server rows are re-fetched after every successful mutation.

The `McpLogo` component shows a logo, source first: the community registry's icon (matched to a server by name → registry id; [[src/main/registry.ts#toItem]] resolves the entry's repo-relative `icon` to the registry service's icon URL, `https://registry.hermesone.org/registry-icon/<path>`), then the HTTP server's own-domain favicon, then a generic server glyph.

Registry icons are Iconify-style SVGs — many monochrome (`fill="currentColor"`). Rather than fetch + inline them to theme the colour, `McpLogo` follows the registry's own web UI (`EntryIcon`): render a plain `<img>` on a **white tile in both themes**, so black single-colour glyphs stay legible (they'd vanish on a dark tile) and colour logos keep their colours. This needs no main-process fetch — `<img>` loads directly under CSP `img-src https:`; the generic glyph fallback keeps the normal dark tile. Since the icons are immutable content-addressed assets, [[src/main/app/start.ts#startMainProcess]]'s `onHeadersReceived` rewrites their `Cache-Control` to `max-age=31536000, immutable`, so each is fetched at most once and served from the on-disk HTTP cache across restarts.
4 changes: 3 additions & 1 deletion lat.md/model-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ The override is held in renderer state on each `<Chat>` run ([[src/renderer/src/

## Two-pane picker grouped by display brand

The bottom [[src/renderer/src/screens/Chat/ModelPicker.tsx]] dropdown is a two-pane layout: a left **provider rail** filters a right **flat model list**, with a top search box narrowing both.
The bottom [[src/renderer/src/screens/Chat/ModelPicker.tsx]] dropdown is a two-pane layout: a left **provider rail** filters a right **flat model list**, with a top search box (leading magnifier icon) narrowing both.

The panel is styled as a floating native surface — translucent glass (`backdrop-filter`) lifted on a soft ambient shadow rather than a hard border, a recessed search field, and a filled-tint selection instead of an outlined row — so it reads as a desktop popover, not a web form. Depth comes from light (elevation, highlight, inset), not strokes; all radii use `var(--radius-*)` so the squared-corners theme toggle is respected.

The rail has an "All models" entry plus one row per brand (logo + model count); each list row shows the model title, a `Provider · model-id` subtitle, and a check on the active model. The currently-selected model is sorted **first** within whatever filter is shown (exact provider+model+baseUrl match, then same provider+model), leaving the rest of the list in its original order.

Expand Down
17 changes: 17 additions & 0 deletions lat.md/reasoning-effort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Reasoning effort control

The composer's reasoning-effort control sets how hard the model thinks for the current chat: a `Brain` trigger opens a glass popover with a Faster⟷Smarter slider over six ordered levels (auto → minimal → low → medium → high → xhigh).

[[src/renderer/src/screens/Chat/ReasoningEffortPicker.tsx#ReasoningEffortPicker]] renders it. The label reads from each API value's `labelKey` (the `medium` API value shows "Standard", `xhigh` shows "Max"); the hint warns that manual levels may be ignored by models that lack effort support.

## Slider interaction

The panel is a single `role="slider"` track, not a list of options — the six stops are non-focusable visual dots, and the track owns all input.

Pointer drag: pressing anywhere on the track and moving maps the pointer x to the nearest stop and applies it live (the rail spans the first/last dot centres, 9px in from each track edge; `indexFromClientX` does the math). Keyboard: the focused track handles ←/→ plus ↑/↓, Home, and End. Every path funnels through `commit(index)`, which clamps to range, dedupes against the last-applied index (a ref kept in sync with the selected value), and calls `select` — so a drag fires **one** `onChange` per real level change, not one per pixel, and the click that follows a pointer release is a no-op. The `--effort-frac` custom property (selectedIndex / (n−1)) drives the accent rail-fill and knob position in CSS.

## Stays open until dismissed

Choosing a level no longer closes the popover, so the user can nudge the level several times in a row without it vanishing.

`select` only awaits `onChange` and clears or sets the save-error flag; it never toggles `isOpen`. Dismissal is explicit: a `mousedown` outside `pickerRef`, or Escape (which the slider's key handler ignores so it bubbles to the dropdown's `onKeyDown`). A failed save keeps the panel open with an inline error and restores the prior selection.
12 changes: 7 additions & 5 deletions lat.md/sidebar-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ The sidebar starts with New Chat, keeps app destinations pinned, then gives conv

## Collapse toggle brand mark

The sidebar header's collapse control doubles as the brand mark: collapsed it shows a circular dot that swaps to the expand icon on hover; expanded it shows the full wordmark beside the collapse icon.
The sidebar header's collapse control doubles as the brand mark: collapsed it shows a circular dot that swaps to the expand icon on hover; expanded it is just the collapse icon, parked top-right for a clean, logo-free header.

[[src/renderer/src/screens/Layout/Layout.tsx#Layout]] renders `.sidebar-collapse-toggle`. Collapsed, it holds a fixed-size `.sidebar-collapse-swap` box stacking a `.sidebar-collapse-mark` circle (filled with `--text-primary`, so white on dark themes and dark on light) over the `PanelLeftOpen` icon; only opacity toggles on hover/focus, so the button never reflows. Expanded, the maskable `.sidebar-logo` wordmark shows next to the `PanelLeftClose` icon.
[[src/renderer/src/screens/Layout/Layout.tsx#Layout]] renders `.sidebar-collapse-toggle`. Collapsed, it holds a fixed-size `.sidebar-collapse-swap` box stacking a `.sidebar-collapse-mark` circle (filled with `--text-primary`, so white on dark themes and dark on light) over the `PanelLeftOpen` icon; only opacity toggles on hover/focus, so the button never reflows. Expanded, the `.sidebar-brand` row holds only the `PanelLeftClose` collapse toggle (`justify-content: flex-end`); the wordmark was removed so the top reads clean below the traffic lights.

## Infinite sidebar list

Expand Down Expand Up @@ -50,13 +50,15 @@ The modal in [[src/renderer/src/screens/Layout/Layout.tsx#Layout]] renders [[src

## Profile switch and active chat

The footer profile switcher keeps the selected shell profile aligned with the visible chat run, while preserving older conversations under their original profiles.
The footer profile control keeps the selected shell profile aligned with the visible chat run, while preserving older conversations under their original profiles.

[[src/renderer/src/screens/Layout/ProfileSwitcher.tsx#ProfileSwitcher]] persists the selected profile through main-process profile switching, then [[src/renderer/src/screens/Layout/Layout.tsx#Layout]] applies [[src/renderer/src/screens/Layout/chatRuns.ts#selectProfileRunTransition]] before rendering Chat. If the active chat is blank, it is re-homed to the selected profile; if it already belongs to another profile, the shell activates an existing blank run for the selected profile or creates a fresh one. This prevents the footer, Settings, recent sessions, and chat transport from disagreeing about which agent is active.
[[src/renderer/src/screens/Layout/ProfileSwitcher.tsx#ProfileSwitcher]] is two affordances, not a popover: the chip (avatar + name) opens the **current** profile's edit modal via `openProfile` ([[src/renderer/src/components/profile/ProfileModalContext.ts]]), and a dedicated switch button opens a **modal** listing every profile (active one checked) — picking one calls `setActiveProfile` and `onSwitch`, and a "Manage profiles" row jumps to the Agents screen. Collapsed, the lone avatar opens the switch modal.

That switch persists the selected profile through main-process profile switching, then [[src/renderer/src/screens/Layout/Layout.tsx#Layout]] applies [[src/renderer/src/screens/Layout/chatRuns.ts#selectProfileRunTransition]] before rendering Chat. If the active chat is blank, it is re-homed to the selected profile; if it already belongs to another profile, the shell activates an existing blank run for the selected profile or creates a fresh one. This prevents the footer, Settings, recent sessions, and chat transport from disagreeing about which agent is active.

Opening a sidebar session after switching profiles consumes that blank selected-profile run instead of appending beside it. [[src/renderer/src/screens/Layout/chatRuns.ts#openSessionRunTransition]] replaces the active scratch run when it belongs to the same profile as the resumed session, so the tab strip shows the previous session without an extra "New conversation" tab.

The switcher trigger preserves the old app-brand label for an unrenamed default profile: when `listProfiles` returns the fallback `name === id === "default"`, the button shows `common.appName`; once a custom name is stored, it shows that user-facing name.
The profile chip preserves the old app-brand label for an unrenamed default profile: when `listProfiles` returns the fallback `name === id === "default"`, the chip shows `common.appName`; once a custom name is stored, it shows that user-facing name.

The same per-profile appearance also drives the agent avatar inside the transcript. [[src/renderer/src/screens/Layout/Layout.tsx#Layout]] passes `getAppearance(run.profile)` to each [[src/renderer/src/screens/Chat/Chat.tsx]] as `agentAppearance`, which forwards `{ name, color, avatar }` through [[src/renderer/src/screens/Chat/MessageList.tsx]] to every [[src/renderer/src/screens/Chat/MessageRow.tsx#HermesAvatar]] (and the reasoning/tool-activity rows in [[src/renderer/src/screens/Chat/HistoryRow.tsx]]). `HermesAvatar` plays the looping `loadingo.gif` only while a turn is generating (`active`); once generation stops it runs out the current gif loop, then swaps to the agent's [[src/renderer/src/components/common/ProfileAvatar.tsx]] so idle turns are identified by who produced them. The live typing indicator has no resolved agent yet, so it falls back to the gif.

Expand Down
18 changes: 16 additions & 2 deletions lat.md/window-chrome.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@ The top strip of the main window is a browser-style title bar: it is the window'

On macOS the window is frameless (`titleBarStyle: "hiddenInset"`, traffic lights inset at x/y 16 — see [[src/main/app/start.ts#startMainProcess]]), and [[src/renderer/src/App.tsx]] renders a fixed full-width `.drag-region` (`-webkit-app-region: drag`, z-index 1000) so the whole top band — including over the sidebar/traffic-light area — drags the window. This strip is mac-only; other platforms keep the OS title bar.

`.app` fills the window (`height: 100vh`) so the chrome reaches every edge. The sidebar is a flush full-height panel: it only rounds its left corners (`border-radius: 16px 0 0 16px`) to follow the window's rounded corners, while its right edge is square against the content column.
`.app` fills the window (`height: 100vh`) so the chrome reaches every edge. The sidebar rounds only its **top-left** corner (`border-radius: 16px 0 0 0`); the full-width status strip owns the window's bottom edge and rounds both bottom corners (`0 0 16px 16px`), so the sidebar's bottom-left is square against it. A hairline seam (`.content` `border-inline-start`) separates the content pane from the sidebar.

## Translucent sidebar (macOS vibrancy)

The sidebar is frosted glass on macOS — the window material shows through it — while the content pane stays opaque and readable.

[[src/main/app/start.ts#createWindow]] gives the window `vibrancy: "under-window"` + `visualEffectState: "active"` + a transparent `backgroundColor` on macOS. The material's light/dark **tone follows the app theme**, not the system appearance: [[src/renderer/src/components/ThemeProvider.tsx#ThemeProvider]] pushes the resolved theme's `appearance` to the main process via the `set-native-appearance` IPC ([[src/main/ipc/register.ts#registerIpcHandlers]] → `nativeTheme.themeSource`), passing `"system"` through only for the "System" theme so its `prefers-color-scheme` still tracks the OS. This is the fix for the earlier milky-sidebar bug: `under-window` alone follows the *system* appearance, so a dark theme on a light-mode Mac frosted light; syncing `themeSource` keeps a dark theme's frost dark. `createWindow` seeds `themeSource = "dark"` (the default theme) so the first paint isn't milky before the renderer refines it.

For the material to paint, the renderer leaves surfaces transparent: [[src/renderer/src/App.tsx]] adds `shell-vibrant` to `.app` only on macOS and only on the `main` screen (onboarding stays solid). Under it, `body`/`#root`/`.app` go transparent, the `.sidebar` and `.status-bar` become a translucent `--bg-secondary` tint, and `.content` keeps an opaque `--bg-primary`. Because a transparent window is no longer masked to its rounded shape by macOS, `.content` also rounds its own top-right corner (`0 16px 0 0`) — the sidebar owns top-left, the status bar owns the bottom two — so the opaque panes don't show square corners.

## Bottom status strip

A native system strip pinned full-width beneath the sidebar+content row surfaces live state that was otherwise hidden: gateway/connection, active model, and skill count, plus real keyboard hints.

[[src/renderer/src/screens/Layout/StatusBar.tsx#StatusBar]] self-fetches from `listProfiles` (active profile's `model`, `skillCount`, `gatewayRunning`) and `getConnectionConfig` (`mode`), polling every 4s. Every field is real — an unknown value drops its chip rather than showing a placeholder, and the hints advertise only shortcuts that exist (`/` commands, `⌘,`/`Ctrl,` settings), never a fabricated `⌘K`. [[src/renderer/src/screens/Layout/Layout.tsx]] wraps its `.layout` row in a `.layout-shell` column and renders the strip as the row's sibling; the online/offline dot uses the theme-aware `--success` token.

## Tabs layered above the drag region

[[src/renderer/src/screens/Layout/ActiveSessionsBar.tsx#ActiveSessionsBar]] is the content column's title bar. It owns the top band browser-style: empty space drags, the chips stay clickable.

- The bar itself is `-webkit-app-region: drag` with `position: relative; z-index: 1001`, so it stacks above the global `.drag-region` (z 1000) and is the drag handle for the content column.
- Each `.active-session-chip` opts back out with `-webkit-app-region: no-drag`, keeping select/close clickable above the drag layer — the same priority model browsers use for tabs over a draggable tab strip.
- `min-height: 34px` ( the 28px global drag strip) means content rendered after the bar clears the fixed drag layer, so the old `.is-mac .content { padding-top: 28px }` offset is no longer needed.
- `min-height: 34px` (= the 34px global drag strip) means content rendered after the bar clears the fixed drag layer, so the old `.is-mac .content { padding-top: 28px }` offset is no longer needed.

Visually the strip is a Safari-style tab bar: the strip uses the darker `--bg-secondary` toolbar shade; tabs are flat (no border/fill) and separated by thin vertical dividers drawn with an `::before` on each non-first chip. The active tab fills with `--bg-primary` — the same colour as the transparent content area below it — and rounds its top corners, so it docks into the page; the dividers flanking the active tab are hidden for a seamless join.

Expand Down
Loading
Loading