Commit graph

622 commits

Author SHA1 Message Date
ethernet
b288de8bf4
Merge pull request #31081 from NousResearch/bb/tui-skinny-status-rule
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Docker Build and Publish / move-latest (push) Has been cancelled
Tests / save-durations (push) Has been cancelled
fix(tui): keep status rule one-line in skinny terminals
2026-05-25 01:24:29 -04:00
brooklyn!
a627981a65
fix(tui): stop slash dropdown from chopping last char of /goal (#31311)
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Docker Build and Publish / move-latest (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Two independent bugs caused the slash-command autocomplete to render
`/goal` as `/goa` (and `/gquota` as `/gquot` for that matter) in the TUI:

1. `tui_gateway/server.py` was forwarding `c.display` from
   prompt_toolkit's `Completion` straight into the JSON-RPC payload.
   prompt_toolkit normalizes `display=` into `FormattedText` (a `list`
   subclass), so the wire format became `[["", "/goal"]]` instead of
   the `string` that `CompletionItem.display` in the TUI declares.
   `meta` already went through `to_plain_text` — `display` did not.

2. The dropdown row in `appOverlays.tsx` used `flexDirection="row"`
   with the display `<Text>` and the (very long) meta `<Text>` as
   siblings. When the meta overflows the row width, Ink/Yoga shrinks
   the *first* column by one cell, lopping the trailing character off
   the command name. `/goal` triggers it reliably because its meta
   string is the longest of any built-in command (description +
   embedded `[text | pause | resume | clear | status]` usage hint).
   Wrapping the display column in `<Box flexShrink={0}>` keeps it at
   its natural width and lets the meta wrap or truncate instead.
2026-05-23 22:12:55 -07:00
brooklyn!
f63ef74eaf
fix(tui): refresh virtual transcript on viewport resize (#31077)
* fix(tui): refresh virtual transcript on viewport resize

Notify scroll subscribers when ScrollBox viewport bounds change and key virtual-history updates on viewport height so resize/keyboard changes remount the tail rows instead of leaving stale spacers visible.

* test(tui): isolate viewport-height remount regression

Keep the resize delta below the virtual history scroll quantum so the regression test specifically depends on viewport height entering the snapshot key.

* test(tui): clarify virtual history resize snapshot

Update the resize regression and comments so the test specifically guards viewport-height changes in the virtual-history snapshot key.

* docs(tui): clarify scrollbox subscription signals

Document that ScrollBox subscribers are notified for renderer-computed viewport and content bound changes, not only imperative scrolls.

* fix(tui): recompute virtual tail after width resize

Avoid preserving a frozen virtual transcript range when wrapped rows shrink enough that the old tail window no longer covers the viewport.

* fix(tui): preserve transcript tail across resizes

Wraps + heights are column-dependent, so a width change must remeasure
every row and the renderer must repaint the full viewport.

- Key virtualRows on cols so React remounts wrapped rows on resize.
- Snap back to bottom after sticky-mode resize once React rerenders.
- Reserve a scrollbar + gap column in transcriptBodyWidth (non-termux).
- Full repaint on any viewport height change (was: shrink-only).
- ScrollBox scrollHeight uses deepest child bottom so sticky-bottom
  math can reach the real final rendered row after reflow.
- DECSTBM fast-path now requires full container rect match.

* feat(tui): responsive banner tiers

Terminals can't scale glyphs, so the banner now picks a layout per
column width instead of always rendering the full 101-col logo:

- Wide (>= logo width): full ASCII logo + tagline.
- Mid (>= 58 cols): centered rule banner that expands with viewport.
- Narrow (>= 34 cols): brand line + tagline, both width-aware.
- < 34 cols: hidden.

SessionPanel surfaces model/cwd/sid inline when the hero column is
hidden, so narrow layouts don't lose that info. Logo width constants
derive from the art itself.

* fix(tui): re-check sticky inside resize debounce + document remount

Addresses Copilot review on PR #31077:

- onResize now re-checks isSticky() inside the 100ms timer so manual
  scrolls during the debounce window don't get snapped back to tail.
- Comment on the virtualRows cols-keying calls out the deliberate
  trade-off: per-row local state (e.g. systemOpen) resets on resize so
  yoga can remeasure off live geometry. The hook's scale-by-ratio path
  is too approximate for mixed markdown widths.
2026-05-23 19:39:53 -05:00
chyuwei
7de8cd4c5f fix(tui): clear TTS env var on voice off, and add TTS indicator to status bar
Bug 1: /voice off in TUI mode did not clear HERMES_VOICE_TTS,
leaving TTS stuck ON with no way to disable it (the voice.toggle
tts handler requires voice mode to be ON).

Bug 2: TUI status bar only showed 'voice on/off' without any
indication of whether TTS speech output is active, because the
frontend never tracked voiceTts state.

- tui_gateway/server.py: clear HERMES_VOICE_TTS when voice is turned off
- ui-tui/src/app/useMainApp.ts: add voiceTts state, thread setVoiceTts
  through voice contexts, display [tts] in status bar
- ui-tui/src/app/slash/commands/session.ts: sync tts from voice.toggle response
- ui-tui/src/app/interfaces.ts: add setVoiceTts to all voice context interfaces
2026-05-23 16:21:29 -07:00
Brooklyn Nicholson
511b8e2325 fix(tui): re-check sticky inside resize debounce + document remount
Addresses Copilot review on PR #31077:

- onResize now re-checks isSticky() inside the 100ms timer so manual
  scrolls during the debounce window don't get snapped back to tail.
- Comment on the virtualRows cols-keying calls out the deliberate
  trade-off: per-row local state (e.g. systemOpen) resets on resize so
  yoga can remeasure off live geometry. The hook's scale-by-ratio path
  is too approximate for mixed markdown widths.
2026-05-23 17:48:28 -05:00
Brooklyn Nicholson
35fdf11145 feat(tui): responsive banner tiers
Terminals can't scale glyphs, so the banner now picks a layout per
column width instead of always rendering the full 101-col logo:

- Wide (>= logo width): full ASCII logo + tagline.
- Mid (>= 58 cols): centered rule banner that expands with viewport.
- Narrow (>= 34 cols): brand line + tagline, both width-aware.
- < 34 cols: hidden.

SessionPanel surfaces model/cwd/sid inline when the hero column is
hidden, so narrow layouts don't lose that info. Logo width constants
derive from the art itself.
2026-05-23 17:37:51 -05:00
Brooklyn Nicholson
0277194e3b fix(tui): preserve transcript tail across resizes
Wraps + heights are column-dependent, so a width change must remeasure
every row and the renderer must repaint the full viewport.

- Key virtualRows on cols so React remounts wrapped rows on resize.
- Snap back to bottom after sticky-mode resize once React rerenders.
- Reserve a scrollbar + gap column in transcriptBodyWidth (non-termux).
- Full repaint on any viewport height change (was: shrink-only).
- ScrollBox scrollHeight uses deepest child bottom so sticky-bottom
  math can reach the real final rendered row after reflow.
- DECSTBM fast-path now requires full container rect match.
2026-05-23 17:37:42 -05:00
Brooklyn Nicholson
2a75bec607 fix(tui): recompute virtual tail after width resize
Avoid preserving a frozen virtual transcript range when wrapped rows shrink enough that the old tail window no longer covers the viewport.
2026-05-23 14:49:26 -05:00
Brooklyn Nicholson
521c870a05 docs(tui): clarify scrollbox subscription signals
Document that ScrollBox subscribers are notified for renderer-computed viewport and content bound changes, not only imperative scrolls.
2026-05-23 14:18:56 -05:00
Brooklyn Nicholson
a4c27af697 fix(tui): measure status cwd by display width
Budget the right-hand status label by terminal display width so wide Unicode paths cannot wrap skinny status bars.
2026-05-23 14:11:44 -05:00
Brooklyn Nicholson
d1ad919a44 test(tui): clarify virtual history resize snapshot
Update the resize regression and comments so the test specifically guards viewport-height changes in the virtual-history snapshot key.
2026-05-23 14:11:44 -05:00
Brooklyn Nicholson
4d9791c551 fix(tui): reclaim status width when cwd is hidden
Make the cwd separator width conditional so the computed status layout matches the rendered row on ultra-narrow terminals.
2026-05-23 14:06:08 -05:00
Brooklyn Nicholson
cc61e3be49 test(tui): isolate viewport-height remount regression
Keep the resize delta below the virtual history scroll quantum so the regression test specifically depends on viewport height entering the snapshot key.
2026-05-23 14:06:08 -05:00
Brooklyn Nicholson
11b0d9ed2f fix(tui): keep status rule one-line in skinny terminals
Clamp and truncate the cwd/branch segment so narrow status bars cannot wrap into the composer input row.
2026-05-23 13:47:35 -05:00
Brooklyn Nicholson
4fea02cc16 fix(tui): refresh virtual transcript on viewport resize
Notify scroll subscribers when ScrollBox viewport bounds change and key virtual-history updates on viewport height so resize/keyboard changes remount the tail rows instead of leaving stale spacers visible.
2026-05-23 13:41:46 -05:00
brooklyn!
874c2b1fe6
fix(tui): ignore late thinking deltas after completion (#31055)
* fix(tui): ignore late thinking deltas after completion

Prevent stale reasoning events from repainting the TUI status after a turn has already completed and the UI is idle.

* test(tui): restore timers after thinking delta assertion

Keep fake timer cleanup in a finally block so assertion failures cannot leak timer mode into later tests.
2026-05-23 13:31:06 -05:00
brooklyn!
e6ca730a22
fix(tui): log parent gateway lifecycle exits (#31051)
* fix(tui): log parent gateway lifecycle exits

Add parent-side breadcrumbs for TUI gateway shutdown and transport exits so future backend EOF/SIGTERM reports identify the parent action that caused them.

* chore(tui): retrigger lifecycle logging checks

Retry transient GitHub checkout failures on the lifecycle logging PR.
2026-05-23 13:28:40 -05:00
brooklyn!
026f64f8e0
fix(tui): commit composer input bursts immediately (#31053)
* fix(tui): commit composer input bursts immediately

Salvage the WSL/terminal multi-character input burst fix with focused regression coverage so delayed pseudo-paste buffers cannot reorder later edits.

* fix(tui): keep newline input bursts on paste path

Preserve paste handling for multi-character chunks with newlines while keeping repeated printable key bursts on the immediate composer path.

* refactor(tui): share composer frame batch interval

Use one frame-sized batching constant for parent updates, local renders, and input burst flushes.
2026-05-23 13:27:16 -05:00
brooklyn!
1264fab156
fix(tui): surface verbose tool details (#30225)
Some checks failed
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Docker Build and Publish / move-latest (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Waiting to run
Nix Lockfile Fix / fix (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / e2e (push) Waiting to run
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
* fix(tui): surface verbose tool details

Emit redacted structured verbose args/results to the TUI so /verbose verbose can show full tool detail without reopening stdout, and fail closed if redaction is unavailable.

Salvages #29011.

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>

* fix(tui): address verbose detail review

Label verbose tool failures as errors, cover forced verbose reasoning, and avoid new diff type warnings from the redaction regression tests.

* fix(tui): bound verbose tool payloads

Cap verbose tool detail text before emitting JSON-RPC events and preserve verbose results on inline diff completions.

* fix(tui): align termux argv test with gc flag

Update the stale TUI launch expectation so the Termux freshness path matches the current direct Node argv.

---------

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>
2026-05-22 00:16:52 -05:00
adybag14-cyber
d08c2a016a fix(tui): termux-gate composer rendering tweaks for Ink TUI
Salvaged from #28942 (adybag14-cyber). Only the Ink TUI half is taken
here — the bundled "termux compatibility note" added to skills_tool.py
in the original PR did not address the actual user-reported bug
(skill_matches_platform() filtering Linux skills out on Termux) and
also regressed the EXCLUDED_SKILL_DIRS set used to prune nested
.venv/site-packages skills.

Changes:
- ui-tui/src/lib/prompt.ts: single-cell ASCII '>' marker in Termux mode
  to avoid ambiguous-width glyph artifacts while typing.
- ui-tui/src/components/appLayout.tsx: suppress profile prefix on
  narrow Termux panes (>=90 cols still shows it).
- ui-tui/src/lib/inputMetrics.ts + components/messageLine.tsx +
  lib/virtualHeights.ts: termux-aware transcript body width — drop
  the desktop 20-col floor on narrow mobile layouts, align virtual
  heights with actual rendered width.
- ui-tui/src/components/textInput.tsx: disable fast-echo bypass by
  default in Termux to avoid ghosting at soft-wrap boundaries.
  HERMES_TUI_TERMUX_FAST_ECHO=1 opts back in.

Tests: ui-tui/src/__tests__/{prompt,termuxComposerLayout,textInputFastEcho}.test.ts
(12 PR-added tests pass; 3 pre-existing wrapAnsi-bundling failures on
main are unrelated.)

The real skill-listing fix on Termux ('android' platform matching
Linux skills) ships as a follow-up commit on this branch.
2026-05-21 19:08:38 -07:00
brooklyn!
9e30ef224d
fix(tui): preserve scrollback when branching sessions (#30162)
Keep the visible transcript mounted after /branch switches to the new session, since the backend already carries the copied history forward.
2026-05-21 21:01:04 -05:00
brooklyn!
a7cd254c29
feat(tui): mouse_tracking DEC mode presets (salvage of #26681) (#30084)
* feat(tui): make display.mouse_tracking pick which DEC modes to enable

Previously the boolean flag was all-or-nothing across modes 1000+1002+1003+1006.
Inside tmux, mode 1003 (any-motion) makes every mouse cross of the prompt row
fire a clipboard probe that surfaces as "No image in clipboard" — sometimes
dozens in a row. Disabling tracking entirely killed scroll-wheel scrolling too,
since tmux's own scrollback is preempted by the alt-screen TUI.

`display.mouse_tracking` (and `/mouse <preset>`) now accepts `off | wheel |
buttons | all` in addition to the legacy booleans. `wheel` is 1000+1006:
scroll wheel + click only, no drag, no hover — the tmux-friendly subset.
`buttons` adds 1002 for drag-to-select. `all` (= legacy `true`) keeps the
hover-driven UI (scrollbar paginate-on-hover, link mouseenter, etc.).

* fix(tui): repaint + sync mouse mode when display.mouse_tracking changes

Two interacting bugs left the TUI blank when `display.mouse_tracking`
switched at runtime (config edit, /mouse <preset>):

1. AlternateScreen's effect re-runs on every `mouseTracking` change,
   tearing down and re-entering the alt screen. After re-entry, ink's
   frame buffers are reset by `resetFramesForAltScreen()` but nothing
   schedules the follow-up render — the alt screen sits blank until
   some other state change happens to trigger one. Add a
   `scheduleRender()` in `setAltScreenActive`'s active=true branch so
   the freshly-entered alt screen gets a full repaint immediately.

2. `setAltScreenActive` early-returns when `active` hasn't changed,
   which silently drops a `mouseTracking` change if the cleanup→setup
   pair somehow leaves `altScreenActive` already true. Call
   `setAltScreenMouseTracking` explicitly from the AlternateScreen
   effect so the in-memory mode and terminal DECSET sequence stay in
   sync regardless of how `setAltScreenActive` resolved (the call is a
   no-op when the mode is unchanged).

* fix(tui): address copilot review #4341269705

- tui_gateway/server.py: drop the never-referenced _MOUSE_TRACKING_MODES
  frozenset (comment #3284802434). _MOUSE_TRACKING_ALIASES already
  centralizes the canonical preset set via its values; the separate
  constant added no behavior.
- tests/test_tui_gateway_server.py: update the existing
  test_config_mouse_uses_documented_key_with_legacy_fallback to assert
  the new preset strings ('all'/'off' instead of 'on'/'off',
  display.mouse_tracking persisted as 'all' instead of True) and add
  test_config_mouse_accepts_preset_strings_and_aliases covering /mouse
  set with wheel/click/unknown (comment #3284802453). The on/off legacy
  config.set return shape was an implementation detail of the boolean
  flag, not a stable API — the slash command, gateway help text, and
  docs all advertise the preset values now.
- ui-tui/packages/hermes-ink/src/ink/ink.tsx: schedule a render at the
  end of reenterAltScreen() (comment #3284802461). Mirrors the same fix
  in setAltScreenActive() from ece0a2f4c — without it, SIGCONT/resize
  self-heal/stdin-gap re-entry leaves the alt screen blank because
  every caller returns early after invoking us.

* fix(tui): address copilot review #4341308478 round 2

- ui-tui/src/config/env.ts (comment #3284837577): the precedence
  comment was misleading. Actual behavior on origin/main is
  HERMES_TUI_MOUSE_TRACKING (explicit override) > Termux default >
  HERMES_TUI_DISABLE_MOUSE legacy kill-switch. This is preserved from
  main; the only change here was the wrong comment that claimed
  DISABLE_MOUSE kept kill-switch semantics. Rewrote the comment block
  to document the actual precedence ladder.
- tui_gateway/server.py /mouse set (comment #3284837607): replaced
  'str(value or "").strip().lower()' with the explicit None idiom
  already used for /indicator, so programmatic callers can pass 0 /
  False and have them route through _MOUSE_TRACKING_ALIASES → 'off'
  instead of collapsing to '' and triggering the toggle path.
- ui-tui/packages/hermes-ink/src/ink/components/AlternateScreen.tsx
  (comment #3284837620): always prepend DISABLE_MOUSE_TRACKING before
  enableMouseTrackingFor(...) on mount. Otherwise selecting
  'wheel'/'buttons' from a state where DEC 1003 was already asserted
  (crash, another app, debugger) would silently leave hover on. Also
  unconditionally DISABLE on unmount so a crash mid-mount can't leak
  DEC modes back to the host shell.

* chore(release): map nat@nthrow.io to @nthrow for #26681 salvage

* fix(tui): drop redundant setAltScreenMouseTracking in AlternateScreen

Copilot review #4341356637 (comment #3284880417). The explicit
setAltScreenMouseTracking(mouseTracking) after setAltScreenActive(true,
mouseTracking) was defensive paranoia added in the previous fix commit
that's not actually reachable in practice:

- React's cleanup always runs before the next setup, so on any prop
  change (mouseTracking or writeRaw) the cleanup sets active=false
  first. Setup then sees active was false and applies the new mode
  via setAltScreenActive without early-returning.
- On the impossible 'active stayed true' path, the writeRaw above has
  already sent DISABLE_MOUSE_TRACKING + enableMouseTrackingFor(newMode)
  to the terminal, so the in-memory mode would lag but the visible
  state is already correct.

Removing the redundant call means a single DEC sequence per mount.
If the 'active stayed true' path ever manifests in practice, the
right fix is in setAltScreenActive (track mode regardless of the
active early-return), not here.

* fix(tui): always DISABLE before enableMouseTrackingFor in ink.tsx

Copilot review #4341379994 (comments #3284900825, #3284900840,
#3284900852). Three remaining call sites in ink.tsx still re-enabled
mouse tracking without first sending DISABLE_MOUSE_TRACKING:

- handleResize alt-screen recovery (line ~577)
- reassertTerminalModes stdin-gap re-assertion (line ~1351)
- reenterAltScreen SIGCONT/resize/stdin-gap self-heal (line ~1408)

For 'wheel'/'buttons' presets, omitting DISABLE leaves any externally-
asserted DEC 1003 (other apps, prior crash, tmux state) still active
and the hover-free preset silently has hover on. DISABLE_MOUSE_TRACKING
is idempotent and safe to send unconditionally — it resets all four
modes. Matches the pattern already in setAltScreenMouseTracking and
the AlternateScreen mount path.

* fix(tui): always DISABLE before enableMouseTrackingFor in exitAlternateScreen

Copilot review #4341452823 (comment #3284959762). exitAlternateScreen()
was the last call site in ink.tsx still re-enabling mouse tracking
without DISABLE first. Editors (vim/nvim/less) and tmux can leave
DEC 1003 hover asserted across the handoff back; without DISABLE,
'wheel'/'buttons' presets silently kept hover on after the editor
quit. Now all five enableMouseTrackingFor() call sites in ink.tsx
prepend DISABLE_MOUSE_TRACKING — handleResize, reassertTerminalModes,
reenterAltScreen, setAltScreenMouseTracking, exitAlternateScreen.

* fix(tui): add defensive default to enableMouseTrackingFor switch

Copilot review #4341485231 (comment #3284979323). TS exhaustive switch
returns string per the type system, but a JS caller / corrupted config
/ hot-reload-in-dev could reach the function with an unknown value at
runtime. Without a default, that path returns undefined which then
concatenates as the literal string 'undefined' into the terminal byte
stream — visibly garbling output. Treat unknown as 'off' (no DEC
sequences) so the worst case is silent input loss rather than a
wrecked screen.

---------

Co-authored-by: Nat Thrower <nat@nthrow.io>
2026-05-21 20:25:52 -05:00
ethernet
2fdefca570
Merge pull request #28269 from cresslank/chore/tui-remove-unused-babel-deps
chore(tui): remove unused Babel build deps
2026-05-21 10:21:31 -04:00
ethernet
1566d71726
Merge pull request #29342 from NousResearch/fix/tui-linux-copy
fix(tui): clipboard copy on linux/wayland
2026-05-20 21:40:37 -04:00
ethernet
c42edd8055 fix(tui): clipboard copy on linux/wayland
`probeLinuxCopy` and `copyNative` in `osc.ts` await `execFileNoThrow`
for wl-copy / xclip / xsel. Those tools double-fork a daemon that
holds the system selection live, and the daemon inherits stdio pipes
from `spawn(stdio: 'pipe')`. Node's 'close' event only fires when
stdio is fully closed → the daemon keeps the pipes open → 'close'
never fires → the await leaks past the timeout (kill(SIGTERM) on an
already-exited child is a no-op, daemon survives).

Result: `linuxCopy` cache stays `undefined` permanently, the actual
copy never runs, ctrl-c silently does nothing on wayland/x11.
Reproduced in isolation, confirmed across wl-copy and a
daemonization-shaped fixture.

Fix: add `resolveOnExit` option to `execFileNoThrow`. When set, the
promise settles on the immediate child's 'exit' event instead of
waiting for stdio drainage. Wired into both the probe and the actual
copy spawns for every clipboard tool (pbcopy, wl-copy, xclip, xsel,
clip).

Tests: 5 new vitest cases covering daemon-style child handling,
non-zero exit propagation, timeout behavior, and double-resolve
guard. The forever-hang case is committed as `it.skip` with
documentation so a reviewer can verify the bug by hand.
2026-05-20 19:47:30 -04:00
cresslank
8ad34db551 chore(tui): remove unused Babel build deps
Remove the stale Babel compiler config and direct Babel dev dependencies from the TUI package.

Regenerate the npm lockfile and refresh the Nix fetchNpmDeps hash for the trimmed dependency graph.
2026-05-20 17:23:36 -05:00
brooklyn!
88f5186d35
fix(tui): anchor splitReasoning unclosed-tag regex to start of input (#29426)
`splitReasoning()` strips paired `<think>…</think>` blocks first, then runs
an unclosed-trailing regex to catch reasoning that hasn't yet streamed its
closer. That second regex was unanchored and greedy:

    new RegExp(`<${tag}>([\\s\\S]*)$`, 'i')

So any literal `<think>` somewhere in prose — a model quoting the tag, a
code example, or a stream-mid-tag before the closer arrives — consumed
every paragraph after it to EOF. User-visible symptom: "TUI eats last
paragraph of output," both during streaming and on settled turns.

Real reasoning streams always lead the message (that's the only place an
unclosed opener can legitimately appear during streaming). Anchor the
regex to `^\s*` so mid-prose mentions of the tag are preserved.

Empirical repro before the fix:

    splitReasoning('final answer paragraph one.\n\n<think>internal note\n\nfinal answer paragraph two.')
    → text: 'final answer paragraph one.'        ← paragraph two GONE

After:

    → text: 'final answer paragraph one.\n\n<think>internal note\n\nfinal answer paragraph two.'

Updated the existing trailing-unclosed test to lead with `<think>` (the
real-world shape) and added a regression test pinning the mid-text case.

ui-tui type-check clean, 808/808 vitest pass.
2026-05-20 14:09:38 -05:00
H-Ali13381
697d38a3f4 feat: auto-launch Chromium-family browser for CDP
Add browser CDP launch candidates for Chrome, Chromium, Brave, and Edge while preserving Chrome-first selection. Retry candidate launch failures instead of giving up after the first executable.

Update /browser CLI and TUI messaging, docs, and tool descriptions from Chrome-only wording to Chromium-family browser support. Add regression coverage for Brave/Edge paths, Chrome-first precedence, fallback launches, and CDP endpoint probing.
2026-05-19 22:34:05 -07:00
dependabot[bot]
aa4e49275e
chore(deps): bump ws from 8.20.0 to 8.20.1 in /ui-tui
Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.20.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.20.0...8.20.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 21:37:58 +00:00
adybag14-cyber
7c2ff742a4 fix(tui): termux-gate scrollback preservation, touch-friendly defaults
Adds a Termux runtime detection helper and gates three TUI defaults on it:

- Skip the startup scrollback clear on Termux so users can review/copy
  earlier output after reopening the app. Desktop keeps the existing
  \x1b[2J\x1b[H\x1b[3J slate (AlternateScreen takes over there anyway).
- Default INLINE_MODE on under Termux: primary-buffer rendering makes
  long-thread review and copy/paste much less fragile when users
  background/foreground the app. Override with HERMES_TUI_INLINE=0/1.
- Default mouse tracking off under Termux so touch selection isn't
  intercepted by terminal mouse protocols. Explicit override via
  HERMES_TUI_MOUSE_TRACKING=0/1; legacy HERMES_TUI_DISABLE_MOUSE still
  works on desktop.

Detection is purely env-based (TERMUX_VERSION or PREFIX path) with an
explicit opt-out HERMES_TUI_TERMUX_MODE=0 for debugging. Non-Termux
platforms keep every existing default.

Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com>
2026-05-19 12:49:23 -07:00
brooklyn!
b0af1d0931
Merge pull request #28829 from NousResearch/bb/tui-no-history-truncation
fix(tui): render full assistant text in scrollback (no history truncation)
2026-05-19 12:17:35 -05:00
dependabot[bot]
a3ef21793e
chore(deps): bump ws in /ui-tui/packages/hermes-ink (#28183)
Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.20.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.20.0...8.20.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 09:17:19 -04:00
helix4u
6cac56f314 fix(tui): preserve dunder identifiers in markdown 2026-05-19 00:06:08 -07:00
burjorjee
8c3b065124 fix(cli): show active profile in TUI prompt 2026-05-19 00:05:20 -07:00
YuanHanzhong
7321b3c2db fix(tui): keep x status citation fallbacks link-like 2026-05-18 20:01:58 -07:00
Austin Pickett
2ef501e1f5
feat(cli): add /update slash command to CLI and TUI (#23854)
* feat: add /update slash command to CLI and TUI

* test(cli): add Python tests for /update slash command

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address Copilot review for /update slash command

Route classic CLI /update through prompt_toolkit modal confirmation and
defer relaunch to the main-thread cleanup path after app.exit(). Tighten
Y/n semantics, add Python wrapper and catalog coverage tests, and assert
/update stays visible in the TUI command catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address review feedback on /update command

- Replace raw input() with _prompt_text_input_modal in _handle_update_command
  to avoid EOF/hang/keystroke-leak races with prompt_toolkit's stdin ownership
- Fix confirmation logic: only proceed on recognized affirmative aliases
  (y/yes/1/ok); cancel on everything else including empty string, typos,
  and unrecognized input — matches all other [Y/n] prompts in the codebase
- Route relaunch through main-thread shutdown path: set _pending_relaunch
  and return False from process_command so process_loop triggers app.exit();
  run() then calls relaunch() after prompt_toolkit has restored terminal modes
  and after cleanup — safe on both POSIX (execvp) and Windows (subprocess+exit)
- Fix misleading docstring in test_update_command.py: the Vitest only covers
  the TypeScript slash handler that emits code 42, not the Python wrapper
  branch that acts on it
- Rewrite tests to use SimpleNamespace pattern (like test_destructive_slash_confirm)
  so _prompt_text_input_modal can be stubbed directly
- Add Python test for _launch_tui exit-code-42 → relaunch branch in main.py

Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb

Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>

* fix(cli): polish test fixtures for /update command

- Remove unused _prompt_text_input from SimpleNamespace stub
- Use pytest.fail sentinel in managed-install guard test to catch unexpected modal invocations

Agent-Logs-Url: https://github.com/NousResearch/hermes-agent/sessions/f6da68cf-e7b1-4b7a-aed6-3d4b0f523bdb

Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>

* chore: re-trigger CI after Copilot review fixes

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: austinpickett <260188+austinpickett@users.noreply.github.com>
2026-05-18 20:10:46 -04:00
Austin Pickett
609c485fc6
Merge pull request #27971 from NousResearch/austin/fix/goal-statusbar
fix(tui): keep /goal verdict out of compact status row
2026-05-18 08:42:33 -04:00
Brooklyn Nicholson
caac54796b chore: revert unrelated package-lock + nix hash churn to keep PR diff minimal 2026-05-17 13:33:10 -05:00
Brooklyn Nicholson
711f46e4bd review(tui): update stale comment refs to renamed visualLines helper 2026-05-17 12:32:29 -05:00
Brooklyn Nicholson
8c78f533dd review(tui): route cursorLayout through @hermes/ink wrapAnsi shim (Bun runtime parity)
Copilot caught an important runtime parity gap on PR #27489: the fix
imported the npm `wrap-ansi` package directly, but Ink's `<Text
wrap="wrap">` uses a runtime-selecting shim
(`ui-tui/packages/hermes-ink/src/ink/wrapAnsi.ts`) that prefers
`Bun.wrapAnsi` when running under Bun and falls back to the npm package
elsewhere. So under Bun, Ink would render via `Bun.wrapAnsi` while
`cursorLayout` would compute breaks via the npm package — any
disagreement reintroduces the exact cursor-drift symptom the PR is
meant to eliminate.

Fix:

- Export `wrapAnsi` from `@hermes/ink` (`packages/hermes-ink/src/entry-exports.ts`
  and `packages/hermes-ink/index.d.ts`) so the shim is the public surface.
- Switch `ui-tui/src/lib/inputMetrics.ts` from `import wrapAnsi from
  'wrap-ansi'` to `import { wrapAnsi } from '@hermes/ink'`. Both
  renderer (Ink) and cursor layout now traverse the same shim, so
  they share the runtime-selected implementation by construction.
- Same swap in `textInputWrap.test.ts` and `cursorDriftRegression.test.ts`
  — tests now assert parity through the shim, which means under Bun
  they actually exercise Bun's implementation instead of asserting a
  tautology against the npm package.
- Drop the direct `"wrap-ansi": "^9.0.0"` from `ui-tui/package.json`.
  `@hermes/ink` (which IS a declared dep) pulls wrap-ansi in
  transitively — that's not a phantom dep because the import path
  goes through `@hermes/ink`'s public exports, not through a
  hoisting accident.

Verified: 791/791 vitest tests pass. `@hermes/ink` rebuilt
(`dist/entry-exports.js` includes `wrapAnsi` export). TUI bundle
rebuilt clean.
2026-05-17 11:52:21 -05:00
Brooklyn Nicholson
1c0e59e557 review(tui): address Copilot feedback on cursorLayout wrap-ansi rewrite
Three small follow-ups from the Copilot review on #27489:

1. Declare `wrap-ansi` as a direct dependency of `ui-tui`. It was a
   phantom dep that resolved via npm hoisting from `@hermes/ink`'s
   transitive graph — fine on hoisted installs, but breaks under pnpm
   or `npm install --no-install-strategy=hoisted` style isolated
   installs. Now listed as `"wrap-ansi": "^9.0.0"` matching the
   @hermes/ink version. Lockfile regenerated.

2. Implement the defensive resync the comment promised. Previously the
   comment claimed the loop would "fall back to advancing by one to
   stay in lockstep" on wrap-ansi desync, but the code unconditionally
   advanced `originalIdx` with no actual check — so any future
   wrap-ansi option change or styled-input caller could silently slide
   `originalIdx` past the end of `value` and emit garbage line ranges.
   Now actually compares `value[originalIdx] === ch`, re-syncs via
   `indexOf` on mismatch, and bails out (returning whatever was built
   so far) if the desync is unrecoverable. Production paths still hit
   the equality fast-path on every char.

3. Drop the `visualLines` wrapper. It was a one-line indirection over
   `visualLinesFromWrappedOutput`. Renamed the implementation to
   `visualLines` and removed the wrapper — same name, no extra layer.

No behavior change beyond the defensive realign; all 791 vitest tests
still pass.
2026-05-17 11:34:06 -05:00
Brooklyn Nicholson
3b4dd68326 fix(tui): align composer cursorLayout with wrap-ansi to kill multiline cursor drift
The composer's `cursorLayout` (in `ui-tui/src/lib/inputMetrics.ts`) used a
hand-rolled word-wrap algorithm to decide where `useDeclaredCursor`
should park the hardware cursor. But Ink's `<Text wrap="wrap">` renders
the same text via `wrap-ansi`. The two algorithms disagreed on common
real-world inputs — `"branch investigate"` at cols=20, `"hello world"`
at cols=8, exact-fill strings like `"abcdefgh"` at cols=8 — so the
hardware cursor parked several cells past where Ink actually rendered
the last character. Users saw a multi-cell blank gap between their
last-typed letter and the cursor block, especially on narrow terminals
(the Cursor IDE built-in terminal was the worst offender).

Three previous PRs (#26717, #25860, #22197) chased fast-echo
displayCursor/cursorDeclaration drift and in-band-vs-native cursor
heuristics. None of them touched the underlying wrap-algorithm
mismatch, which is why the bug kept resurfacing.

Fix: source cursorLayout's line breaks from wrap-ansi directly. Walk
its emitted string char-by-char, tracking original-string offsets, push
a VisualLine at each '\n'. Also drop the buggy `column >= w` overflow
rule in cursorLayout — that's what pushed exact-fill text onto a
phantom next row.

canFastBackspaceShape now detects the wrap boundary in BOTH coordinate
conventions (column === 0 OR column >= columns), since exact-fill now
reports as (0, columns) instead of the previous (1, 0). The physical
state is identical — the terminal auto-wraps at column N either way —
but the layout function reports the position more honestly.

Tests:
- ui-tui/src/__tests__/textInputWrap.test.ts: 3 tests that pinned the
  BUGGY behavior were updated to assert wrap-ansi parity (the real
  invariant). Added a typing-prefix invariant: cursorLayout must agree
  with wrap-ansi at every character of a long input.
- ui-tui/src/__tests__/cursorDriftRegression.test.ts: new file. Walks
  the user-reported bug message char-by-char at 7 widths and asserts
  agreement with wrap-ansi at every prefix.

Verification:
- 791/791 vitest tests pass.
- 84/84 tui-gateway pytest tests pass via scripts/run_tests.sh.
- PTY repro (typing into a real `hermes --tui` PTY at cols=50/55/60):
  cursor lands exactly 1 cell past the last typed char in every case
  the bug previously drifted.
2026-05-17 11:10:06 -05:00
Brooklyn Nicholson
a65f723e68 fix(review): address Copilot follow-up on sanitizer and file decode errors
Consume multi-byte non-CSI ESC sequences during ANSI sanitization and handle UnicodeDecodeError for `hermes send --file` so review findings are resolved without regressions.
2026-05-16 23:00:58 -05:00
Brooklyn Nicholson
7e1788db5d fix(tui): harden ansi sanitizers for dangling CSI
Strip incomplete CSI prefixes before rendering, remove carriage returns from sanitized output, and add regression tests to prevent escape-sequence recomposition across message boundaries.
2026-05-16 22:58:00 -05:00
Brooklyn Nicholson
290bf93104 fix(tui): harden Terminal.app render behavior
Avoid Terminal.app paint corruption by disabling fast-echo in that terminal, sanitizing non-SGR control sequences before ANSI rendering, and defaulting Apple Terminal back to the safer 256-color path unless truecolor is explicitly requested.
2026-05-16 22:51:51 -05:00
brooklyn!
70b663504f
fix(tui): keep Ink displayCursor in sync with fast-echo writes so cursor stops drifting (#26717)
* fix(tui): keep Ink displayCursor in sync with fast-echo writes so cursor stops drifting

TextInput's fast-echo bypass writes characters directly to stdout to
avoid waiting on a React re-render for each keystroke. The hardware
cursor advances by text.length cells, but Ink's cached `displayCursor`
(the basis for the next frame's relative cursor-move preamble in
log-update) stayed unchanged. When ANY unrelated component re-rendered
between the fast-echo write and the deferred composer setCur/setParent
flush — status bar timer, streaming reasoning, etc. — the next frame's
preamble emitted a relative cursor move from a stale parked position
and the hardware cursor parked N cells offset from the actual caret.

Visible symptom: extra whitespace between the just-typed character and
the cursor block, intermittent, worse on long sessions during streaming.
Alt-screen was immune because frames begin with absolute CSI H.

This adds a small API in @hermes/ink:

  - `Ink.noteExternalCursorAdvance(dx, dy?)` — bumps displayCursor if
    set, otherwise seeds from frontFrame.cursor so the next preamble's
    relative move correctly cancels the external advance. No-op on
    alt-screen.
  - `CursorAdvanceContext` + `useCursorAdvance()` hook to expose it.

TextInput then calls `noteCursorAdvance(text.length)` after the
fast-echo `stdout.write(text)` append, and `noteCursorAdvance(-1)`
after the fast-backspace `\b \b` sequence.

Tests: 4 new vitest cases pin the API contract (bumps when set, seeds
from frontFrame.cursor when null, alt-screen no-op, zero-delta no-op).
All 751 ui-tui tests pass; tests/test_tui_gateway_server.py (177) pass.

* fix(tui): also advance cursorDeclaration so fast-echo survives deferred React state

Copilot review on PR #26717 flagged a gap in the original fix:
TextInput's fast-echo path defers the React `cur` state update by
16ms (perf optimization that batches re-renders during heavy typing).
Inside that window, `useDeclaredCursor` still publishes a target
computed from the PRE-keystroke `cur` — `cursorLayout(display, cur,
columns)`. Advancing only `displayCursor` would let any unrelated
re-render in that 16ms window run onRender's cursor-park branch with
the stale declaration and visually undo the fast-echo's advance.

The fix is symmetric: `noteExternalCursorAdvance` now bumps BOTH
`displayCursor` (the log-update relative-move basis) AND, if non-null,
`cursorDeclaration.relativeX/Y` (the target the cursor parks at after
every frame). When React finally flushes `setCur`, `useDeclaredCursor`
publishes a fresh declaration that supersedes our bumped one — exactly
what we want.

Adds two new vitest cases covering both halves:
  - active declaration advances in lock-step with displayCursor
  - null declaration stays null (no spurious bump)

All 753 ui-tui tests pass; tests/test_tui_gateway_server.py (177) pass.

Closes review threads:
  PRRT_kwDOPRF1G86ChKtD (textInput.tsx:1016 fast-echo append)
  PRRT_kwDOPRF1G86ChKtF (textInput.tsx:924 fast-backspace)
  PRRT_kwDOPRF1G86ChKtG (ink-cursor-advance.test.ts:57 missing coverage)

* fix(tui): make fast-echo survive TextInput rerenders + alt-screen (Copilot round 2)

Round 2 of PR #26717 review. Three real holes Copilot flagged after the
initial cursorDeclaration bump:

1. alt-screen early-return skipped BOTH halves of the notifier. But the
   default TUI wraps the composer in <AlternateScreen> — that IS the
   production path. CSI H resets log-update's relative-move basis, but
   the alt-screen park branch uses absolute CUP =
   `rect.x + decl.relativeX`, so a stale declaration there still parks
   the cursor at the pre-keystroke caret. Fix: skip ONLY the
   displayCursor half on alt-screen; still bump cursorDeclaration.

2. TextInput's own rerender could clobber the Ink-level bump. The fast-
   echo path defers setCur by 16ms; if a parent state change rerenders
   TextInput in that window, the layout effect inside useDeclaredCursor
   reads the stale React `cur` state and re-publishes a declaration at
   the OLD column. Fix:
   `cursorLayout(display, curRef.current, columns)` — read the always-
   up-to-date ref, not the deferred state. useMemo dropped (compute is
   cheap, single-line wrap-text in the common case).

3. Tests bypassed the production wiring. Added two structural tests:
   - `still advances cursorDeclaration on alt-screen` in the Ink-level
     suite, asserting displayCursor stays put but the declaration
     advances by the delta.
   - `textInputCursorSourceOfTruth.test.ts` pins three structural
     invariants: layout reads curRef.current, never the bare `cur`
     state, and the fast-echo stdout.write calls remain paired with
     noteCursorAdvance(±N). Source-grep invariants > flaky Ink mount
     tests for this kind of regression.

757/757 ui-tui tests pass (+3 over round 1). type-check clean. lint
introduces zero new errors on touched files. tests/test_tui_gateway_server.py
(177) pass.

Closes review threads:
  PRRT_kwDOPRF1G86ChOG2 (ink.tsx alt-screen guard)
  PRRT_kwDOPRF1G86ChOG9 (textInput.tsx fast-backspace rerender window)
  PRRT_kwDOPRF1G86ChOHC (textInput.tsx fast-append rerender window)
  PRRT_kwDOPRF1G86ChOHJ (alt-screen test asserts wrong invariant)
  PRRT_kwDOPRF1G86ChOHP (missing integration-style coverage)

* fix(tui): reject fast-backspace at soft-wrap boundary (Copilot round 3)

PR #26717 round 3. Copilot caught two real things:

1. `\b \b` cannot move the terminal cursor onto the previous visual
   row across a soft-wrap boundary. When the caret sits at visual
   column 0 of a wrapped row (e.g. value 'hello ' at width 6 →
   cursorLayout produces (line 1, col 0)), backspace would leave the
   physical cursor in place while the logical caret moves up to the
   end of the previous visual line. `noteCursorAdvance(-1)` would then
   feed Ink a wrong delta. Fix: `canFastBackspaceShape` now takes the
   composer width and rejects when `cursorLayout(value, cursor, columns).column === 0`.
   The fast path falls through to the normal Ink render, which
   correctly lays out the new caret position. The PR-description
   inconsistency about alt-screen is fixed in a separate gh pr edit.

Adds 4 new tests in textInputFastEcho.test.ts pinning the rejection at
exact-multiple wrap boundaries plus a positive control inside a
wrapped line and a back-compat case where `columns` is omitted.

761/761 ui-tui tests pass. type-check / lint clean. 177/177 Python
tests/test_tui_gateway_server.py pass.

Closes review threads:
  PRRT_kwDOPRF1G86ChxE5 (textInput.tsx:933 wrap-boundary regression)

* fix(tui): polish doc + tests after Copilot round 4

Three polish points Copilot raised:

1. canFastBackspaceShape doc comment overstated the legacy contract —
   said it conservatively rejects potential wrap boundaries when
   columns is omitted, but the implementation actually skips the
   wrap-boundary check entirely. Reworded to make the legacy behavior
   explicit and warn callers not to rely on protection they don't get.

2. ink-cursor-advance.test.ts rationale comment for the
   'advances cursorDeclaration in lock-step' case still referenced
   the pre-fix `cursorLayout(display, cur, columns)` expression. Now
   accurately describes the current source of truth — `curRef.current`
   in textInput.tsx — and explains the window the bump is bridging.

3. Removed the three `__get*ForTest` accessors from Ink. The test
   file already cast the instance to inspect private state in the
   couple of tests that needed declaration mutation; the rest now use
   a small `peek(ink)` helper that does the same cast for reads. No
   test-only API surface ships in production.

761/761 ui-tui tests pass. type-check clean. lint introduces zero new
errors on touched files. 177/177 tests/test_tui_gateway_server.py pass.

Closes review threads:
  PRRT_kwDOPRF1G86Ch23W (canFastBackspaceShape doc accuracy)
  PRRT_kwDOPRF1G86Ch23f (stale test rationale)
  PRRT_kwDOPRF1G86Ch23p (test-only API surface in production)

* fix(tui): tighten doc + add dy test coverage (Copilot round 5)

Two polish points from round 5:

1. canFastBackspaceShape doc had two paragraphs that conflicted —
   the main 'Additionally rejects when the physical cursor sits at
   visual column 0' was stated unconditionally, then the columns-param
   paragraph qualified that it only happens when columns is passed.
   Reworked into clear 'When supplied / When omitted' branches with a
   concrete example value ('hello ' returns true without columns even
   though it would be unsafe at width 6). No more inconsistency.

2. Added a test asserting cursorDeclaration.relativeY advances when dy
   is non-zero. Existing tests exercised dy on displayCursor only.
   Newlines in fast-echoed text don't currently hit the bypass
   (canFastAppendShape rejects '\n'), but dy is part of the public
   notifier contract and must propagate symmetrically with dx so
   future callers get a fully-implemented contract.

762/762 ui-tui tests pass (+1). type-check / lint / build clean.

Closes review threads:
  PRRT_kwDOPRF1G86Ch6Sz (doc inconsistency)
  PRRT_kwDOPRF1G86Ch6TE (missing dy coverage on declaration)

* fix(tui): doc polish (Copilot round 6)

Four small but valid points:

1. textInputCursorSourceOfTruth.test.ts used bare 'fs'/'path'/'url'
   imports; the rest of ui-tui consistently uses the 'node:' prefix
   (see src/__tests__/useSessionLifecycle.test.ts, src/lib/editor.test.ts).
   Switched to node:fs / node:path / node:url to match convention.

2. CursorAdvanceContext.ts type-level doc described only displayCursor.
   The notifier intentionally also mutates the active cursorDeclaration
   and that's the only part that matters on alt-screen. Reworked the
   doc into a two-part 'updates both' summary with the alt-screen
   asymmetry called out explicitly.

3. use-cursor-advance.ts hook doc had the same problem. Same fix —
   document both pieces of state, both screen modes.

4. App.tsx onCursorAdvance prop comment was incomplete. Same fix —
   describe both state updates and the screen-mode asymmetry.

No behavior change. 762/762 ui-tui tests pass. type-check / lint /
build clean.

Closes review threads (auto-resolved on PR but valid critiques):
  PRRT_kwDOPRF1G86Ch926 (node: prefix on built-in imports)
  PRRT_kwDOPRF1G86Ch92_ (use-cursor-advance.ts doc)
  PRRT_kwDOPRF1G86Ch93H (CursorAdvanceContext.ts type doc)
  PRRT_kwDOPRF1G86Ch93J (App.tsx prop comment)
2026-05-16 00:28:12 -05:00
brooklyn!
44b63fc6de
fix(tui): allow transcript scroll + Esc during approval/clarify/confirm prompts (#26414)
When an approval / clarify / confirm overlay was active, the global input
handler in useInputHandlers returned for every key that wasn't Ctrl+C, which
silently disabled transcript scrolling. On long threads the context the
prompt was asking about often lived above the visible viewport, and being
unable to scroll while answering felt like the prompt had locked the UI.
ApprovalPrompt also had no Esc handler at all, so the one obvious 'abort'
key did nothing during a permission prompt and the user had to memorize
Ctrl+C or hunt for the deny number.

Fixes:

- Extract shouldFallThroughForScroll(key) (pure, exported) covering wheel
  scrolls, PageUp/PageDown, and Shift+ArrowUp/Down. When a prompt overlay
  is up and the pressed key is a scroll input, skip the early return so it
  reaches the existing wheel/PageUp/Shift+arrow handlers below. Plain
  arrows still drive in-prompt selection — they don't fall through.
- ApprovalPrompt now maps Esc to onChoice('deny'), parity with the global
  Ctrl+C cancellation path that already invokes cancelOverlayFromCtrlC()
  for approvals. The bottom-of-prompt hint now advertises 'Esc/Ctrl+C deny'.
- Extract approvalAction(ch, key, sel) — pure key-dispatch helper for the
  approval prompt, exported so the regression matrix (Esc, numbers, Enter,
  arrows, edge clamping, precedence) is testable without mounting Ink.

Tests:
- useInputHandlers.test.ts: 6 cases covering shouldFallThroughForScroll
  positives (wheel/PageUp/PageDown/Shift+arrows) and negatives (plain
  arrows, bare shift, no scroll key).
- approvalAction.test.ts: 8 cases covering Esc→deny, numeric mapping,
  Enter, ↑↓ within bounds, edge clamping, Esc-beats-others precedence,
  unrelated keystrokes.
2026-05-15 21:59:28 -05:00
Siddharth Balyan
55c9f32060
fix(tui): width-aware markdown table rendering with vertical fallback (#26195)
* refactor(tui): thread cols through Md/StreamingMd/renderTable, update cache key

* feat(tui): three-tier width calc + full-line string rendering in renderTable

Replaces the old renderTable (L203-244) with:
- Empty table guard
- Ragged row normalization
- Three-tier column width calculation (ideal → proportional shrink → hard scale)
- Rounding remainder distribution
- Full-line string rendering (one <Text> per row, not per cell)
- wrap=truncate-end on all table lines
- All cells rendered as plain text via stripInlineMarkup

No wrapping or vertical fallback yet — those come in Phase 3 and 4.

* feat(tui): wrapCell with grapheme-safe hard-break + multi-line row rendering

Adds:
- Intl.Segmenter-based grapheme splitting (fallback to [...word])
- wrapCell() for width-correct word wrapping on stripped text
- Multi-line row rendering with LineEntry metadata (header/separator/body)
- Post-render safety condition (maxLineWidth computed, vertical fallback in Task 4)
- Non-wrapping path preserved for tables that fit at ideal widths

* feat(tui): vertical key-value fallback with scaled threshold + safety check

Wires:
- Scaled row-height threshold (numCols<=3: 8, <=6: 5, else: 4)
- Post-render safety check (maxLineWidth > available space)
- Header-only edge case
- Vertical format: bold headers, stripped cell text, clamped separator width
- Iterates headers (not rows) for consistent key-value fields on ragged rows

* test(tui): pass cols to Md in test helpers, add width-overflow assertions

- renderAtWidth now passes cols={columns} to <Md> so width-aware code paths
  are exercised in tests
- tableFuzz: every rendered line must fit within allocated width (stringWidth)
- tableRepro: separator regex updated to match truncation ellipsis
- stringWidth imported from @hermes/ink for CJK-correct assertions

* fix(tui): address adversarial review — comment tier 3 budget overshoot, eliminate redundant wrapCell

- Add comment on Tier 3 MIN_COL_WIDTH clamp exceeding budget (self-heals via safetyOverflow)
- Track tallestBodyRow during allEntries build pass instead of re-wrapping every cell
  in a second traversal (eliminates O(cells) of redundant stripInlineMarkup+stringWidth)

* fix(tui): pass cols to recursive fenced-markdown Md, fix test frame extraction

- Thread cols into <Md> for fenced markdown blocks (L734) so nested
  tables use the width-aware renderer instead of max-content path
- Fix renderAtWidth helpers to extract final Ink repaint frame instead
  of concatenating all intermediate frames (REPAINT_RE split)
- Add fenced-markdown-table fixture to tableFuzz (exercises the nested path)

* chore: remove repro test suites and tmux driver script

These were scaffolding for development/reproduction — not needed in the PR.
2026-05-15 20:25:56 -05:00
brooklyn!
006937f7d0
fix(tui): handle timeout/error subagent statuses in /agents (#26687)
Accept delegation timeout/error statuses in the TUI subagent model, normalize unknown status strings defensively, and harden /agents overlay rendering/sorting so unknown statuses cannot crash glyph/color lookup. Add regression tests for live event normalization and disk snapshot replay.
2026-05-15 20:19:02 -05:00
brooklyn!
566d8f0d75
fix(tui): keep DECSTBM scroll region off bottom row (#26683)
Avoid shifting the terminal's last visible row in the alt-screen DECSTBM fast path, which can leave transient scroll bleed/discoloration artifacts around the status lane until a repaint. Add regression tests to preserve the fast path when safe and skip it when the hint touches the bottom row.
2026-05-15 20:08:24 -05:00