Commit graph

16755 commits

Author SHA1 Message Date
Brooklyn Nicholson
50170fdd2d fix(themes): reconcile element/syntax tokens with main's derive+adapt pipeline
Element tokens (ui_tool/ui_thinking), skinnable diffs, and code-syntax keys
flow through buildPalette → adaptColorsToBackground instead of a hand-mapped
color block, so they inherit #20379's contrast/polarity machinery. thinking
and syntaxComment track the EFFECTIVE muted (banner_dim override included);
the skin's `background` feeds the surface (it also paints the terminal via
OSC 11); statusFg falls back through ui_text/banner_text. Tests assert the
routing/independence contracts rather than pre-adaptation hexes.
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
428a0534ee test(themes): E2E live skin switch — config write → skin.changed broadcast 2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
3ba6eebc7c feat(themes): dedicated code-syntax palette keys
Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't
be themed independently. Add syntax_string/number/keyword/comment skin keys →
syntax* theme tokens (defaulting to those brand tokens, so defaults are
unchanged) and point the highlighter at them. Documented in the element→key map.
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
4f4f938aef feat(themes): hermes skin set — deterministic one-color tweak, bg untouched
Changing a single color kept wrecking the rest because the agent hand-authored a
new skin (often from `default`, which has no `background`, resetting the terminal
to black). Add `hermes skin set <key> <hex>`: edits the ACTIVE skin's one key in
place (a built-in is forked into an editable copy carrying its full palette), so
everything else — background included — is preserved. Plus `skin use` / `skin
list`. The skill now points tweaks at this command instead of hand-authoring.
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
2a4e5fac1a fix(themes): tweak the ACTIVE skin in place, never fork default
Changing one color ("make the tool ● cyan") forked `default` — which has no
`background` — so applying it reset the terminal to its own (black) default and
dropped the active skin's palette. Teach the skill to edit the active skin's file
in place for a tweak (watcher repaints on the mtime bump), and to fork a built-in
only by carrying its full palette. Hard pitfall: never fork `default` for a tweak.
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
30ee6f749d feat(themes): element tokens (ui_tool, ui_thinking) + skinnable diffs
Theming was semantic-only: the gold tool `●` was `accent`, shared with
headings/links/chevrons, so "recolor tool calls" was impossible and the agent
had no key to point at. Add `ui_tool` (● + tool spinner) and `ui_thinking`
(reasoning body) tokens that fall back to accent/muted — defaults unchanged,
but now independently settable. Make diffs skinnable too (`diff_*`), which
fromSkin previously hardcoded. Document the full element→key map in the skill so
Hermes knows which knob turns what.
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
727f6704a7 feat(themes): TUI paints its own background from the skin (OSC 11)
The TUI inherited the terminal's background; now a skin's `background` paints the
whole surface via OSC 11 when a skin is applied, and clears back to the terminal
default (OSC 111) on revert and on exit (ridden in through resetTerminalModes).
Opt-in: a skin with no `background` leaves the terminal untouched, and the
restore only fires if we actually painted. Desktop already themed its own bg;
this closes the loop so Hermes owns its background on every surface.
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
eb454919b2 feat(themes): agent-authored skins switch live via a gateway skin watcher
A skin Hermes activates (`hermes config set display.skin X`) or recolors in
place now goes live on every surface (CLI, TUI, desktop) within ~half a
second, on its own — no `/skin`, no tool-hook timing, no user action.

A gateway daemon polls the resolved skin signature `(name, active-file mtime)`
every 0.5s and broadcasts `skin.changed` on any real move — a name switch OR a
live color edit to the active skin. It routes through the SAME path `/skin`
uses, so all surfaces repaint identically. The watcher seeds its baseline at
gateway.ready (stdio + ws) so it only fires on a real change; the `/skin` RPC
seeds the baseline too so it never double-broadcasts.

Subsumes the desktop's post-turn `config.get skin` poll (its skin.changed
handler already applies).
2026-07-21 21:00:43 -05:00
Brooklyn Nicholson
91c5c0c1a6 fix(themes): activate skins via hermes config set, never a config.yaml hand-edit
The skill told the agent to `patch` display.skin into config.yaml; a stray indent
corrupts the file and breaks the live gateway (the reported "/ menu broke"), and
a raw file edit never live-applies in a running CLI/TUI ("nothing happened").
Route activation through the safe writer (`hermes config set display.skin`), and
state plainly that a tool call can't hot-switch a running CLI/TUI — the user runs
`/skin <name>` (desktop still auto-repaints on the next turn).
2026-07-21 21:00:42 -05:00
Brooklyn Nicholson
a8444fbcae feat(themes): cross-surface theme SDK — one skin themes CLI, TUI, and desktop
Make the Python skin engine the single source of truth for a canonical theme
shape consumed by every surface, so a skin authored in $HERMES_HOME/skins/*.yaml
(by a user or by Hermes from a prompt) themes the CLI, TUI, and desktop GUI at
once — the theme analogue of the plugin SDK.

- @hermes/shared: canonical `HermesSkin` token shape + `SKIN_COLOR_TOKENS` enum,
  consumed by both TS surfaces (TUI `GatewaySkin` and desktop dedup onto it).
- Desktop: `skinToDesktopTheme` resolver (skin → CSS-var palette, VS Code-style
  derive-from-seed) + `backend-sync` that registers backend skins into the theme
  registry (Appearance/Cmd-K/`/skin`) and applies on a real change. Seeds on
  gateway.ready (never stomps a persisted pick), applies on skin.changed and the
  post-turn `config.get skin` poll (catch-all for agent-edited config.yaml).
- TUI: `fromSkin` now maps the status bar + `background` keys it was dropping.
- Gateway: `config.get skin` also returns the full resolved palette (additive).
- Skill: `hermes-themes` teaches the agent to author + activate a skin.

Each surface keeps its own normalizing resolver (ansi for the TUI, CSS vars for
the desktop, prompt_toolkit/Rich for the CLI).
2026-07-21 21:00:42 -05:00
brooklyn!
32a9f2acbc
Merge pull request #68999 from NousResearch/bb/widget-grid-hardening
fix(ui-tui): widget-grid hardening — review fast-follow for #20379
2026-07-21 20:46:04 -05:00
Brooklyn Nicholson
5c714be5fe ci: retrigger (transient setup-uv manifest fetch flake) 2026-07-21 20:40:29 -05:00
brooklyn!
3de91c6974
fix(desktop): stop long-session transcript from drifting to old turns (#69019)
content-visibility:auto on turn groups (perf: off-screen turns skip
style/layout/paint) pairs with contain-intrinsic-size:auto, which only
remembers a turn's size after it renders. A turn that finished streaming
near the bottom had its smaller mid-stream size remembered; once it
scrolled off the top edge and got skipped, it collapsed to that stale
height. With overflow-anchor:none the viewport can't self-correct, so the
stick-to-bottom lock drifts and the view creeps up over older turns — the
'long session eventually shows old responses' visual glitch.

Exempt the newest turns (live tail) from virtualization so a turn is only
ever skipped after its layout has settled at its final size (remembered ==
real -> skipping changes no height). Off-screen older turns still skip, so
the dialog/popover whole-document recalc win on long transcripts is kept
(it scales with the hundreds of old turns, not the small tail).
2026-07-21 20:30:42 -05:00
Brooklyn Nicholson
502939e19e chore: gitignore node_modules symlinks, not just directories
Worktrees symlink node_modules to the main checkout; the dir-only
node_modules/ pattern doesn't match symlinks, so one slipped into a
commit and broke npm ci on CI (ENOTDIR). Dropping the trailing slash
matches both.
2026-07-21 20:29:48 -05:00
Brooklyn Nicholson
7d757cf7e2 fmt(js): prettier pass on the new review tests 2026-07-21 20:29:48 -05:00
Brooklyn Nicholson
cd24efec61 perf(ui-tui): shimmer loaders share one clock and stop after a bounded period
Review on #20379, finding 5 (Perf). Every ShimmerRows mounted its own 90 ms
setInterval — the session panel can show lazy skills AND lazy tools at
once, and a lazy watch session stays lazy indefinitely, so an otherwise-
idle TUI ran ~22 React state updates per second forever.

All shimmer compositions now subscribe to a single module-level clock: one
interval regardless of how many skeletons are on screen, updates delivered
in one timer callback so React batches them into a single render pass, and
the interval is torn down with the last subscriber. Each mount's animation
is also bounded (SHIMMER_ANIMATE_MS, 30 s): after the budget the skeleton
freezes in place — it still reads as "loading" — and stops costing renders
entirely.

Tests: fake-timer coverage that N subscribers share one timer in lockstep,
the interval stops with the last unsubscribe, and a late subscriber
restarts the clock cleanly.
2026-07-21 20:29:48 -05:00
Brooklyn Nicholson
9fb2a63db8 fix(ui-tui): make npm run visual portable off POSIX — zero new deps
Review on #20379, finding 4 (Medium). Three portability defects in the
visual verification harness:

- `FORCE_COLOR=3 COLORTERM=truecolor tsx ...` POSIX env assignment does not
  work under the Windows npm command shell. The script is now a plain Node
  launcher (scripts/visual/run.mjs) that sets the env itself and spawns tsx
  via require.resolve('tsx/cli') — no cross-env, no shell syntax.
- Hardcoded /tmp/tui-visual.{html,png} resolve to a drive-root path like
  C:\tmp on native Windows (and fail when that directory doesn't exist).
  Both scripts now derive the output directory from a shared paths.mjs
  helper: os.tmpdir()/hermes-tui-visual (created recursively;
  HERMES_TUI_VISUAL_DIR overrides for CI or side-by-side runs).
- electron was undeclared by ui-tui and only worked via hoisting luck. The
  launcher now resolves it EXPLICITLY from the install tree the desktop
  workspace already provides (require('electron') in plain Node returns the
  binary path), with an ELECTRON_BIN override and a clear error pointing at
  the repo-root install when it's absent — instead of declaring a second
  ~100MB dependency on a TUI workspace for a dev-only harness.

Also fixes the trailing-whitespace line in render.tsx that `git diff
--check` flags. Verified end-to-end: render writes the HTML scene sheet
and the electron shot step produces the screenshot from the tmpdir path;
the missing-electron error path prints the guidance message.
2026-07-21 20:29:48 -05:00
Brooklyn Nicholson
c25e08aa2b fix(ui-tui): stacked dialog gets input before the grid under it
Review on #20379, finding 3 (Medium). /grid-test's `d` opens a dialog on
top without clearing the grid, but the grid's input branch ran FIRST — so
Esc/q/Enter mutated the hidden grid (close/unzoom/promote) instead of
closing the visible dialog, contradicting its "Esc/q/Enter close" hint.

Input routing now follows visual stacking: the dialog/grid dispatch is
extracted into handleStackedModalInput() with the dialog branch first, the
hook consumes through it, and tests drive the real dispatch against the
overlay store — each advertised close key closes only the dialog (grid
byte-identical), grid keys don't leak through while the dialog is up, and
the same keys route to the grid again after it closes.
2026-07-21 20:27:15 -05:00
Brooklyn Nicholson
e6cc5612e6 fix(ui-tui): boot theme cache gets provenance — a stale hint can't pin the terminal
Review on #20379, finding 2 (High). The boot cache seeded the previous
session's background into HERMES_TUI_BACKGROUND, the same slot a CURRENT
OSC-11 answer occupies — so a cache written on a light terminal pinned a
now-pure-black terminal to light forever: the new OSC-11 #000000 answer is
distrusted by design, the pure-white OSC-10 foreground is distrusted too,
and the macOS appearance fallback refuses to run while the slot is set.

Seeding now records provenance (themeBoot.seedBootEnvironment, extracted
and testable against a passed env). When the current terminal answers the
background probe with the untrusted fingerprint, the gateway handler calls
invalidateBootBackground(): the slot clears ONLY while it still holds the
seeded value (a trusted answer that overwrote it is authoritative), OSC-10
gets first claim in the same startup batch, and a short settle pass re-
derives from the live fallback chain if nothing answered.

The cache is also pin-coherent now: commitTheme persists the config mode
pin (display.tui_theme) alongside the resolved theme + physical background.
Previously "/theme light" on a dark terminal cached a light theme next to a
dark background — the next launch painted light, flipped dark when the skin
resolved against the seeded background, then flipped light again on config
hydration: the exact multi-stage flash the cache exists to eliminate. The
seeded pin counts as config-owned (bootSeededPin), so a later 'auto' can
still clear it instead of mistaking it for a user shell export.

Tests cover the review's sequences: stale-light cache vs current dark
terminal (invalidate → fallback chain), stale-dark vs ambiguous light,
pinned light on a dark physical background across restart (and the
inverse), trusted-overwrite protection, and the explicit-signal guards.
2026-07-21 20:27:15 -05:00
Brooklyn Nicholson
b11b5ece2e fix(tui): revision-aware reload.mcp — an ack now means the revision was LOADED
Review on #20379, finding 1 (High). Two ways an MCP config revision could
be silently acknowledged without ever being applied:

Client: the poll advanced its accepted mcp_rev BEFORE calling reload.mcp,
and quietRpc collapses failures to null — a reload that failed against a
temporarily broken server left the revision recorded as applied, and no
subsequent poll retried until an unrelated MCP edit. The handshake is now
syncMcpReload(): send the observed rev with the request, advance `accepted`
only when the server answers status=reloaded (to the server's loaded_rev,
falling back to the requested rev on older gateways), and re-compare on
EVERY poll tick — decoupled from mtime — so a transient failure heals on
the next tick. An in-flight guard stops the 5s poll from stacking requests
behind a slow reload.

Server: generation-only coalescing let a follower triggered by revision B
ack against revision A's registry when the config changed under a slow
leader. The leader now re-hashes the MCP-relevant config after discovery
and repeats until stable (bounded), records _mcp_reload_loaded_rev, and a
follower coalesces only when the revision it was asked to load matches —
otherwise it re-runs the full reload itself. Responses carry loaded_rev.

Deterministic tests for the exact failure sequences: failed reload → no
ack, no generation advance; A-then-B overlap → follower re-runs; matching
rev → coalesces; failed leader → follower re-runs; legacy no-rev callers
keep generation-only coalescing (thread ordering via an instrumented lock,
no sleeps). Client: 6 vitest cases on the ack/retry/in-flight contract.
2026-07-21 20:27:15 -05:00
brooklyn!
967e078ae4
fix(windows): share one bounded, tree-killing git probe across both call sites (#68997)
subprocess.run(["git", ...], timeout=...) deadlocks on Windows: run()'s
post-timeout cleanup calls an unbounded communicate() after killing git.
Killing the PATH-resolved launcher can leave a suspended descendant git.exe
holding duplicates of the captured stdout/stderr handles, so the pipes never
reach EOF and the reader-thread join blocks forever — leaking a process +
two reader threads per fired timeout (the accumulating git.exe load behind
Windows Defender CPU spikes).

Two fail-open probe call sites had this identical flaw:
  - tui_gateway/git_probe.py::run_git — on the Desktop agent-build path
    (_start_agent_build -> _session_info -> branch() -> run_git), where the
    hang turned an optional branch label into "agent initialization timed
    out" (#68609).
  - agent/coding_context.py::_git — hangs the agent turn inside
    build_coding_workspace_block under an ACP host (#66037).

Consolidate both onto one shared bounded_git_probe() in
hermes_cli/_subprocess_compat.py (both files already import from there, so
no new import surface):
  - explicit communicate(timeout), then on ANY failure a tree-kill —
    proc.kill() AND, on Windows, best-effort taskkill /T /F so the suspended
    descendant that holds the pipe writers dies too — plus a bounded 1s
    post-kill drain; if the pipes are still held they're abandoned (the
    orphaned reader threads are daemonic and cost nothing).
  - fail open to "" on every path: spawn error, timeout, kill() raising
    (access denied / already reaped — a raise inside the except handler
    previously escaped the contract), and non-timeout communicate() failures
    now also terminate the child instead of leaving it running.
  - the taskkill spawn can't re-enter the deadlock class: it captures no
    pipes (DEVNULL), so its own timeout cleanup has no reader threads to join.

Normal-path spawn contract is preserved byte-for-byte: PIPE/PIPE/DEVNULL,
text + utf-8 errors="replace", hidden-window creationflags on Windows only,
nonzero returncode -> "". Each call site keeps its own timeout (1.5s / 2.5s).

Supersedes #68622 (Sora-bluesky — git_probe fix + tree-kill) and #66038
(iamwongeeeee — coding_context fix), folding both into one shared helper so
the two sites can't drift and every timeout tree-kills the descendant. Tests
consolidated onto the helper, incl. the previously-missing assertion that a
Windows timeout escalates to taskkill /T /F.

Co-authored-by: Sora-bluesky <sora.bluesky.dev@gmail.com>
Co-authored-by: iamwongeeeee <wykim777@naver.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 20:18:43 -05:00
brooklyn!
75be8fb463
test(mcp): stamp breaker-open time on the monotonic clock, not a literal (#69003)
test_session_expired_retry_waits_for_new_session hardcoded
_server_breaker_opened_at["hindsight"] = 123.0 to simulate a circuit breaker
whose cooldown has already elapsed. But the breaker in tools/mcp_tool.py
compares that stamp against time.monotonic() (age = monotonic() - opened_at,
elapsed when age >= _CIRCUIT_BREAKER_COOLDOWN_SEC). time.monotonic()'s origin
is arbitrary and small on a freshly-booted CI container, so age worked out to
only a few seconds there (< the 60s cooldown) — the breaker stayed open, the
half-open probe never fired, and the retry returned the "unreachable" error
instead of "bank ok". It passed on long-uptime dev boxes (large monotonic)
and failed under CI, with the reported "Auto-retry available in ~Ns" drifting
run to run as the container's monotonic clock varied.

Stamp opened_at relative to the same clock the code reads
(time.monotonic() - _CIRCUIT_BREAKER_COOLDOWN_SEC - 1.0) so the cooldown is
provably elapsed regardless of the monotonic origin, exercising the intended
half-open transition deterministically.
2026-07-21 19:54:40 -05:00
hermes-seaeye[bot]
8208fc5270
fmt(js): npm run fix on merge (#68977)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-21 23:05:16 +00:00
hermes-seaeye[bot]
96ba7ec529
fmt(js): npm run fix on merge (#68976)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-21 22:59:05 +00:00
brooklyn!
e0e15436ee
Merge pull request #20379 from NousResearch/bb/widget-grid-slots
feat(ui-tui): widget-grid layout engine + background-aware theme engine
2026-07-21 17:51:58 -05:00
hermes-seaeye[bot]
146f4ed07d
fmt(js): npm run fix on merge (#68938)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-21 21:41:10 +00:00
Brooklyn Nicholson
0ada7837e4 feat(ui-tui): shimmer skeleton for the lazy tools section
The lazy-loaded Available Tools section rendered a BLANK gap that popped
when data landed. It now shows animated shimmer rows shaped like the real
content (label block + value run, diagonal band sweep), and the summary
line prints "… tools · … skills" instead of "0 tools · 0 skills" while
counts load.

components/loaders.tsx ships the primitives (shimmerSegments band math,
Shimmer, useShimmerPhase, ShimmerRows) — colors are caller-owned theme
tones, one interval per composition. Cherry-picked from the SDK-shape
branch so the skeleton lands with this PR; the widget-SDK consumers stay
in the follow-up.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
c543c691fc fix(ui-tui): record config theme pin even when env already matches (Bugbot r6)
Regression from the r3 shell-pin fix: applyConfiguredTuiTheme('light'|'dark')
returned early when HERMES_TUI_THEME already matched, leaving
configPinnedTheme false — so a later '/theme auto' refused to clear the pin
and the session stayed forced to the old mode while config said auto. Set
configPinnedTheme before the match short-circuit.

Bugbot's other three r6 findings (GatewaySkin paired-palette fields,
ConfigMtimeResponse.mcp_rev, picker maxWidth props) are diff-truncation
false positives — all declared in gatewayTypes.ts / the picker prop
interfaces; typecheck is green.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
9be9a91d13 fix(ui-tui): seed mcp_rev baseline at startup so first cosmetic write doesn't reload MCP (Bugbot r5)
The startup config.get seeded mtimeRef but not mcpRevRef; after a normal
boot mtime is already non-zero so the poller's baseline branch never runs,
leaving mcpRevRef empty. The first /skin (or other cosmetic) write bumped
mtime with an unchanged mcp_rev, and empty !== hash tripped a full
reload.mcp — exactly the reconnect this optimization removes. Seed the
revision alongside mtime at boot.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
2fb0dc6e29 fix: restore package-lock.json @esbuild platform entries (Bugbot r4)
The branch had stripped every node_modules/@esbuild/* optional-platform
entry from the lockfile (442 deletions, 0 additions) — collateral from an
earlier local @esbuild/darwin-arm64 reinstall that rewrote the lockfile to
this machine's platform only. esbuild still declares them as
optionalDependencies, so `npm ci` on Linux CI would skip the platform
binary and break Vitest / ui-tui builds. No dependency was added on this
branch (the only package.json delta is a dev `visual` script), so the
lockfile is restored to match main exactly.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
4bb9e6cfb0 fix(ui-tui): first-swap repaint + config-auto respects shell theme pin (Bugbot r3)
- commitTheme compares the first commit against the SEED theme uiStore
  mounted with (boot cache/default), not null — a cold start that resolves
  to a skin differing from the default previously skipped the anti-tearing
  forceRedraw on that first swap.
- applyConfiguredTuiTheme('auto') now clears only a pin CONFIG set (tracked
  via configPinnedTheme), never a HERMES_TUI_THEME the user exported in their
  shell — that env override outranks auto-detection per detectLightMode's
  documented priority, and a config hydrate was wiping it.

(Bugbot's recurring "GatewaySkin missing paired palette fields" is a
diff-truncation false positive — gatewayTypes.ts declares light_colors/
dark_colors, typecheck green.) 81 handler + build/lint green.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
6982c61b8c fix(tui): mcp_rev includes mcp_servers; reload survives leader failure; /theme persists first (Bugbot r2)
- mcp_rev hash now covers `mcp_servers` (the server DEFINITIONS the classic
  CLI watches) alongside `mcp`/`tools` — editing a server previously bumped
  mtime but not mcp_rev, so the TUI skipped reload.mcp and new servers never
  connected until a manual /reload-mcp.
- reload.mcp coalescing survives a failed leader: a completed-generation
  counter (bumped only after a successful shutdown+discover) gates the
  follower. If the leader threw (flapping server) the follower re-runs the
  full reload itself instead of returning a bogus success over an empty
  registry.
- /theme applies AFTER config.set confirms (mirrors /indicator) with a
  guardedErr catch — a failed persist no longer leaves the session showing a
  theme that reverts on restart.

384 tui_gateway + 1246 TS tests, lint, build green.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
fdac23b641 fix(tui): reload.mcp lock scope + coalesced refresh + macOS polarity flip (Bugbot)
Three real findings from the review of the reload.mcp pooling + OSC-10 work:

- Lock released too early: the leader now holds _mcp_reload_lock across
  shutdown+discover AND its own agent refresh — releasing after discover let
  a second reload tear the registry down while the first was still reading it
  to rebuild the session's tool snapshot.
- Coalesced reload skipped the agent refresh: a follower returned
  "reloaded" without rebuilding ITS OWN session's snapshot, so a coalesced
  session kept stale tools. Followers now wait, then refresh their agent
  against the freshly-built registry (under the lock, skipping the redundant
  shutdown/discover). Shared _finish_reload tail for the `always` opt-out.
- macOS AppleInterfaceStyle fallback never set `resolved`, so a late OSC-10
  foreground reply could re-flip the committed inference (visible churn). It
  now marks resolved; a real OSC-11 background measurement still corrects it
  (that listener intentionally doesn't gate on resolved — measurement beats
  inference).

Bugbot's other four findings were diff-truncation false positives (color.ts,
themeBoot.ts, and the mcp_rev/light_colors/dark_colors types all exist;
typecheck + build green). 384 tui_gateway + TS suites pass.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
11f2e54f0c fix(ui-tui): overlay width caps are absolute — clampOverlayWidth (Copilot review)
Every picker/hub forced width >= 24 AFTER applying the caller's maxWidth,
so a grid cell narrower than 24 (or a FloatBox cell under 28 with its 4
cols of chrome) overflowed and clipped at the terminal edge. One shared
clampOverlayWidth(preferred, maxWidth, min=24): the caller's cap is
ABSOLUTE (a cell knows its budget), the usability floor applies only when
the cap allows it, uncapped keeps the old floor semantics. Five call
sites (model/pet pickers, skills/plugins hubs, session switcher) route
through it; the grid-test FloatBox drops its own 24 floor. Contract-tested
including the sub-floor cap case from the review.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
5bfffcd445 refactor(ui-tui): every selectable list rides the shared selection chip — zero inverse left
/agents, /journey, model picker, skills hub, plugins hub, pet picker, and
the approval/confirm prompts all used SGR inverse for the active row —
terminal-interpreted against unknowable defaults (black slab on transparent
profiles) and visually divergent from completions/session-switcher. New
spreadable chipRowProps(t, active) in overlayPrimitives (chip bg + lifted
ink + bold; spread after `color` so chip ink wins) converts each site to
the one selection treatment. rg confirms zero inverse={} remaining in
components/.
2026-07-21 16:36:58 -05:00
Brooklyn Nicholson
ce8c2c97aa fix(ui-tui): completions popover — aligned name track + neutral descriptions
Two-column grid: the name track auto-sizes to the widest visible command so
descriptions align in their own column instead of running under the names.
Descriptions render in the neutral statusFg gray — label and muted are
near-twins on the gold skins, which made command and description read as
one unparseable run.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
a0a9aaf8b8 refactor(ui-tui): DRY the chrome primitives — shared scrollbarColors + hintRgb
scrollbarColors(t, hover, grabbed) in overlayPrimitives is now THE scheme
for both scrollbars (was duplicated formulas); textInput's hex parsing
collapses into one hintRgb helper feeding colorizeHint and hintCursorCell.
Comment bloat trimmed. No behavior change — 1243 tests byte-green.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
505b2de484 style(skins): default light overlay = goldenrod ladder, not neon or mustard
On white, the vivid #FFD700/#FFBF00 read as glare and the WCAG-darkened
mustard reads as mud. The sweet spot is the statusbar's goldenrod family
(hue kept, saturation tamed, mid luminance): title #C8961E, headers
#D89B04, labels #A97E10, muted #B8860B unchanged, warm bronze ink body
#5C4718, deepened semantics + shell blue. Hierarchy on white: ink 8.9:1 >
fade 5.2 > label 3.7 > muted 3.3 > title 2.7 > headers 2.4. Dark mode
renders the vivid block untouched (explicitly approved as-is).
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
e594e11baa fix(ui-tui): session-panel fade anchors on muted, not the surface — readable on every pole
mix(text, surface) was invisible whenever text was already pale (the
light-rendered default: cream blended toward white = nothing) and inherited
wrong-polarity detection through the surface. mix(muted, text, .5) is
pole-proof: muted is mid-luminance by construction, so the midpoint stays
readable everywhere. Audited 9 skins x both poles: 2.0-2.9:1 on white,
5.6-9.3:1 on dark (worst case 1.92 for a light-authored skin on dark).
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
4426d57a84 feat(ui-tui): OSC-10 foreground polarity tiebreaker for transparent terminals
Transparent profiles make OSC-11 useless (xterm reports the unset default,
pure black, regardless of the composited surface) so polarity detection
lagged editor theme flips. OSC-10 reports the theme's REAL foreground on
those hosts — its luminance reveals the pole. hermes-ink grows a foreground
slot (shared reportedColorSlot factory), App.tsx queries both in the
startup batch (background first so a trusted answer wins without churn),
and the app commits an inferred pole only when the background was
distrusted AND the foreground is decisive (bright=dark theme, dark=light;
mid-grays and #000/#fff defaults commit nothing). User pins still outrank.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
3c135abea5 fix(ui-tui): session-panel hierarchy + polarity-proof placeholder tone
Tool/skill rows rendered labels in muted and member lists in text — which
inverts per skin (muted is the strong family tone on gold skins but the
weak gray on blue ones; slate/poseidon read backwards). Labels now lead in
the theme's label tone, values recede via an explicit fade toward the
surface; audited across all 9 built-ins x both poles. The composer
placeholder lands on muted — the "(and N more toolsets…)" tone — a
mid-luminance family color that reads receded on both poles even when
polarity detection is wrong.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
6929f5f71f fix(ui-tui): eradicate every transparent-terminal black-slab trigger
On terminal.background #00000000 xterm paints "drawn blank" and
attribute-styled cells against an opaque black RGB the user never sees
elsewhere. Verified by PTY byte capture + stateful SGR replay that we emit
no black backgrounds — then removed every trigger: the banner's opaque
space-fills, the scrollbar's non-scrollable space column and SGR-dim
track, the placeholder's SGR inverse cursor and dim fallback, and the bold
full-width banner rule. Chrome styling is now explicit truecolor only:
scrollbar thumb rides primary (accent on hover/drag) over a blended track,
the placeholder cursor is a theme-colored chip (48;2 bg + luminance-picked
ink), hints always carry an explicit 38;2 foreground.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
28e05a3c85 fix(ui-tui): light mode renders the vivid palette RAW, not WCAG-darkened
Pixel-sampled against the reference screenshots: the beloved classic
light-mode look is the vivid authored golds rendered essentially raw
(#FFD700 shows as bright #F5C242) — transparent terminal profiles apply no
contrast lift of their own, and pre-darkening every foreground to WCAG 4.5
produced the reported mustard mud. Light-mode display floors become
near-invisible rescues only (1.18 display / 1.6 semantic; dark keeps
1.45/2.2); the default skin ships a fills-only light_colors OVERLAY
(polarity-flip the navy menu/status fills, foregrounds inherit the vivid
colors), and themeForSkin merges polarity overlays instead of replacing.
Palette audit reworked: base colors audited fully, overlays for valid keys
and fill polarity.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
296303302d perf(tui): skin switches stay hot — MCP gating, pooled reload, swap repaint
Four responsiveness/hardening fixes surfaced by rapid /skin switching:
config.get mtime now carries an mcp_rev hash so the TUI reloads MCP only
when MCP-relevant config changed (cosmetic /skin writes cost seconds of
reconnects before); reload.mcp runs on the RPC pool serialized by a lock
(inline it froze the stdio reader for the duration of a flapping server's
retry loop — config.set/complete.slash sat unread and the TUI appeared
dead); theme swaps schedule one full repaint (incremental diffs after a
recolor tear — stale cells keep the old palette, read as "shadows"); and
OSC-11 pure-black answers are distrusted universally (unset-default
fingerprint on xterm.js hosts and tmux). Parent EIO zombie fixed: a dead
PTY made every render write throw once a second forever; exit after 5
consecutive dead-stream errors.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
c0763bd13a feat(ui-tui): theme engine — seeds to derived-tone ladder + flash-free boot
The desktop color-mix system, ported: a theme is a handful of identity
SEEDS (text, primary, accent, border, status hues); every secondary tone
(muted, label, surfaces, chips, selection) is a color-mix derivative
against the real terminal background. lib/color.ts consolidates the
primitives (parse/mix/luminance/contrast/retone + xterm.js's multiplicative
liftForContrast). Knobs are grid-search fitted so the math reproduces the
classic hand-tuned literals (contract-tested). The display shim renders
authored palettes RAW and only rescues near-invisible colors. Boot reads
the last resolved theme from $HERMES_HOME/tui-theme-boot.json so the first
frame paints in the right palette (no default-dark flash), and the
placeholder cursor follows the bubbles textinput pattern.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
cd05498e2c feat(ui-tui): background-aware theme adaptation + paired palettes + /theme pin
OSC-11 asks the terminal for its actual background at startup (env
heuristics are blind on xterm.js hosts) and the theme re-derives against
the answer: desktop-contract adaptation (contrast floors + fill polarity),
a shared list-row selection primitive instead of per-picker panel fills,
paired light_colors/dark_colors skin blocks with a machine audit, and a
/theme auto|light|dark pin (display.tui_theme) for hosts whose probe lies.
E2E coverage for the OSC reply chain + /theme-info diagnostics.
2026-07-21 16:36:57 -05:00
Brooklyn Nicholson
4a129af709 refactor(ui-tui): route production surfaces through the widget-grid engine
Banner (responsive tiers: full logo -> compact rule -> text -> hidden),
SessionPanel (fixed hero track + flexible info track, the desktop pane
shell's fixed-vs-flex contract), floating overlays, prompt zone, and the
pickers all render through WidgetGrid instead of hand-rolled flex math.
Pickers gain maxWidth so grid cells can cap them.
2026-07-21 16:36:17 -05:00
Brooklyn Nicholson
58d75ec5c6 feat(ui-tui): widget-grid 2-axis layout engine + overlay primitives (#20379 rebase)
Rebase of the widget-grid PR onto current main, then grow it into a real
2-axis engine: resolveGridTracks (grid-template tracks — fixed cell counts
and weighted fr shares with mins), layoutWidgetGrid (1D auto-packing flow),
and layoutGridAreas (2D absolute placement with row/col spans and implicit
row growth). Overlay/Dialog primitives give zoned viewport-level modals
with an optional scrim. /grid-test (interactive: areas, nesting, zoom,
gap/padding) and /grid-test streams (4x3 mission-control GridAreas board
with promote-to-main) exercise everything end to end.
2026-07-21 16:36:17 -05:00
brooklyn!
81f4095bc1
Merge pull request #68918 from NousResearch/bb/composer-focus-keys
feat(desktop): type-to-focus the composer from empty chat chrome
2026-07-21 16:33:45 -05:00
Brooklyn Nicholson
a1a406fd40 feat(desktop): type-to-focus the composer from empty chat chrome
Printable keys and soft `/`/Enter pull focus back to the composer when
nothing else owns the key (dialogs, terminal, buttons on Enter, …).
2026-07-21 16:03:19 -05:00