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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
* 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>
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.
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.
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)
- 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
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.
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.
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
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
Add voice.submit_mode=direct|draft without model-refine hooks or callbacks. Validate the config, preserve direct-submit compatibility, render editable drafts in the Ink composer, and document both locales.
Co-authored-by: BELIVIN MEDIA <212580280+KarateWilly@users.noreply.github.com>
Review follow-ups on the composite salvage (whole-bug-class sweep):
- session.branch and _persist_branch_seed copied parent history without
display_kind/display_metadata, so a tagged timeline marker (personality
pivot, model switch, auto-continue) re-entered the branched session as a
bare role=user row after a restart — re-planting the phantom-ordinal
class this PR fixes. Both projection dicts now carry the tags; regression
asserts added to both branch tests (mutation-checked: fail without the
fix).
- ui-tui renderer learns display_kind=personality_switch (was falling
through to an opaque user bubble; desktop got the case in commit 1).
- programmatic-integration docs: document the two new 4004 refusals
(boolean ordinal, bare confirm_truncate).
- hermes_state comment: archived rows are searchable only with
include_inactive=True, not by default search — align comment with the
actual FTS filter.
- strip stray trailing blank line in test_tui_gateway_server.py
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).
PowerShell's Get-Clipboard -Raw outputs text in the system's ANSI codepage
(e.g. CP1252, CP936), not UTF-8. When Node.js reads this with encoding: 'utf8',
non-ASCII characters (CJK, emoji, accented chars) are corrupted.
The write path already solved this by base64-encoding UTF-8 bytes and passing
them via -Command argument (see comment at line 94-98). This fix applies the
same approach to the read path:
- Change PowerShell read command to base64-encode the clipboard content
using [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(...))
- Add base64 flag to read command type for PowerShell entries
- Decode base64 result in readClipboardText when flag is set
Also adds child.unref() to spawned clipboard child processes in the write
path to prevent delaying process.exit() when the app exits mid-clipboard-write.
CI caught a split defect in this salvage: the PR's long-reasoning tail
test was kept but its production hunk was dropped as 'cosmetics'. It
isn't — cleanThinkingText runs several full-string regex passes and
reasoning grows on every streamed token, so re-cleaning the whole
accumulated string per chunk is O(n) per token / O(n^2) per stream.
Only the tail is displayed (boundedLiveRenderText caps it downstream),
so bound the input to 1.5x LIVE_RENDER_MAX_CHARS first.
Restores the one text.ts hunk from cd99e65fc (author preserved); the
italic-thinking display change and profile script from that commit
remain out of scope.
Addresses teknium1 sweeper review (2026-07-30) requiring coverage of:
1. ThreadSafeAsyncQueue.put_threadsafe() off-loop boundary: a real
daemon thread pushes into the queue from outside the owning event loop
while the consumer awaits get(), mirroring the run_conversation
worker-thread producer path. Includes a 20-concurrent-thread
no-drop regression.
2. Long-reasoning bound stability for thinkingPreview: 100k-char input
plus empty/collapsed cases must not crash and must retain the visible
tail marker inside the bounded 24k clean window.
Re-derivation of #38491 by @stremtec onto current main (the original is
10,119 commits behind; the hook moved into ui-tui/src/app/). The hook
returned a fresh object literal every render, defeating memoization in
useMainApp's consumers; useMemo over the (all-useCallback-stable)
handles makes the return referentially stable.
Dep array covers ALL nine returned handles incl. trimTail (the
re-derivation initially omitted it - stale-closure class).
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).