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

78 lines
4.1 KiB
Markdown

---
date: 2026-10-19
pr: pending
feature: Terminal CYBER theme, Nerd Font fallback, and bounded reconnect
impact: 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.