Commit Graph

428 Commits

Author SHA1 Message Date
bixycler 70d0f556d7 fix(branding): use the Caduceus ☤ (U+2624), not the Rod of Asclepius ⚕ (U+2625)
Every inline glyph — CLI banner/status bar/response labels/goodbye, setup
and doctor boxes, gateway update prompts, WhatsApp reply prefix, TUI theme,
locale strings and the docs — used ⚕, the staff of Asclepius (medicine).
Hermes carries the Caduceus ☤. The ASCII-art logo was already correct.

Mechanical swap across 60 files (no logic change); both glyphs are
East-Asian-width Neutral so no layout shifts. Skins that set their own
`response_label` / `goodbye` are unaffected.

Direction from PR #7064 (@bixycler), the earliest of #7064 / #9611 / #15574,
redone against current main.

Fixes #9565
2026-09-12 08:25:54 -07:00
Teknium 92e0de0ac4 feat(vault): Desktop, TUI and CLI surfaces for password-manager unlock
Desktop
- Settings → Credential Vault gains a "Password managers" section: per-manager
  toggle (disabled with a hint when the CLI isn't installed), Locked/Unlocked
  pill, Unlock (masked master-password dialog → vault.unlock) and Lock.
  Items from a manager show a source badge instead of a delete button.
- Mid-turn vault.unlock.request renders a masked card in the chat (same
  contract as the secret/sudo cards: dismiss = keep locked, late answers
  tolerated, blocks the composer, badges background sessions).
- i18n parity en/ar/ja/zh/zh-hant.

Ink TUI (hermes --tui): vault.unlock.request/expire overlay via MaskedPrompt;
Esc keeps the manager locked.

CLI: `hermes vault sources [--enable|--disable NAME]`; `hermes vault list`
shows the source column and names enabled-but-locked managers.

Docs: credential-vault.md covers managers, per-session unlock, and the
headless (cron/webhook/API/-q) no-prompt posture.
2026-09-10 10:35:07 -07:00
Siddharth Balyan ae43fd6df6 fix(tui): bare URLs render verbatim instead of a derived site label (#106843)
The markdown renderer resolved every link to a label — an authored one when
present, otherwise the fetched HTML <title>, otherwise a slug derived from the
last path segment. For a bare URL that meant the target never reached the
screen: `Connect link: https://connect.example.com/link/lk_...` rendered as
`Connect link: <site name>`, so the connect-link handoff could not be read,
copied or retyped from the TUI.

A bare URL is now its own text. Authored markdown labels still win, because
`Link` emits the OSC 8 hyperlink unconditionally and the renderer records it
per cell, so a label never strands its target. Title resolution no longer
drives any render path.
2026-09-10 02:21:54 +05:30
hermes-seaeye[bot] d4d4ecfae0 fmt(js): npm run fix on merge (#105739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 10:47:44 +00:00
Teknium 3863d13440 docs: describe one-line docks and consistent transcript controls 2026-09-08 03:42:08 -07:00
Teknium 0a3b7fdce2 fix: preserve Ink composer cursor across agent monitors 2026-09-08 03:42:08 -07:00
Teknium f7fe32bfde feat: add compact Ink agent dock and Enter live tail 2026-09-08 03:42:08 -07:00
hermes-seaeye[bot] 04b88a4c0a fmt(js): npm run fix on merge (#105732)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 10:17:46 +00:00
hermes-seaeye[bot] 7ee52894a7 fmt(js): npm run fix on merge (#105729)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 10:11:57 +00:00
Teknium 3669e17a50 fix: keep the compact agent dock passive 2026-09-08 03:06:30 -07:00
Teknium 4aad9b11b1 fix: distinguish the live agent dock with a themed surface 2026-09-08 03:06:30 -07:00
Teknium 924c5ded2e fix: scope subagent stops and publish authoritative live progress 2026-09-08 03:06:30 -07:00
Teknium 8a7b6ffad2 fix: preserve newer Ink controls when status hydration arrives late 2026-09-08 03:06:30 -07:00
Teknium 8e4b4e0cc8 fix(tui): follow newest transcript output in tail view 2026-09-08 03:06:30 -07:00
Teknium 1ae7389502 fix(tui): keep agent controls visible on narrow terminals 2026-09-08 03:06:30 -07:00
Teknium f3a9ca089f feat(tui): inspect live tails and steer from full-height agent roster 2026-09-08 03:06:30 -07:00
Teknium 2a908d762b feat(tui): dock live subagent roster above composer
Salvage bounded row projection and status glyphs from PR #70899; hydrate the existing tree from session-scoped snapshots and open with Ctrl+T without disturbing drafts.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
2026-09-08 03:06:30 -07:00
Ben Barclay 6e2b8e070d fix(tui): match the redo chord case-insensitively so Cmd+Shift+Z works on extended-key terminals (#105493)
Since #90674 hermes-ink restores the shifted letter's case for CSI-u and
modifyOtherKeys input, so Cmd+Shift+Z reaches the composer as inp 'Z' with
key.shift set. The redo branch compared inp === 'z' and missed, falling
through to the printable path and inserting a literal "Z". The legacy
raw-byte path (ESC Z) already delivered 'Z', so the binding was latently
case-sensitive on both paths.

Compare with inp.toLowerCase(), matching the copy/paste chords a few lines
above. Linux Ctrl+Shift+Z is unaffected (ctrl chords keep the lowercase
key name).

The test drives the real TextInput through renderSync with kitty CSI-u
bytes (super+z, then super+shift+z) and asserts the redo lands and no "Z"
is inserted; it fails on the unfixed tree with "aZ".
2026-09-08 12:04:34 +10:00
hermes-seaeye[bot] 966637323e fmt(js): npm run fix on merge (#105451)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-08 00:13:36 +00:00
Teknium 04767e7aaa fix(display): distinguish estimated context from provider usage 2026-09-07 08:13:01 -07:00
hermes-seaeye[bot] 6840bb02e8 fmt(js): npm run fix on merge (#101102)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-02 08:42:10 +00:00
Teknium a2600740e8 feat(delegate): tag every subagent progress line with its batch id
Concurrent or nested delegation batches (a parent's 9-way fan-out plus a
child's own 3-way fan-out) printed interleaved `✓ [3/3]` / `✓ [3/9]` lines
with nothing identifying which batch each belongs to.

- CLI: batch header `🔀 [6a66] delegating 9 tasks`; completion lines and
  child tree-view lines become `[6a66 3/9]`; spinner remaining-count tagged.
- Relay: `delegation_id` rides on every `subagent.*` event (TUI gateway
  payload, api_server SSE subagent.start/complete).
- TUI: `[6a66 3/9]` prefix on /agents rows; Desktop Agents pane groups
  workers by exact delegation_id (heuristic shape/time grouping kept for
  older backends) and shows the tag on the group header.
- Tag = last 4 hex of the deleg_xxxxxxxx id (format_batch_tag), same id
  returned by the dispatch and used for cache/delegation/live/<id>/.
2026-09-02 01:06:24 -07:00
Teknium b1d99fe9e4 style(ui-tui): sort StatusRule compacting prop for perfectionist lint 2026-08-31 09:57:04 -07:00
HexLab98 3a542bbef4 fix(tui): show status while idle/auto compaction runs
Idle and preflight compaction arrived as lifecycle status without the
"Compacting context" marker, so TUI never entered a compacting state.
Re-tag those lines and freeze the busy FaceTicker on "compacting" for
the whole pause instead of restoring "running…" after 4s.
2026-08-31 09:57:04 -07:00
Teknium 86a2fdc634 feat(tui): status rule shows cache-hit %, latency, t/s and honors display.status_bar.fields
Extends PR #98250's classic-CLI status-bar upgrades to the Ink TUI:
- tui_gateway/server.py _get_usage() now emits cache_hit_pct,
  avg_latency_s, avg_tps (reads the same per-call deque history from
  agent/conversation_loop.py; keys omitted when no data — Codex
  app-server has no latency, zero cache reads show no %)
- StatusRule renders the three read-outs as width-budgeted tail
  segments (breakpoints 96/104/110 cols, lowest priority — they shed
  first on narrow terminals)
- display.status_bar.fields (the SAME key the classic CLI honors)
  filters TUI segments too: cache_hit, latency, tps, duration,
  compressions, bg_tasks, bg_subagents, voice, battery, title,
  context_pct, context_detail
- values ride the existing usage payload/ticker; constants between
  events so the usage==last dedup keeps suppressing repaints
- 3 new server tests, 5 new TUI tests; full ui-tui suite 1727 green
2026-08-29 19:12:24 -07:00
nftpoetrist 8142494401 fix(tui): render nested todo subtasks via the parent field
CLI, ACP, and the desktop app all got nested-subtask rendering (the
optional `parent` field on a todo item), but the TUI never did. Its
TodoItem type had no `parent` field, parseTodos() in turnController.ts
dropped it even if the tool payload sent it, and TodoPanel rendered the
list with a flat map() and a single fixed indent — a session using
nested subtasks showed every subtask at the same visual level as its
parent, with no hierarchy cue, in the terminal UI.

- types.ts: add the optional `parent` field to TodoItem, matching
  apps/desktop/src/lib/todos.ts's TodoItem exactly.
- turnController.ts: parseTodos() now preserves parent (trimmed,
  dropped if empty or self-referential), the same normalization
  desktop's parseArray() applies.
- lib/todo.ts: port todoTree() from apps/desktop/src/lib/todos.ts
  verbatim — same DFS-with-depth algorithm, same dangling/cycle
  handling, so both surfaces render identical hierarchy from the same
  `parent` field.
- todoPanel.tsx: render todoTree(todos) instead of a flat map(), with
  per-row indentation scaled by depth (capped at 4 levels, mirroring
  desktop's status-row.tsx cap).
2026-08-29 17:58:41 -07:00
Brooklyn Nicholson 2725d3225b fix(tui): stop the composer placeholder from sticking Terminal.app into dim
The placeholder hint and its synthetic cursor chip hand-rolled truecolor
escapes ([38;2;r;g;b / [48;2;r;g;b]) and wrote them raw past Ink's depth
layer. Legacy Terminal.app has no truecolor parser — it walks compound
params one by one, so the literal 2 in 38;2;… lands as SGR 2: dim ON,
with no 22m ever emitted. Every frame that painted the placeholder left
the terminal's dim attribute stuck, and subsequent cells rendered dimmed
until an unrelated bold span's 22m happened to clear it — text randomly
flipping dim and back, worst right after the composer empties.

Measured on a live resumed session (PTY capture, params interpreted the
legacy way): 1026 glyphs painted with stuck dim on main, 0 with the fix.

Route both helpers through Ink's own colorize, the same repair colorizeEcho
got for the fast-echo path (gray-accent bug) — the escape now downgrades
with the terminal's real color depth, and a 256-color terminal gets 38;5;N
it can actually parse.

Also harden hermes-ink's transitionAnsiCodes for compound SGRs: real tool
output ships [1;31m-style sequences whose endCode is [0m, dodging the
endCode-based weight detection — parse the params instead (skipping 38/48
extended-color arguments) so a compound bold→dim transition passes through
SGR 22 too.
2026-08-19 11:03:01 -05:00
ethernet d9f98fe071 feat(tui): restore the earlier answer when a batch question is re-visited
Tab or Shift-Tab onto an answered question now restores its state, the
same model as the CLI panel. A choice answer puts the cursor back on
its row. An answer that matches no choice was typed via Other, so the
cursor lands on the Other row with the text staged in the input —
Enter then edits the earlier text instead of starting blank. The
restore logic lives in a pure helper (clarifyBatchRevisitState) with
direct tests, because the prompt component has no keystroke harness.
2026-08-18 21:28:53 -04:00
ethernet 96b17787c7 feat(tui): Tab cycles batch clarify questions directly
The batch prompt had a separate browse mode: Tab toggled between the
question list and the expanded question, and the arrows walked the
list. Now Tab moves to the next question and Shift-Tab moves to the
previous one, with wrap, and the active question is always the
expanded one — the same model as the CLI panel.

A locked answer now renders on its own indented line in the ok color
under its question, instead of an arrow suffix on the status line, so
the answers stay readable while Tab walks the list. A skipped (empty)
answer renders muted and italic.
2026-08-18 21:28:53 -04:00
ethernet a9b8d69a11 feat(tui): compact multi-question clarify prompt
Batch clarify renders as a status list — every question on its own
line (✓ answered / ▸ current / · pending) with only the active
question's choices expanded, so a 5-question batch stays a few rows
tall. Enter locks the active question's answer (clarify.respond with
question_id) and the cursor jumps to the next unanswered question;
Tab walks the question list to answer in any order; the hint reads
'confirm and continue' when one question remains. Esc cancels the
whole batch.

Answered rows collapse to '✓ question → answer'. The abandoned-prompt
transcript record keeps locked partials (they survive a server-side
timeout), and reconnect replay seeds them back into the overlay.
2026-08-18 21:28:53 -04:00
Brooklyn Nicholson c1358e45d2 feat(tui): paint references in the composer as you type them
The composer renders one flat string, so a reference only became visible
after sending. It now wears the theme accent live, through both the cursor
and selection renderers; a masked input is a password and never highlights.

Two things the fast-echo bypass needed. It writes only the new cells, so a
keystroke that RECOLORS existing ones — `]` closing a token, a second `/`
demoting `/usr` to a path — has to take the Ink path instead. And its own
escape went through Ink's colorize rather than a hand-rolled truecolor
sequence: `38;2;` is unparseable on a 256-color terminal, where the accent
fell back to the default foreground and read gray.
2026-08-18 08:42:37 -05:00
Brooklyn Nicholson e69d2fda8a feat(tui): one reference vocabulary for the composer and the transcript
A sent message accented a `/skill` named mid-prose and nothing else, so an
`@file:` ref and an `[[ Image 1 ]]` token flattened into body text. The
composer painted none of it.

splitComposerHighlights covers the whole vocabulary the desktop chips —
`/work` invoked or referenced, every `@ref` shape including quoted values,
and attachment/paste tokens — and both surfaces read it, so what you type
is what you see once it lands. Supersedes splitSlashSkillRefs.
2026-08-18 08:42:37 -05:00
brooklyn! 21c1fe6686 fix(tui): modified Enter and bare LF insert a newline in the composer across IDE and macOS terminals (#87854)
* fix(tui): send atomic CSI u for modified Enter in IDE terminals

VS Code/Cursor/Windsurf terminals bound Shift/Ctrl/Cmd+Enter to the
legacy \\r\n sequence, which Ink's parse-keypress split into a
backslash keypress plus a plain Return — inserting a stray backslash and
submitting instead of adding a newline. Emit Kitty CSI u sequences that
encode the modifier atomically, and migrate keybindings users already
have on disk.

Co-authored-by: yatesjalex <yatesjalex@users.noreply.github.com>

* fix(tui): treat a bare LF as a newline in macOS composer terminals

Terminals that can't send a distinct Shift+Enter collapse a modified
Enter / Ctrl+J down to a bare LF. shouldPreserveCtrlJNewline() already
handles the env-detectable cases (SSH, Windows Terminal, Ghostty, WSL),
but plain macOS terminals (Terminal.app, iTerm2 defaults) do the same and
aren't env-detectable, leaving no keyboard-driven newline there. Fold the
return-key decision into shouldInsertNewlineOnReturn() and accept a bare
LF as a multiline fallback on macOS too, keeping CR as submit everywhere.

Co-authored-by: LeonSGP43 <LeonSGP43@users.noreply.github.com>

---------

Co-authored-by: yatesjalex <yatesjalex@users.noreply.github.com>
Co-authored-by: LeonSGP43 <LeonSGP43@users.noreply.github.com>
2026-08-16 17:33:23 +00:00
null-runner 24af6685b0 fix(tui): word-delete forward and transactional cut
The dashboard maps Ctrl+Delete to ESC d for delete-word-forward, but the
composer had no binding for it: hermes-ink decodes ESC d as meta+'d', which
fell through to the printable path and typed a literal "d" instead of
deleting the next word. Add a meta+d branch that mirrors the existing
Ctrl+W delete-word-backward, sharing a deleteWordForward helper with the
Delete+word path.

The new cut() wrote the clipboard fire-and-forget and removed the selected
text immediately, so on a headless/SSH box with no clipboard backend the
write fails and the text is lost with no copy to paste back. Make cut
transactional via cutSelection(): await the write and only remove the
selection when it succeeds; on failure the selection stays intact. The
removal also re-checks the selection to avoid slicing with stale offsets
after the awaited write.
2026-08-15 01:05:39 -07:00
null-runner be250390db fix(tui): restore copy and cut shortcuts 2026-08-15 01:05:39 -07:00
Teknium 77fcc2ea31 feat(display): honor display.timestamps across desktop transcript and TUI
One config key everywhere (#41531): the same display.timestamps that stamps
[HH:MM] on classic-CLI labels now gates the desktop transcript's timeline
timestamps and renders dim [HH:MM] labels on TUI user/assistant rows.

- desktop: $displayTimestamps store fed from config.yaml via
  use-hermes-config; TimelineTimestamp renders nothing while the key is off
  (the default). Hover tooltips with the exact time stay ungated (#70450).
- TUI: tui_gateway forwards each persisted row's timestamp in the display
  projection; toTranscriptMessages threads it as Msg.createdAt; live rows
  are stamped at append (the #82840 rule); MessageLine shows a dim [HH:MM]
  above user/assistant rows when display.timestamps is on.
- No new config keys, no HERMES_* env vars; display-only, prompt-cache safe.
2026-08-15 01:04:19 -07:00
hermes-seaeye[bot] af585de28e fmt(js): npm run fix on merge (#86801)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 07:46:57 +00:00
Jordy Elfferich 2b0b4a2191 feat(tui): auto-collapse reasoning blocks only when the reasoning phase ends
Under display.sections.thinking: collapsed, the TUI now keeps the LIVE
reasoning panel open while reasoning streams and collapses it the moment
the reasoning phase ends (first tool call, final answer, or new turn).

Previously 'collapsed' meant the panel was always collapsed — including
the currently-streaming reasoning — and there was no way to get
'expanded while live, collapsed when done'. This makes 'collapsed' an
auto preference:

- turnController tags the open reasoning segment isLiveReasoning and
  seals the tag in endReasoningPhase/closeReasoningSegment
- streamingAssistant passes reasoningActive only to the live segment,
  so sealed reasoning segments from earlier phases stay collapsed
- ToolTrail auto-opens while reasoningActive under collapsed mode;
  expanded/hidden/MoA-reference semantics are unchanged

Adds thinkingLiveCollapse.test.tsx covering open-on-stream, close-on-
finish (including mid-turn rerender), and the expanded-mode no-op.

(cherry picked from commit 6ef4ef77d3a0837c8ef5c5dac3de74f352bd637d)
2026-08-15 00:34:57 -07:00
LeonSGP43 b587681afc fix: keep historical thinking collapsed in tui
(cherry picked from commit 354999388a4e465b6b7efa46cdc61d8d5b22b35f)
2026-08-15 00:34:57 -07:00
hanhvs 5dec501c6e fix(tui): stop Vietnamese Telex IME from dropping characters
Third-party Vietnamese IMEs (OpenKey/Unikey/EVKey in Telex mode) recompose
a syllable by emitting an erase burst followed by the finished characters.
Two layers of the TUI input pipeline mishandled this, dropping letters and
leaving a stray space mid-syllable (e.g. "hạnh" rendered as "hạ  ", and
"vương sỹ hạnh" as "vương sỹ hạ  ").

Root causes, both confirmed from real captured byte streams:

1. parse-keypress: an IME often fuses a control byte (\x7f/\b, or even the
   U+202F marker OpenKey injects) with the recomposed text in a single stdin
   read. parseKeypress only recognizes a control key when the whole string is
   exactly that byte, so a mixed chunk fell through every branch, returned
   name:"" with a non-printable sequence, and the composer's printable gate
   discarded the entire chunk — taking the surrounding letters with it. Split
   text tokens on every control byte so the printable runs survive.
   CR/LF are deliberately not split, preserving paste/return semantics.

2. textInput: multi-character (IME/paste) inserts were committed through the
   16ms deferred key-burst path, which raced an interleaved re-render and
   snapped the buffer back to a stale value, dropping the recomposed tail.
   Commit them synchronously. Additionally, the fast-echo "\b \b" backspace
   shortcut desynced the screen when it ran right after an Ink repaint (forced
   by the U+202F marker), stranding the marker glyph; suppress fast-echo for
   the recompose burst that follows an Ink repaint and resume it on the next
   real keystroke.

Tested with real OpenKey and EVKey captures of "vương sỹ hạnh" across read
timings, plus parser unit coverage and an EVKey no-regression guard.
2026-08-15 00:33:49 -07:00
InphinitiZ 834a9fc89d fix(tui): preserve IME text before return submit
Preserve printable IME commit text when xterm delivers it in the same input burst as Return, so Dashboard/TUI submits the visible draft instead of dropping the final segment.

Also fixes the TUI type-check stdio tuple typing and adds focused regression coverage.
2026-08-15 00:33:49 -07:00
Cornna 084fca9dbf fix(tui): clear input after Korean IME submit 2026-08-15 00:33:49 -07:00
hermes-seaeye[bot] e34c4c009a fmt(js): npm run fix on merge (#82771)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 22:43:38 +00:00
ygd58 408634c2e1 fix(tui): fix unreadable session-title chip contrast in the status bar
Fixes #82465.

The session-name chip at the right end of the TUI status bar rendered
white/near-white text (t.color.statusFg) on a raw, full-saturation
accent-hue background (t.color.accent, #FFBF00 -- bright yellow -- in
DARK_SEEDS). Two token problems stacked: accent is the accent
IDENTITY hue, never used elsewhere as a solid fill (fills are always
softened, e.g. activeRow = mix(surface, accent, 0.22)); and statusFg
is derived as a light gray lifted toward near-white text, a tone never
designed to sit on a saturated fill. Together: roughly 1.5-2:1
contrast, unreadable on the default dark theme.

Applied the issue's recommended first option: drop the background fill
entirely and render the title as accent-colored text on the normal
status bar background. Same highlight intent (the title still stands
out via its color), readable contrast on both dark and light seeds.

Updated the existing test that had encoded the buggy background-fill
expectation, and added an explicit contrast-regression assertion.
Verified as a genuine regression by reverting the fix and confirming
the test fails with the exact reported #FFBF00 background color.

54/54 pass across the three appChrome-related test files (no
regression).
2026-08-09 13:59:29 -07:00
Teknium 5a16635f40 feat(cli): show session titles in status bars 2026-08-08 17:02:55 -07:00
kshitij e8c1882766 refactor(ui-tui): single source for the floating-panel kind set
Review follow-up: $isStatusRuleOccluded and FloatingOverlays each
enumerated the same six overlay kinds — adding a 7th floating panel
required updating both or the timer gate silently missed it. Extracted
hasFloatingPanel as the shared predicate (completions stays local to
FloatingOverlays; it deliberately never occludes the status rule).
Full ui-tui suite green (1487 tests).
2026-08-03 10:00:24 +05:30
briandevans ee8765c930 fix(ui-tui): gate status-rule timers on real occlusion, not $isBlocked
`$isBlocked` answers "is text input suspended", not "is the status rule
covered". appLayout uses it only to hide the input rows (appLayout.tsx:384);
`StatusRulePane` renders outside that guard, at :365 for `at="top"` and :449
for `at="bottom"`. So the previous revision paused FaceTicker /
SessionDuration / IdleSince under prompts that leave the rule fully on
screen — approval, billing, subscription, confirm, clarify, sudo and secret
all render through PromptZone in NORMAL FLOW above ComposerPane
(appOverlays.tsx:58-162, appLayout.tsx:553-568). They push the rule down;
they do not cover it. Freezing a visible clock is a worse bug than the churn
being removed.

Replace it with `$isStatusRuleOccluded`, a narrow derived store over
overlay + ui state covering only what actually paints over the rule:

- `widget` — the modal widget slot renders at viewport level
  (ActiveWidgetSlot, sdk/host.tsx:209) so it can anchor the full-screen
  absolute `Overlay` against the whole terminal.
- the FloatingOverlays set (modelPicker, pager, petPicker, sessions,
  skillsHub, pluginsHub) — but only when `ui.statusBar === 'top'`. That
  panel is `position="absolute" bottom="100%"` inside ComposerPane's
  relative Box (appOverlays.tsx:387), so it grows UPWARD over the top rule
  and can never reach the bottom one.

Deliberately excluded: the PromptZone flow states above; `agents` and
`journey`, which unmount the entire ComposerPane subtree (appLayout.tsx:553)
so React's effect cleanup already clears the intervals; `ambient`, an
in-flow dock; and composer completions, which share the floating grid but
are a render prop that changes per keystroke — re-arming a 1s interval on
every character would restart the countdown each time and starve the tick.
`statusBar: 'off'` needs no branch: StatusRulePane returns null for both
slots, so the timers never mount.

Tests: the store-level cases are re-split into occluding and non-occluding
sets, and an AppLayout-level `describe` mounts the real layout so the rule
sits in its true position — asserting that under approval and sudo the rule
is still rendered AND its clock advances (1m 0s to 1m 30s), that a floating
model picker suppresses the clocks with the rule at the top, and that the
same picker leaves them armed with the rule at the bottom.
2026-08-03 10:00:24 +05:30
briandevans 873af72f8c fix(ui-tui): pause status-chrome timers while a blocking overlay is open
`StatusRulePane` renders `StatusRule` outside the `!isBlocked` guard in
appLayout.tsx, so the status rule stays mounted underneath approval,
model-picker, pager, sessions and every other blocking overlay. Its three
timer-driven components keep firing the whole time: `FaceTicker` (glyph,
1s clock, verb rotation), `SessionDuration` (1s) and `IdleSince` (1s).
Every tick re-renders a rule nobody can see, and in an Ink TUI that churn
reads to the user as the dialog flickering.

Gate all three components' interval creation on the existing `$isBlocked`
computed store, so nothing is armed while an overlay covers the rule.

The pause alone would leave the elapsed read-outs frozen at the moment the
overlay opened, so each effect re-seeds `now` from the wall clock when it
re-arms. `SessionDuration` and `IdleSince` already did this; `FaceTicker`
gains the same re-sync. Closing a five-minute overlay now resumes at the
true elapsed time instead of the pre-overlay value.

No new store is introduced — `$isBlocked` already exists and already ORs
the current OverlayState field set.
2026-08-03 10:00:24 +05:30
hermes-seaeye[bot] 17e5f7244a fmt(js): npm run fix on merge (#75582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 18:54:27 +00:00
brooklyn! f27d45e288 feat(tui): reach the model picker without wrecking your draft, and switch mid-turn (#74756)
* feat(tui): Ctrl+O opens the model picker without clearing your draft

Reaching the model picker meant typing /model, which forces you to wipe
whatever you'd already drafted. Bind Ctrl+O to open the same picker overlay
directly, leaving the composer untouched. Ctrl+O is added to the textInput
pass-through allowlist so the composer doesn't swallow it, mirroring the
existing Ctrl+X session-switcher path.

* feat(tui): apply a mid-turn model switch at the next turn instead of rejecting it

Picking a model while a turn was streaming hit a 4009 'session busy' reject:
switch_model() mutates the agent's model/provider/base_url/client in place and
the worker thread reads those every iteration. Now config.set queues the pick
in session[pending_model_switch] and _apply_pending_model_switch applies it on
the turn thread at the next turn start, before any model call — no race, no
interrupt, no waiting on the client rebuild. The TUI paints the pick optimistically
and notes '(applies next turn)'.
2026-07-30 10:18:30 +00:00