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.
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.