Files
hermes-ui/docs/chat-chain-changes/2026-10-19-chat-cyber-regression-guard.md
T
toor 5e1003ceb7
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
replace the monochrome theme with the CYBER neon-cyan skin
The project shipped a black-and-white theme that was only half converted:
the login page looked right, the message page was halfway, and the
settings page had not been started. This finishes the move to the CYBER
reference in prototype_reference/.

Colors now come from a single token source (styles/variables.scss), where
every semantic color is declared twice: once as OKLCH for the CSS, and
once as an RGB triple so translucent variants can be built as
rgba(var(--*-rgb), alpha). Cyan is the system itself, amber means the user
has to intervene, green means the machine is working, and red only ever
means something broke. The RGB triples are derived from the OKLCH values
so the two declarations cannot drift apart.

Pages adopt the .cy-* shell classes from the reference (view, panel,
kpis, tabs, grid, table, field) instead of ad-hoc markup, which is what
makes 19 of 26 views consistent. The remaining seven are deliberately
bespoke: chat streams, the kanban board, the workflow canvas, the
terminal, and the missing-agent screen.

The chart palette and the kanban column colors move into single-source
modules so they cannot drift either. A new guard test rejects raw hex
colors, named colors, and literal RGB triples anywhere in the client
source, with an explicit exemption list for the places that genuinely
cannot read a CSS variable. The existing chat semantics spec keeps the
chat chain honest.

No behavior changes: no logic, no i18n keys, no data structures, and the
/chat-run event contract is untouched.
2026-10-20 11:17:46 +08:00

105 lines
5.5 KiB
Markdown

---
date: 2026-10-19
pr: pending
feature: Regression guards for the chat CYBER treatment, plus a real fix for the ModelSelector provider jump
impact: Adds a Playwright spec that fails if `live-ring` is duplicated, if a multi-hue cycle returns, if the send key is flattened back to a grey block, or if either new breakpoint is lost; and removes a side effect from `providerOptions` that was silently resetting the user's provider choice. No `/chat-run` payload, no store logic, no markup semantics changed.
---
# Chat CYBER regression guards
The CYBER pass in `2026-10-19-chat-cyber-skeleton-pass.md` was rendering-layer
only, which meant nothing in the test suite could see it. Two of its three
deliberate decisions were structural rather than visual, and structural
regressions are exactly what code review misses:
1. **`live-ring` must have one home.** It had three copies of `rainbow-glow`
before, and a copy is easy to paste back. The test scans the source tree
instead of the DOM — a duplicate keyframe is invisible to any runtime
assertion, because the last definition silently wins.
2. **The send key must not be flattened.** `border: 0 !important` plus
`box-shadow: none !important` had erased the global neon tube, and the
disabled state was a hardcoded `#9f9f9f` outside the palette. The test
asserts the computed shadow is present and that the border is not that grey.
The same spec also pins the computed values that carry meaning: the LED's
dimensions and its breathe animation, the stop button's amber border (stopping
is a "needs you" action), the header's radial wash, the Outline overlay at
≤1180px, and the workspace badge hiding at ≤900px.
The live-ring test also asserts the keyframe body contains no raw hex. The
original cycle was `#ff6b6b → #feca57 → #48dbfb → #ff9ff3`, so any hex literal
in that block means a multi-hue cycle has crept back in.
## ModelSelector provider jump
`providerOptions` assigned to `customProvider` inside its computed getter. Any
recomputation — a model list refresh, a profile switch — overwrote whatever the
user had just picked in the dropdown, which is the "provider 选择自己跳"
symptom in `known-issues.md` #1.
The getter is now a pure read and the synchronisation moved to
`watch(() => appStore.selectedProvider, ...)`. `openModal()` keeps its original
assignment because `watch` is lazy and does not fire on the first render.
Covered by `tests/client/model-selector-provider-sync.test.ts`, which also
asserts the watch is anchored to `selectedProvider` rather than to
`providerOptions` — anchoring it to the computed would just relocate the same
side effect.
`known-issues.md` #2 (ProfileSelector i18n keys) was investigated and found to
be a misreport: `t()` returns translated text, all 33 keys used by that
component exist in both locales, and no call site anywhere passes a bare key to
`message.*`. That entry is marked as a misreport rather than left to be
"fixed" into a real bug.
## A second flattening site, found by the guard
Writing the source-level scan immediately paid for itself: it flagged
`group-chat/GroupChatInput.vue`, which carried the identical
`border: 0 !important` + `box-shadow: none !important` pair and the same
hardcoded `#9f9f9f` disabled block. The CYBER pass had only fixed the
`chat/` twin, so the group chat input was still a flat grey block. Fixed the
same way — the overrides are gone and the disabled state falls back to the
global `.arco-btn-disabled` rule, which extinguishes the tube rather than
painting it grey.
## Verification
```
npx vitest run tests/client tests/shared 109 files, 662 tests passed
npm run test:e2e 46 passed (was 35)
npm run harness:check passed
npm run build passed
```
The e2e count moved from 35 to 46: eleven new cases in
`chat-cyber-semantics.spec.ts`.
Four of those eleven failed on the first run and each failure was a wrong
assumption in the test rather than a product bug, which is worth recording
because two of them would have quietly become false-passing tests:
- Asserting the send key's tube in its empty state. Empty input *is* the
disabled state, and `global.scss` deliberately sets `box-shadow: none` there
— the tube going dark is the intended "nothing to send" signal. The
assertion now checks the dark disabled look first, then the lit tube after
typing.
- Reaching the stop state by clicking send. The mocked socket in
`fixtures.ts` records emits but never acknowledges a run, so `isStreaming`
never became true and the button stayed in its lit state. The test now
triggers `run.started` through `__PW_CHAT_SOCKET__`, the same way
`chat-streaming.spec.ts` does.
- Comparing rendered colours against the palette. This cannot work here and
the errors explain why: the palette declares each hue twice — an OKLCH value
(`--success: oklch(0.860 0.190 128)`) and an RGB triple
(`--success-rgb: 107, 247, 84`) — and the two forms sit about 30° apart, so
a correct colour reports as a mismatch. Colour semantics are now pinned in
the source-level tests, which can see which token a rule references. The
browser tests assert what rendering can answer unambiguously: which class is
active, which keyframe runs, whether a glow exists, and that nothing has been
flattened to grey.
- Looking for the workspace badge after a send. The badge is behind
`v-if="chatStore.activeSession?.workspace"`, and the default mock returns no
sessions, so it never rendered. The fixture now supplies a session with a
workspace.