Files
hermes-ui/docs/chat-chain-changes/2026-10-19-terminal-cyber-theme.md
T
toor 8052bf5781
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
read the shell prompt again and stop the reconnect loop
The terminal was the last surface in the chat tool panel still wearing
someone else's design system, and it had two defects underneath that.

The font stack named no Nerd Font, so the private-use glyphs a default
zsh prompt draws its separators and branch marks from rendered as notdef
boxes. The ~ and the path after it drew fine, which is what made it read
as a font bug rather than a missing-glyph one.

Both components also carried their own copy of the palette -- four themes
in one, fifteen in the other, drifting, defaulting to a near-black that
appears nowhere in the token set. Changing --code-bg did nothing to the
terminal because the terminal never read a token. They now share one
module built from the tokens, so an error in the terminal is the same
red as an error anywhere else.

TerminalView reconnected on a bare setTimeout with no attempt cap and no
error surface, retrying invisibly every three seconds forever against an
unreachable backend. It now shares the panel's cap of three and renders
the same retry affordance, counting down again once a session is created.

Also drops a dead ws.onopen assignment and a 21px padding that was on no
spacing scale, and records what the chat page still owes the prototype.
2026-10-19 17:03:38 +08:00

4.1 KiB

date, pr, feature, impact
date pr feature impact
2026-10-19 pending Terminal CYBER theme, Nerd Font fallback, and bounded reconnect The chat tool panel's Terminal tab now paints from the CYBER token set instead of a hardcoded One Dark palette, resolves its font through a Nerd Font stack so shell prompts stop rendering as tofu boxes, and stops reconnecting after three failed attempts with an inline retry instead of looping forever. No WebSocket message names, payload shapes, or session lifecycle behavior changed.

Terminal brought onto the CYBER token set

The terminal was the last surface in the chat tool panel still wearing someone else's design system. Three defects, in order of how visible they were.

Tofu boxes in the prompt

The font stack was Menlo, Monaco, "Courier New", monospace. A default zsh prompt (powerlevel10k, oh-my-zsh) draws its separators, arrows and branch marks from the Unicode private use area — U+E0A0 through U+E0D4. None of those fonts carry those glyphs, so the browser fell back to drawing notdef boxes for the entire prompt line. The ~ and the path after it rendered fine, which is what made it read as a font bug rather than a missing-glyph bug.

The stack now names Nerd Font variants explicitly, in variables.scss as --font-mono. It is a CSS custom property rather than only an SCSS variable because xterm reads it from JS at Terminal construction time.

Colors came from a different product

Both terminal components carried their own copy of the palette — four themes in TerminalPanel.vue, fifteen in TerminalView.vue, drifting from each other, all of them third-party themes with a #1a1a2e default that appears nowhere in the token set. Changing --code-bg did nothing to the terminal, because the terminal never read a token.

Both now import utils/hermes/terminal-theme.ts, which holds a CYBER theme built from the tokens plus two user-selectable alternates. The mapping is recorded in that file: --bg-input for the background, --text-secondary for foreground, --accent-primary for the cursor, and --error / --success / --warning / --accent-info mapped onto red / green / yellow / blue so an error in the terminal is the same red as an error anywhere else.

xterm's ITheme only accepts hex, so the OKLCH values were converted rather than interpolated. The conversion is documented per-color; changing a token means updating that file too, which is a real coupling and the reason the comment says so.

Note the tradeoff on ANSI cyan: it is deliberately not the accent cyan. --accent-primary at full strength reads as UI chrome, and a terminal where every cyan prompt fragment glows competes with the actual UI. The accent is spent on the cursor, and brightCyan carries it for text.

Reconnect looped forever

TerminalView.vue reconnected on a bare setTimeout with no attempt cap and no error surface, so an unreachable backend produced an invisible retry every three seconds indefinitely. TerminalPanel.vue already capped at three attempts and rendered a retry affordance; TerminalView.vue now does the same, sharing the same cap and the same connectionFailed / connectionClosed / connectionError strings. The counter resets when a session is successfully created, so a transient blip does not permanently exhaust the budget.

Two smaller items came out of the same pass: ws.onopen was assigned twice in TerminalView.vue with the first assignment dead, and the header used a 21px vertical padding that was on no spacing scale. Both are gone.

Not changed

The session list, theme picker, and new-terminal button stay. The static prototype has no equivalent of any of them, and deleting working features to match a mock is the inverse of the problem worth solving.

Verification

npm run harness:check, npm run test and npm run build pass. The existing tests/e2e/terminal.spec.ts still passes unchanged — it asserts the WebSocket URL carries the token, that create is sent, that keyboard input is forwarded, and that a resize with non-zero cols/rows arrives. Those four are exactly the behaviors this change had to preserve.