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.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
date: 2026-10-19
|
||||
pr: pending
|
||||
feature: Chat view CYBER pass — single-colour live ring, send-key neon tube, status LED, responsive outline
|
||||
impact: The chat chain's "machine is working" indicator is now a single lime pulse instead of a six-colour cycle, the send key renders as a neon tube with the standard sweep instead of a flat grey block, the header carries a connection LED and a cyan press-light, and the Outline panel becomes an overlay at ≤1180px. Rendering-layer only — no store logic, no `/chat-run` payload, no markup semantics beyond one decorative status span, no event wiring changed.
|
||||
---
|
||||
|
||||
# Chat view CYBER pass
|
||||
|
||||
Follow-up to `2026-10-19-three-colour-semantics.md`. That change split the
|
||||
palette by meaning but left several call sites hardcoded, and the chat view was
|
||||
still missing the page-level treatment the rest of the app had.
|
||||
|
||||
## rainbow-glow collapsed into one keyframe
|
||||
|
||||
Three components each carried their own copy of a 44-line `rainbow-glow`
|
||||
keyframes block and cycled `#ff6b6b → #feca57 → #48dbfb → #ff9ff3 → #54a0ff →
|
||||
#5f27cd`. Four semantic hues flashing in one animation means the indicator
|
||||
carries no meaning at all — which is exactly what the three-colour rule exists
|
||||
to prevent.
|
||||
|
||||
`live-ring` is now defined once in `global.scss` and pulses `--success`
|
||||
(lime) between a dim and a bright ring. All three call sites use it:
|
||||
|
||||
- `MessageItem.vue` — `.speech-playing` on the message bubble
|
||||
- `SessionListItem.vue` — `.streaming` ring on the agent logo
|
||||
- `GroupMessageItem.vue` — `.speech-playing` on the group-chat bubble
|
||||
|
||||
All three are "the machine is doing something right now", which is one meaning,
|
||||
so they share one colour and one animation.
|
||||
|
||||
## Send key
|
||||
|
||||
`ChatInput.vue` had `border: 0 !important` and `box-shadow: none !important`
|
||||
on `.send-button`, which erased the neon tube that `global.scss` gives every
|
||||
`.arco-btn-primary`. The disabled state was also a solid `#9f9f9f` block —
|
||||
a hardcoded grey outside the palette. Both are gone; the button now inherits
|
||||
the global tube + `neon-sheen` sweep and uses the standard dimmed-tube disabled
|
||||
treatment.
|
||||
|
||||
Added `.send-button--stop`: while a run is streaming, the same button becomes
|
||||
the stop control, which is a "needs you" action rather than a "system" action,
|
||||
so it turns amber — the same semantic the approval card uses.
|
||||
|
||||
## Status LED
|
||||
|
||||
New `.cy-led` in `global.scss`: a 7px dot, lime with a glow and a slow
|
||||
breathe when connected, red and static when not. A dead connection should not
|
||||
look like it is still doing work, so the breathe animation only runs in the
|
||||
connected state. Bound to `appStore.connected` in the chat header, reusing the
|
||||
existing `sidebar.connected` / `sidebar.disconnected` strings.
|
||||
|
||||
## Session list current item
|
||||
|
||||
`.session-item.active` already had the soft cyan fill; the prototype's defining
|
||||
feature is the 2px glowing left bar. Added via `inset` box-shadow rather than
|
||||
`border-left`, which would have shifted the row's content by 2px.
|
||||
|
||||
## Header press-light
|
||||
|
||||
`.chat-header` gets a `radial-gradient` cyan wash from the top edge. Radial,
|
||||
not linear — a linear gradient lights the whole strip evenly and loses the
|
||||
"pressed from above" quality.
|
||||
|
||||
## Responsive
|
||||
|
||||
Verified the real implementation had only the 768px tier. Added the one tier
|
||||
that was genuinely missing:
|
||||
|
||||
- **≤1180px** — `OutlinePanel` becomes a `position: absolute` overlay. Below
|
||||
this width the message stream, Outline and tool panel cannot coexist as three
|
||||
columns; the header's Outline button already toggles it, so no extra close
|
||||
control is needed. Below 768px it keeps its existing narrower width.
|
||||
- **≤900px** — the workspace badge hides. The session sidebar deliberately does
|
||||
**not** hide at this width: it is controlled by the header toggle, and
|
||||
removing the column would leave no way to open it.
|
||||
|
||||
## Not implemented
|
||||
|
||||
Three items in the tracking doc were already done by
|
||||
`2026-10-19-three-colour-semantics.md` and are recorded there, not repeated
|
||||
here: bubble role colours (cyan right edge on user, lime left edge on
|
||||
assistant), the approval card's amber zone, and the three-segment context meter.
|
||||
|
||||
## Verification
|
||||
|
||||
`npm run harness:check`, `npm run test` and `npm run build` pass. The LED,
|
||||
the send key's enabled/disabled/stop states, the active session row, and both
|
||||
new breakpoints were checked in a browser.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
date: 2026-10-20
|
||||
pr: pending
|
||||
feature: Chat and group chat dark-theme residue cleanup
|
||||
impact: Message attachments, file cards, audio blocks, thinking headers and tool detail headers now paint from the CYBER token set instead of the light-theme black/white washes, so they are visible against the dark surfaces. Purely visual; no message shapes, socket events, or emission timing changed.
|
||||
---
|
||||
|
||||
# Light-theme residue in the chat surface
|
||||
|
||||
The CYBER theme is a single dark theme. A handful of surfaces inside the chat
|
||||
chain still carried the light-theme habit of tinting with `rgba(0, 0, 0, α)`:
|
||||
a 4% black wash reads as "slightly grey" on white, and as "nothing at all" on
|
||||
`--bg-input`. Those surfaces had lost their fill entirely.
|
||||
|
||||
## What changed
|
||||
|
||||
| File | Surface | Before | After |
|
||||
| --- | --- | --- | --- |
|
||||
| `MarkdownRenderer.vue` | audio container, file card | `rgba(0,0,0,0.04)` | `--bg-input` |
|
||||
| `MarkdownRenderer.vue` | file card hover | `rgba(0,0,0,0.08)` | `--bg-card-hover` |
|
||||
| `MessageItem.vue` | attachment block | `rgba(0,0,0,0.04)` | `--bg-input` |
|
||||
| `MessageItem.vue` | thinking header hover, tool row hover | `rgba(0,0,0,0.03)` | `rgba(255,255,255,0.05)` |
|
||||
| `MessageItem.vue` | tool detail code header | `rgba(0,0,0,0.02)` | `rgba(255,255,255,0.02)` |
|
||||
| `GroupMessageItem.vue` | same three hover/header rules | same | same |
|
||||
|
||||
The rule is directional: a *fill* uses a background token, a *hover lift* uses a
|
||||
white wash (there is no lighter token that reads correctly on top of
|
||||
`--bg-input`), and a *divider tint* uses a 2% white wash.
|
||||
|
||||
## Not changed
|
||||
|
||||
Message rendering, streaming, tool-call grouping, and the `/chat-run` event
|
||||
contract. This fragment exists because the touched files sit on the chat chain
|
||||
path, not because any behavior moved.
|
||||
|
||||
## Verification
|
||||
|
||||
`npm run harness:check` and `npm run build` pass. The existing
|
||||
`tests/e2e/chat-cyber-semantics.spec.ts` still passes unchanged.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
date: 2026-10-20
|
||||
pr: pending
|
||||
feature: Chat surface shadow tokenization and CYBER color guard
|
||||
impact: The fork divider pill in the chat message list now paints its shadow from the shared --shadow-sm token instead of a hardcoded light-theme slate wash, so it reads as a lifted surface on the dark background. A new source-level guard now blocks raw hex colors, named colors, and literal RGB triples from reappearing anywhere in the client source. Purely visual; no message shapes, socket events, or emission timing changed.
|
||||
---
|
||||
|
||||
# Chat surface shadow tokenization
|
||||
|
||||
## What changed
|
||||
|
||||
| File | Surface | Before | After |
|
||||
| --- | --- | --- | --- |
|
||||
| `MessageList.vue` | fork divider pill | `box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05)` | `box-shadow: var(--shadow-sm)` |
|
||||
|
||||
`rgba(15, 23, 42, 0.05)` is a slate-900 wash at 5% alpha — a light-theme habit
|
||||
for a hairline lift on white. On `--bg-card` it is effectively invisible, so
|
||||
the pill lost its "lifted" read entirely. `--shadow-sm` is the dark-theme
|
||||
equivalent (`0 2px 8px oklch(0 0 0 / 0.28)`).
|
||||
|
||||
The same lint also flagged a stale shadow in `PetdexView.vue`
|
||||
(`rgba(127, 127, 127, 0.08)` checkerboard → `rgba(var(--text-muted-rgb), 0.08)`)
|
||||
and the kanban category colors, which were promoted to the new
|
||||
`--kanban-review` / `--kanban-review-rgb` tokens. Those two files are not on
|
||||
the chat chain, but they are recorded here because the guard that found them
|
||||
was introduced as part of this same change.
|
||||
|
||||
## New guard
|
||||
|
||||
`tests/client/style-tokens.test.ts` scans every `.vue` / `.scss` / `.css` /
|
||||
`.ts` file under `packages/client/src` and fails on:
|
||||
|
||||
1. raw hex colors (`#rrggbb`)
|
||||
2. named color keywords (`color: red`)
|
||||
3. literal `rgba(0,0,0,` shadows
|
||||
4. literal RGB triples in `rgba()` (`rgba(15, 23, 42, 0.05)`) — these must
|
||||
become `rgba(var(--*-rgb), α)`
|
||||
|
||||
Comments and HTML entities are stripped before matching, and the stripping step
|
||||
has its own self-check so a broken guard cannot silently pass.
|
||||
|
||||
The exemption list is explicit: chart palette (`ui-baseline.md` §7), the xterm
|
||||
theme presets, the Arco `ConfigProvider` theme, the `arkscore` vendor
|
||||
decoration layer, and the language badges in `MessageItem.vue`. The language
|
||||
badge palette is additionally pinned by an assertion, so the exemption cannot
|
||||
widen unnoticed.
|
||||
|
||||
## Not changed
|
||||
|
||||
Message rendering, streaming, tool-call grouping, and the `/chat-run` event
|
||||
contract. The chat chain fragment exists because the touched file sits on the
|
||||
chat chain path, not because any behavior moved.
|
||||
|
||||
## Verification
|
||||
|
||||
`npm run harness:check`, `npm run build`, and `npm run test:e2e` (46 passed)
|
||||
all pass. The existing `tests/e2e/chat-cyber-semantics.spec.ts` still passes
|
||||
unchanged.
|
||||
Reference in New Issue
Block a user