Commit Graph

958 Commits

Author SHA1 Message Date
hermes-seaeye[bot]
74ad422d50 fmt(js): npm run fix on merge (#94046)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 17:03:13 +00:00
Eva
e3f695e5e0 fix(tui): heartbeat and bounded reconnect for silent WebSocket drops
the client half of the gateway.ping heartbeat contract (#89958); detects a silently-dropped socket via missed ping-acks and reconnects with bounded backoff; part of the #83166 recovery series.
2026-08-24 04:01:53 -07:00
Andrex Ibiza, MBA
6d48fbed1b fix(tui): settle tmux clipboard load on child exit
Mark the write-only tmux load-buffer call as resolve-on-exit so a daemonized tmux server cannot retain inherited stdio and force a false timeout after the direct child has succeeded.

Completes the call-site acceptance item from #93134 as a companion to #93148.

Co-authored-by: JoaoMarcos44 <87440198+JoaoMarcos44@users.noreply.github.com>
2026-08-23 18:25:27 -07:00
liuhao1024
27fb1179f8 fix(tui): settle execFileNoThrow on timeout even when a daemon holds stdio
The timeout handler only called settle(124) when resolveOnExit was true.
In the default path the promise waits for 'close', which requires every
inherited stdio handle to close — a daemonized grandchild that kept the
pipes open meant 'close' never fired, and after the timeout SIGTERM
(which only reaches the direct child) nothing settled the promise. The
await hung forever: the clipboard path (setClipboard -> tmuxLoadBuffer
-> osc.ts spawn without resolveOnExit) leaked a pending promise whenever
a spawned tool forked a stdio-inheriting daemon (#93134).

Settle(124) unconditionally in the timeout handler. The settled-guard
makes it a no-op when the child's own 'exit'/'close' won the race, so
normal timeout behavior is unchanged; in the daemon case it becomes the
only exit and returns the same 124 the close path would have.

Also un-skips the documented-hang regression test, with a 30s daemon
sleeper so it genuinely outlives the timeout (and vitest's own 5s test
timeout — before the fix the test fails by timing out, not asserting),
plus an elapsed bound.
2026-08-23 18:25:27 -07:00
Teknium
1bf8bd2c7d feat(models): 'ox alpha' now finds x-preview-f-free in every model picker
The OpenCode Zen wire slug for the Ox Alpha stealth model is opaque
(x-preview-f-free); users searching the picker for 'ox' or 'ox-alpha'
found nothing. Adds the search alias across all four synced alias
tables (CLI, desktop, web, TUI) plus tests. Wire id is unchanged and
still what renders and gets sent to the provider, matching the k3 →
kimi-k3 precedent. No canonical-dedup collision with opencode-go's
keyed ox-alpha-free slug.
2026-08-21 14:38:19 -07:00
hermes-seaeye[bot]
27562ad5f8 fmt(js): npm run fix on merge (#90637)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 07:37:56 +00:00
kshitijk4poor
45f11263bd fix(tui): skip the kitty protocol push for Ghostty in the Ink TUI too
Widen the cli.py Ghostty exception to the sibling sites the review found: the Ink TUI pushes CSI >1u at raw-mode entry (App.tsx), on alt-screen exit, and on the extended-keys re-assert path (ink.tsx) for every EXTENDED_KEYS_TERMINALS entry including ghostty - same Alt-stripping bug. New skipKittyKeyboardProtocol() helper in terminal.ts gates the ENABLE push at all 3 sites; the DISABLE (pop) stays unconditional since popping an empty stack is a spec no-op. Also fix the cli.py comment citing the modifyOtherKeys encoding where the kitty CSI-u form (ESC[127;3u) is what the broken path expected, dedupe the quadruplicated Ghostty comment, and update the stale 'mirroring the Ink TUI' docstring. 7 new vitest cases.
2026-08-20 11:39:03 +05:30
hermes-seaeye[bot]
eab087c06f fmt(js): npm run fix on merge (#90140)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 16:56:58 +00:00
Brooklyn Nicholson
6a3d50c6e0 fix(tui): allow the ESC byte in the SGR param matcher
eslint no-control-regex rejects the CSI regex even though ESC is the
sequence we have to parse.
2026-08-19 11:03:01 -05: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
royzhrxy-glitch
a787bfeab9 fix(desktop): restore profile/agent switching in release builds (nanostores 1.4.2)
nanostores 1.4.0-1.4.1 annotate batch() @__NO_SIDE_EFFECTS__. Rollup
(via vite build) honors that and erases a result-unused batch(...) call
as dead code -- callback included. Since d57f94a33/053eb7aab/4e520f085
moved the gateway-switch publication (activate() +
+ ) inside batch(), packaged desktop builds lost the entire
publication: clicking a profile in the rail did nothing at all.

Dev builds and vitest run unminified, so only the packaged app broke.

nanostores 1.4.2 removes the annotation from batch() (it stays on the
creation functions, where it is correct). Bump all three pinned copies
(apps/desktop, apps/bootstrap-installer, ui-tui) and add a regression
test asserting the installed nanostores never re-annotates batch.
2026-08-19 01:58:21 -07:00
Austin Pickett
4180c3f326 Merge pull request #89623 from NousResearch/fix/tui-focus-regain-atomic-repaint
fix(tui): heal focus regain without a separate screen clear (supersedes #88596)
2026-08-19 02:56:43 -05:00
hermes-seaeye[bot]
b7bed2419e fmt(js): npm run fix on merge (#89619)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 01:35:26 +00: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
31f62d76af fix(hermes-ink): reset SGR 22 when a style transition drops bold or dim
Bold (SGR 1) and dim (SGR 2) are independent terminal attributes that
share a single reset code (SGR 22). ansi-tokenize's diffAnsiCodes models
'same endCode' as 'same slot' — emitting [2m over a bold cell yields
bold+dim instead of dim, and dropping a weight entirely emits nothing.
Every such transition leaves the real terminal diverged from the
StylePool's tracked state, and since later transitions are computed from
that phantom state the corruption compounds and sticks: random spans of
wrong weight/brightness that depend on which cells changed in which
order — the long-standing 'random dimness/opacity changes at whim' in
the TUI.

transitionAnsiCodes() wraps the diff: when a weight flag is removed,
reset the family with SGR 22 and re-apply the target's weights; pure
additions and non-weight styles keep the minimal library diff. Wired
into StylePool.transition (cached per-pair, hot diff path) and the
full-frame renderer.

Proven by an end-to-end probe (LogUpdate frames -> strict SGR
interpreter -> compare cell attrs vs the screen model): 18 divergent
cells on main, 0 with the fix.
2026-08-18 14:08:43 -05: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 Nicholson
43f395a4f8 feat(tui): export Ink's colorize so callers can match its color depth
Anything writing raw SGR past the renderer has to resolve a tone the same
way Ink does — chalk downgrades to the terminal's real depth, and Apple
Terminal takes a bespoke rich-8-bit path on top of that. Sharing the
renderer's own function is the only way a bypass can't drift from it.
2026-08-18 08:42:37 -05:00
Brooklyn Nicholson
0593293962 fix(tui): Ctrl+C clears a typed draft instead of interrupting mid-stream
A non-empty composer used to lose to the busy-turn interrupt branch, so
Cmd/Ctrl+C while typing during a stream killed the agent. Clear first;
interrupt only when the input is already empty.
2026-08-18 08:07:53 -05:00
h8hawk
298f662108 fix(tui): restore Alt+Enter for newlines (#87066)
* fix(tui): restore Alt+Enter for newlines

Restore Alt+Enter support for inserting a new line in the TUI after the behavior was lost during newer input-handling updates.

Legacy terminals encode Alt+Enter as ESC followed by carriage return. Preserve those bytes as a single tokenizer sequence and parse the result as Return with the Meta modifier so TextInput inserts a newline instead of submitting.

Keep plain CR and LF mapped to unmodified Return, and cover the legacy ESC+CR sequence with a regression test.

* fix(tui): scope legacy Alt+Enter tokenization
2026-08-16 23:01:32 +00:00
hermes-seaeye[bot]
ca84f13b97 fmt(js): npm run fix on merge (#87880)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-16 18:07:16 +00: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
hermes-seaeye[bot]
77be513de1 fmt(js): npm run fix on merge (#86813)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 08:13:01 +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
Teknium
29ce482047 style: eslint --fix import ordering in salvaged IME tests 2026-08-15 00:33:49 -07:00
hanhvs
e2f7850ec3 fix(tui): tighten IME test per teknium1 review (#55415)
- Remove unconditional 60ms wait that let deferred path pass sync-commit test
- Use fake timers (setTimeout/setInterval/Date only, NOT setImmediate)
- Assert immediately after final read — no trailing wait/advance
- Add deterministic coverage for 60ms fast-echo suppression reset:
  * suppresses backspace after Ink repaint (IME recompose)
  * does NOT suppress on normal ASCII typing
- Verified: revert sync commit -> deferred path makes 4/6 tests fail
2026-08-15 00:33:49 -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
YuYigeng
77f35add0c fix(tui): honor destructive slash confirmation config 2026-08-15 00:33:32 -07:00
LeonSGP43
f080bc3db1 fix(tui): steady scrollbar after transcript shrink
(cherry picked from commit 40b0e93b12622679a34571fba04e13a27eec8e55)
2026-08-15 00:33:22 -07:00
Gabriel Atkinson
868c400e54 fix(tui): stop disabled pet cell polling 2026-08-15 00:32:53 -07:00
daromaj
fffa303db2 fix(ui-tui/ink): don't skip a zero-height box that hosts absolute children
Opening any floating panel in the TUI (`/resume`, `/sessions`, `/models`,
`/skills`, …) with an ambient dock widget loaded shows nothing: the overlay
takes input (Esc is the only way out) but never paints. Part of #69592.

`renderNodeToOutput` has a ghost guard for boxes Yoga squeezes to h=0 whose
sibling lands on the same row — without it, the shorter content leaves the
longer one's tail on screen. The guard returns before rendering children.

The composer's floating panels are `position: absolute; bottom: 100%`
children of a relative Box that also holds the input rows. Opening a panel
sets `$isBlocked`, which unmounts those rows, so the host collapses to h=0
with a sibling on its row — and the guard drops the whole subtree, overlay
included.

An absolute child paints outside its host's layout bounds, so it never
writes the shared row and cannot ghost it. Skip only when the subtree has
no absolute descendant. The walk runs solely inside the h=0 branch, which
is already rare, so it stays off the hot path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 21:22:06 -07:00
Michael Huang
2cabeba563 fix(tui,desktop): refresh context usage live during active turns 2026-08-14 20:24:42 -07:00
hermes-seaeye[bot]
31e571acf6 fmt(js): npm run fix on merge (#86407)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:16:25 +00:00
Teknium
eb20188d2e style: import order + padding lines in terminal.test.ts 2026-08-14 15:10:42 -07:00
Teknium
5d8569d043 fix(tui): gate BSU/ESU on capability for ALL write paths, not just alt-screen
Follow-up to the salvaged #66538 commit: the ZELLIJ env gate fixed
detection, but writeDiffToTerminal still wrapped main-screen frames in
BSU/ESU unconditionally (skipSyncMarkers was only set for alt-screen).
Under Zellij the multiplexer re-chunks the stream, so the markers buy no
atomicity and stale frames leak into main-screen scrollback as the
repeated chrome reported in #66490. The renderer now passes
!SYNC_OUTPUT_SUPPORTED for every write path; supported terminals keep
today's behavior on both screens. Adds emitted-frame regression tests
for both marker modes.
2026-08-14 15:10:42 -07:00
Brian Sweatt
d509e6df2f fix(tui): don't trust DEC 2026 synchronized output under Zellij
isSynchronizedOutputSupported() only excluded tmux, so running inside
Zellij under an outer terminal that advertises DEC 2026 (e.g. WezTerm
via TERM_PROGRAM) returned true. Zellij, like tmux, sits between us and
the outer terminal and chunks the stream, breaking BSU/ESU atomicity and
pushing old TUI frames into scrollback as repeated output.

Guard on the ZELLIJ env var (set to the session index, e.g. "0") the
same way we already guard on TMUX. Also thread an optional env argument
through the function so the behavior is unit-testable, mirroring
needsAltScreenResizeScrollbackClear() in the same module.

Closes #66490
2026-08-14 15:10:42 -07:00
Teknium
4d1ef7fbd2 style: satisfy curly + padding-line eslint rules in usage comparator 2026-08-14 15:07:24 -07:00
Teknium
513f683aa7 fix(tui): widen usage comparator to key union + regression tests
Follow-up to the salvaged #41484 commit:
- usageChanged() iterates the union of Usage keys generically instead of a
  hardcoded field list — the original PR's list omitted active_subagents
  (consumed by the status rule's subagent segment and resume hint), which
  would have suppressed legitimate updates
- The memo(StatusRule) half of the original PR is intentionally dropped:
  main's StatusRule gained battery/subagent/resume-hint segments since,
  and the wrapper broke the direct-call test seam. The load-bearing fix is
  the stable usage reference: unchanged deltas no longer mint fresh objects,
  so $uiState subscribers stop re-rendering per streaming event
- Regression tests: unchanged-reference retention, active_subagents-only
  update, key-union asymmetry
2026-08-14 15:07:24 -07:00
liuhao1024
8606867ebc fix(tui): reduce status bar flicker during streaming
The status bar flickers visibly during streaming because every state
patch (thinking.delta, reasoning.delta, tool.*, usage notifications)
creates a new $uiState object, forcing StatusRulePane and StatusRule
to re-render and redo expensive layout calculations on every event.

Three fixes applied:

1. Stabilize usage object references in createGatewayEventHandler.ts
   - Add mergeUsageStable() that shallow-compares Usage fields before
     creating a new object. When values haven't changed, returns the
     existing reference, preventing unnecessary StatusRule re-renders.

2. Memoize expensive computations inside StatusRule (appChrome.tsx)
   - statusBarSegments(cols) → useMemo([cols])
   - modelLabel() → useMemo([model, effort, fast])
   - ctxLabel, bar → useMemo([usage fields, segs])
   - Tail segment budget + fits() calculations → single useMemo block
     covering all progressive-disclosure logic

3. Wrap StatusRule in React.memo (appChrome.tsx)
   - Combined with stable usage references, allows React to skip
     re-renders when props haven't actually changed.

Fixes #41480
2026-08-14 15:07:24 -07:00
LeonSGP43
6356dc392e fix(ui-tui): redraw after session resume 2026-08-14 13:51:26 -07:00
John Lussier
96e794aa4a fix(tui): redraw after terminal focus regain 2026-08-14 13:51:26 -07:00