From 5e1003ceb762dea62d4a96b479356b26b182ab8c Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 20 Oct 2026 11:17:46 +0800 Subject: [PATCH] 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. --- .gitignore | 1 - DEVELOPMENT.md | 53 + .../2026-10-19-chat-cyber-regression-guard.md | 104 + .../2026-10-19-chat-cyber-skeleton-pass.md | 89 + .../2026-10-20-chat-cyber-dark-residue.md | 39 + .../2026-10-20-chat-cyber-token-guard.md | 58 + docs/chat-development-todo.md | 89 +- docs/cyber-theme-plan.md | 352 ++ docs/known-issues.md | 87 +- docs/ui-baseline.md | 253 +- packages/client/index.html | 2 +- packages/client/src/App.vue | 6 +- .../src/components/hermes/chat/ChatInput.vue | 46 +- .../src/components/hermes/chat/ChatPanel.vue | 76 +- .../hermes/chat/ConversationMonitorPane.vue | 12 +- .../components/hermes/chat/DrawerPanel.vue | 4 +- .../src/components/hermes/chat/FilesPanel.vue | 4 +- .../components/hermes/chat/FolderPicker.vue | 20 +- .../hermes/chat/HistoryMessageList.vue | 2 +- .../hermes/chat/MarkdownRenderer.vue | 10 +- .../components/hermes/chat/MessageItem.vue | 67 +- .../components/hermes/chat/MessageList.vue | 14 +- .../components/hermes/chat/OutlinePanel.vue | 103 +- .../hermes/chat/SessionListItem.vue | 77 +- .../components/hermes/chat/TerminalPanel.vue | 4 +- .../hermes/files/FileBreadcrumb.vue | 24 +- .../components/hermes/files/FileEditor.vue | 13 +- .../src/components/hermes/files/FileList.vue | 23 +- .../components/hermes/files/FilePreview.vue | 13 +- .../src/components/hermes/files/FileTree.vue | 20 +- .../hermes/group-chat/GroupChatInput.vue | 22 +- .../hermes/group-chat/GroupChatPanel.vue | 18 +- .../hermes/group-chat/GroupMessageItem.vue | 65 +- .../hermes/group-chat/GroupMessageList.vue | 2 +- .../src/components/hermes/jobs/JobCard.vue | 220 +- .../components/hermes/jobs/JobRunHistory.vue | 28 +- .../hermes/kanban/KanbanCreateForm.vue | 12 + .../hermes/kanban/KanbanTaskCard.vue | 93 +- .../hermes/kanban/KanbanTaskDrawer.vue | 229 +- .../components/hermes/mcp/McpServerCard.vue | 201 +- .../hermes/models/AnthropicLoginModal.vue | 8 +- .../hermes/models/AuxiliaryModelsPanel.vue | 103 +- .../hermes/models/CodexLoginModal.vue | 8 +- .../hermes/models/CombinationModelsPanel.vue | 106 +- .../hermes/models/CopilotLoginModal.vue | 8 +- .../hermes/models/GeminiLoginModal.vue | 8 +- .../hermes/models/NousLoginModal.vue | 8 +- .../components/hermes/models/ProviderCard.vue | 255 +- .../hermes/models/ProvidersPanel.vue | 11 +- .../hermes/models/XaiOAuthLoginModal.vue | 8 +- .../hermes/profiles/ProfileCard.vue | 244 +- .../hermes/profiles/ProfileCreateModal.vue | 8 - .../hermes/profiles/ProfilesPanel.vue | 11 +- .../hermes/settings/AccountSettings.vue | 43 +- .../hermes/settings/GithubPreviewSettings.vue | 216 +- .../hermes/settings/ModelSettings.vue | 37 +- .../hermes/settings/PlatformCard.vue | 59 +- .../hermes/settings/PlatformSettings.vue | 9 +- .../settings/UserManagementSettings.vue | 5 +- .../hermes/settings/VoiceSettings.vue | 7 +- .../settings/voice/ProviderMetaItem.vue | 10 +- .../hermes/settings/voice/VoiceApiCard.vue | 53 +- .../settings/voice/VoiceApiFormModal.vue | 2 +- .../components/hermes/skills/SkillDetail.vue | 186 +- .../hermes/skills/SkillExternalDirsModal.vue | 11 +- .../components/hermes/skills/SkillList.vue | 138 +- .../hermes/usage/AgentBreakdown.vue | 74 +- .../components/hermes/usage/DailyTrend.vue | 173 +- .../hermes/usage/ModelBreakdown.vue | 96 +- .../src/components/hermes/usage/StatCards.vue | 77 +- .../hermes/workflow/WorkflowAgentNode.vue | 109 +- .../hermes/workflow/WorkflowModelSelector.vue | 6 +- .../src/components/layout/ModelSelector.vue | 27 +- .../src/components/layout/PageSidebarNav.vue | 2 +- packages/client/src/i18n/locales/en.ts | 12 + packages/client/src/i18n/locales/zh.ts | 12 + packages/client/src/stores/hermes/usage.ts | 15 +- packages/client/src/styles/arco-theme.ts | 4 +- packages/client/src/styles/chart-colors.scss | 42 + packages/client/src/styles/chart-palette.ts | 46 + packages/client/src/styles/code-block.scss | 4 +- packages/client/src/styles/global.scss | 402 +- packages/client/src/styles/variables.scss | 61 +- .../src/utils/hermes/kanban-status-colors.ts | 89 + .../client/src/views/hermes/ChannelsView.vue | 16 +- .../client/src/views/hermes/DevicesView.vue | 294 +- .../client/src/views/hermes/FilesView.vue | 101 +- .../src/views/hermes/HermesMissingView.vue | 140 +- .../client/src/views/hermes/HistoryView.vue | 71 +- packages/client/src/views/hermes/JobsView.vue | 9 +- .../client/src/views/hermes/JourneyView.vue | 155 +- .../client/src/views/hermes/KanbanView.vue | 137 +- packages/client/src/views/hermes/LogsView.vue | 122 +- .../src/views/hermes/McpManagerView.vue | 121 +- .../client/src/views/hermes/MemoryView.vue | 176 +- .../client/src/views/hermes/ModelsView.vue | 53 +- .../src/views/hermes/PerformanceView.vue | 305 +- .../client/src/views/hermes/PetdexView.vue | 160 +- .../client/src/views/hermes/PluginsView.vue | 278 +- .../client/src/views/hermes/ProfilesView.vue | 8 +- .../src/views/hermes/SkillsUsageView.vue | 181 +- .../client/src/views/hermes/SkillsView.vue | 70 +- .../client/src/views/hermes/TerminalView.vue | 10 +- .../client/src/views/hermes/UsageView.vue | 5 +- .../src/views/hermes/VersionPreviewView.vue | 3 +- .../client/src/views/hermes/WorkflowView.vue | 180 +- prototype_reference/hermes-ui-cyber-api.html | 3294 +++++++++++++++++ prototype_reference/hermes-ui-cyber-chat.html | 657 ++++ .../hermes-ui-cyber-login.html | 480 +++ .../hermes-ui-cyber-shell-v1.html | 1311 +++++++ .../hermes-ui-cyber-shell.html | 2222 +++++++++++ scripts/harness-check.mjs | 65 + tests/client/chart-palette-sync.test.ts | 56 + .../fixtures/script-setup-let-assignment.vue | 21 + .../model-selector-provider-sync.test.ts | 51 + tests/client/profile-card-config-edit.test.ts | 2 +- .../provider-card-built-in-action.test.ts | 6 +- .../script-setup-let-assignment.test.ts | 29 + tests/client/style-system.test.ts | 62 +- tests/client/style-tokens.test.ts | 196 + tests/e2e/chat-cyber-semantics.spec.ts | 289 ++ 121 files changed, 13340 insertions(+), 3364 deletions(-) create mode 100644 docs/chat-chain-changes/2026-10-19-chat-cyber-regression-guard.md create mode 100644 docs/chat-chain-changes/2026-10-19-chat-cyber-skeleton-pass.md create mode 100644 docs/chat-chain-changes/2026-10-20-chat-cyber-dark-residue.md create mode 100644 docs/chat-chain-changes/2026-10-20-chat-cyber-token-guard.md create mode 100644 docs/cyber-theme-plan.md create mode 100644 packages/client/src/styles/chart-colors.scss create mode 100644 packages/client/src/styles/chart-palette.ts create mode 100644 packages/client/src/utils/hermes/kanban-status-colors.ts create mode 100644 prototype_reference/hermes-ui-cyber-api.html create mode 100644 prototype_reference/hermes-ui-cyber-chat.html create mode 100644 prototype_reference/hermes-ui-cyber-login.html create mode 100644 prototype_reference/hermes-ui-cyber-shell-v1.html create mode 100644 prototype_reference/hermes-ui-cyber-shell.html create mode 100644 tests/client/chart-palette-sync.test.ts create mode 100644 tests/client/fixtures/script-setup-let-assignment.vue create mode 100644 tests/client/model-selector-provider-sync.test.ts create mode 100644 tests/client/script-setup-let-assignment.test.ts create mode 100644 tests/client/style-tokens.test.ts create mode 100644 tests/e2e/chat-cyber-semantics.spec.ts diff --git a/.gitignore b/.gitignore index a2f5328..c48b2bf 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ pnpm-lock.yaml pnpm-workspace.yaml # Local design prototypes and reference snapshots (read-only, not part of this project) reference_code/ -prototype_reference/ # Server data packages/server/data/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4677aae..312309a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -7,11 +7,14 @@ This document defines project-level development rules for hermes-ui. It is tool- - Node.js `>=23` (see `engines` in `package.json`). - Python with `hermes-agent` installed. The Hermes CLI is not bundled; point `HERMES_BIN` at the executable or make `hermes` resolvable from `PATH`. +- A C/C++ toolchain, **but only on Linux**: `python3`, `make`, `g++`. See + "Platform Notes" below. ## Commands ```bash npm ci --ignore-scripts +npm rebuild node-pty npm run dev npm run test npm run test:coverage @@ -20,12 +23,62 @@ npm run build ``` - `npm ci --ignore-scripts` installs exactly what `package-lock.json` pins. Do not upgrade dependencies. +- `npm rebuild node-pty` is **required**, not optional. `npm ci --ignore-scripts` + skips every install script, and `node-pty`'s install script is what puts a + loadable native module in place. Skipping it leaves the terminal feature + silently disabled — the server logs a warning and keeps running. - `npm run dev` starts the Vite client and Koa server together. - `npm run test` runs Vitest unit tests. - `npm run test:coverage` is what the Build workflow runs before `npm run build`. - `npm run test:e2e` runs Playwright browser tests against a mocked BFF API. - `npm run build` type-checks and builds both client and server. +## Platform Notes + +The server runs on Windows, macOS, and Linux. The parts that differ are +isolated behind `process.platform` checks, so the same source runs everywhere. + +### node-pty + +`node-pty` ships prebuilt binaries for Windows and macOS only: + +| Platform | Where the native module comes from | Build step | +| --- | --- | --- | +| Windows | `prebuilds/win32-/` (shipped in the npm tarball) | not needed | +| macOS | `prebuilds/darwin-/` (shipped in the npm tarball) | not needed | +| Linux | **none — must be compiled from source** | `npm rebuild node-pty` | + +On Linux, `npm rebuild node-pty` compiles `node-pty` from source and needs +`python3`, `make`, and `g++` on the machine. The Dockerfile installs exactly +those for this reason. + +On macOS the prebuilt `spawn-helper` may arrive without its executable bit. +The server repairs that at startup +(`packages/server/src/routes/hermes/terminal.ts`), and `bin/hermes-ui.mjs` +does the same for the installed CLI. + +### Other platform differences + +- **Terminal shell**: `powershell.exe` on Windows, `$SHELL` (falling back to + `/bin/zsh` then `/bin/bash`) on macOS and Linux. +- **Agent Bridge transport**: a Unix domain socket at + `ipc:///tmp/hermes-agent-bridge.sock` on macOS and Linux, a TCP port + (`tcp://127.0.0.1:18765`) on Windows, which has no Unix socket support in + Node. +- **Python interpreter**: `python3` on macOS and Linux, `python` on Windows. + Virtualenv layout differs too (`venv/bin/python3` vs `venv\Scripts\python.exe`). +- **Gateway process tree**: `taskkill /T` on Windows, process-group signals on + macOS and Linux. +- **Hermes home**: `~/.hermes` on macOS and Linux; on Windows, an existing + `%LOCALAPPDATA%\hermes` or `%APPDATA%\hermes` takes priority. + +### Verification + +Linux is covered by the `Build` and `Playwright` workflows, which run on +`ubuntu-latest`. macOS is not covered by any hosted runner; the +`macOS Verification` workflow (`workflow_dispatch` only) runs the same checks +when a macOS runner is available. + ## Architecture - Frontend code lives under `packages/client/src`. diff --git a/docs/chat-chain-changes/2026-10-19-chat-cyber-regression-guard.md b/docs/chat-chain-changes/2026-10-19-chat-cyber-regression-guard.md new file mode 100644 index 0000000..854fb1f --- /dev/null +++ b/docs/chat-chain-changes/2026-10-19-chat-cyber-regression-guard.md @@ -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. diff --git a/docs/chat-chain-changes/2026-10-19-chat-cyber-skeleton-pass.md b/docs/chat-chain-changes/2026-10-19-chat-cyber-skeleton-pass.md new file mode 100644 index 0000000..548fcbc --- /dev/null +++ b/docs/chat-chain-changes/2026-10-19-chat-cyber-skeleton-pass.md @@ -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. diff --git a/docs/chat-chain-changes/2026-10-20-chat-cyber-dark-residue.md b/docs/chat-chain-changes/2026-10-20-chat-cyber-dark-residue.md new file mode 100644 index 0000000..f32e9dc --- /dev/null +++ b/docs/chat-chain-changes/2026-10-20-chat-cyber-dark-residue.md @@ -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. diff --git a/docs/chat-chain-changes/2026-10-20-chat-cyber-token-guard.md b/docs/chat-chain-changes/2026-10-20-chat-cyber-token-guard.md new file mode 100644 index 0000000..f8daa8d --- /dev/null +++ b/docs/chat-chain-changes/2026-10-20-chat-cyber-token-guard.md @@ -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. diff --git a/docs/chat-development-todo.md b/docs/chat-development-todo.md index 03a0ede..654bd68 100644 --- a/docs/chat-development-todo.md +++ b/docs/chat-development-todo.md @@ -6,6 +6,11 @@ 本文记录已核实的差距和推进顺序。所有条目均已对照 `prototype_reference/hermes-ui-cyber-chat.html` 与真实组件确认,不是推测。 +> **进度:P0 / P1 / P2 已全部完成**(2026-10-19)。变更片段见 +> `chat-chain-changes/2026-10-19-chat-cyber-skeleton-pass.md` 与 +> `chat-chain-changes/2026-10-19-three-colour-semantics.md`。 +> 下方各条目已标注实测结果;与真实代码不符的旧记录就地更正并说明原因。 + ## 为什么对话台排在最前面 对话台是结构最复杂的一页:一个页面里有会话列表、气泡流、审批卡、输入区、 @@ -18,6 +23,11 @@ ## 现状:骨架类使用情况 +> **注意**:本节记录的是 P0/P1 动工**之前**的基线。对话台最终没有套用 +> `cy-view` / `cy-panel` 这类通用骨架类 —— CYBER 语义是按「每种颜色只承担 +> 一种含义」逐组件落的(见下文 P1),直接套骨架类反而会把语义抹平。 +> 真正补上的是 `global.scss` 里的 `.cy-led` 与 `@keyframes live-ring`。 + `cy-view` / `cy-panel` / `cy-tabs` / `cy-sec-t` 的全仓库使用点: | 文件 | 位置 | @@ -30,7 +40,7 @@ ## 任务清单 -### P0 · 修 rainbow-glow(违反三色语义规则) +### P0 · 修 rainbow-glow(违反三色语义规则)✅ 已完成 一次动画里四种语义色同时在闪,直接违反「同屏内每种颜色只承担一种含义」。 硬编码值,未走令牌。 @@ -43,31 +53,42 @@ | `components/hermes/chat/SessionListItem.vue` | `409`(定义)、`395`(调用) | | `components/hermes/group-chat/GroupMessageItem.vue` | `1051`(定义)、`1114`(调用) | -当前色值循环:`#ff6b6b` → `#feca57` → `#48dbfb` → `#ff9ff3` → 回环。 +原色值循环:`#ff6b6b` → `#feca57` → `#48dbfb` → `#ff9ff3` → 回环。 -处理方向:按调用场景收敛到单色。`.speech-playing` 是语音播放态, -语义上属于「机器在动」,应改用 `--success`,去掉循环。 +**处理结果**:三处本地 `@keyframes rainbow-glow` 全部删除(约 130 行重复), +改用 `global.scss` 里单一定义的 `@keyframes live-ring` —— 荧光绿 +(`--success`,语义为「机器在动」)单色呼吸,不再循环。`.speech-playing` +与 `.streaming` 两类「机器正在动」的指示形态从此共用这一个动画。 -### P1 · 组件级语义配色 +### P1 · 组件级语义配色 ✅ 已完成 -| 项 | 原型 | 真实实现 | 差距 | -| --- | --- | --- | --- | -| 发送键扫光 | 原型 133-140:28×26,青管壁 + `sheen` 每 3.4s 扫过 | `ChatInput.vue:1765-1783`:30×30,`border: 0 !important` + `box-shadow: none !important`,禁用态 `#9f9f9f` 纯灰 | **未实现**,且被 `!important` 显式压制 | -| 气泡 role 配色 | 原型 216-227:用户=青、助手=绿,助手气泡左缘一道 `lime-line` | `MessageItem.vue` 无 role 配色规则 | **未实现** | -| LED 指示灯 | 原型 151-156:7px 圆点 + 发光 + 呼吸动画 | 全局无 `.led` 类 | **未实现** | -| 会话列表当前项 | 原型 190-192:青软底 + 左侧 2px 发光条 | `SessionListItem.vue:288`:`box-shadow: 0 0 0 3px` 描边 | 形似,神韵不同 | -| 顶栏青色微光 | 原型 200-201:`radial-gradient` 压顶 | 无 | **未实现** | -| 审批卡橙区 | 原型 264-267:橙边 + 内发光 | `MessageList.vue:942` 有 `approval-pulse`,非原型那套橙语义区 | 部分 | -| 上下文余量条 | 原型 273-278:绿→橙→红三段 | `ChatInput.vue` 有 context meter | 已有,分段配色待确认 | +| 项 | 原型 | 真实实现(更新于 2026-10-19) | +| --- | --- | --- | +| 发送键扫光 | 原型 133-140:28×26,青管壁 + `sheen` 每 3.4s 扫过 | **已完成**。移除 `border: 0 !important` + `box-shadow: none !important`(原本把 `neon-tube` 整个抹平),恢复全站霓虹管 + 扫光;禁用态不再硬编码 `#9f9f9f` 灰块,走全局熄灭态;新增 `.send-button--stop` 琥珀橙中断态 | +| 气泡 role 配色 | 原型 216-227:用户=青、助手=绿,助手气泡左缘一道 `lime-line` | **此前记录有误,实为已完成**。`MessageItem.vue:1310` 用户气泡 `border-right: 2px solid rgba(var(--accent-primary-rgb), 0.45)`;助手气泡 `border-left: 2px solid var(--success-line)`;`.message-bubble.system` 琥珀橙边 + 橙底;`.agent-error` 用 `$error`。由 `2026-10-19-three-colour-semantics.md` 完成 | +| LED 指示灯 | 原型 151-156:7px 圆点 + 发光 + 呼吸动画 | **已完成**。`global.scss` 新增 `.cy-led`(7px 圆点),绑定真实 `appStore.connected`:连通=绿 + `led-breathe` 呼吸,断开=红且**静止**(死连接不该看起来还在动)。挂在 `ChatPanel.vue` 顶栏标题后 | +| 会话列表当前项 | 原型 190-192:青软底 + 左侧 2px 发光条 | **已完成**。`SessionListItem.vue` 的 `.session-item.active` 从 `box-shadow: 0 0 0 3px` 描边改为青软底 + `inset 2px 0 0` 左侧发光条(用 inset 而非 `border-left`,避免挤动整行内容宽度) | +| 顶栏青色微光 | 原型 200-201:`radial-gradient` 压顶 | **已完成**。`ChatPanel.vue` 的 `.chat-header` 压一层 `radial-gradient(120% 160% at 50% 0%, rgba(var(--accent-primary-rgb), 0.07), transparent 70%)`。用 radial 而非 linear —— linear 会让整条边均匀发亮,反而没有「压顶」的层次 | +| 审批卡橙区 | 原型 264-267:橙边 + 内发光 | **此前记录有误,实为已完成**。`MessageList.vue` 的 `.approval-float-panel` 已用 `var(--warning-line)` 边框 + `--warning-soft` 底 + `inset 0 0 22px rgba(var(--warning-rgb), 0.06)` 内发光 + `@keyframes approval-pulse`。由 `2026-10-19-three-colour-semantics.md` 完成 | +| 上下文余量条 | 原型 273-278:绿→橙→红三段 | **此前记录为「待确认」,实为已完成**。`ChatInput.vue:1061-1064` 按 `usagePercent` 分三段:默认绿、>60%~80% 转 `context-bar-warn`(橙)、>80% 转 `context-bar-danger`(红) | -### P2 · 响应式 +### P2 · 响应式 ✅ 已完成 原型 166-177 定义了两档断点: - `≤1180px`:Outline 从挤压主区改为浮层覆盖(`position: absolute`) -- `≤900px`:会话侧栏整体隐藏,顶栏 `.ws-tag` 隐藏 + —— **已完成**。`OutlinePanel.vue` 在 ≤1180px 下改 `position: absolute` + + `z-index: 20` + 左侧投影;≤768px 进一步收窄为 `min(280px, 86vw)`。 + 右上角的 Outline 按钮本身就是开关,不需要额外关闭控件。 +- `≤900px`:顶栏 `.ws-tag` / 工作区徽标隐藏 —— **已完成**。 + `ChatPanel.vue` 在 ≤900px 下 `.workspace-badge { display: none }`。 -对话台真实实现是否已有等价断点,未核实。 +> **与原建议的一处偏离**:原型在 ≤900px 还要求「会话侧栏整体隐藏」,**未采纳**。 +> 真实实现的会话侧栏由顶栏 toggle 按钮控制显隐,隐藏整列后用户将无法再打开 —— +> 比挤一点更糟。窄屏下改为只隐藏次要信息(工作区徽标),侧栏交给 toggle。 + +> 核实记录:真实实现原本只有 `$breakpoint-mobile`(768px)一档断点, +> 不存在 ≤1180px 的等价实现。 ## 已知原型侧问题(不是待开发项) @@ -95,13 +116,14 @@ - **顶栏常驻 `streaming` tag**:真实 streaming 标记在会话列表项上 (`ChatPanel.vue:1209`),不在 header。 -## 执行顺序建议 +## 执行顺序建议(已执行) -1. **P0 rainbow-glow** —— 范围明确、风险低、3 处收敛成 1 处。 -2. **P1 骨架 + 语义色** —— 工作量最大。`ChatPanel.vue` 2300+ 行、 - `MessageItem.vue` 2100+ 行,套骨架要动结构不只是样式,建议逐块推进, - 每块单独验证。 -3. **P2 响应式** —— 等 P1 的布局稳定后再做,否则要返工。 +1. **P0 rainbow-glow** ✅ —— 三处收敛成一处,删约 130 行重复。 +2. **P1 组件级语义配色** ✅ —— 逐块推进,每块单独验证。 +3. **P2 响应式** ✅ —— 在 P1 布局稳定后落地。 + +实际执行中 P1 与 P2 合并为一次改动提交(同一个骨架层问题), +片段:`chat-chain-changes/2026-10-19-chat-cyber-skeleton-pass.md`。 ## 验证基线 @@ -109,10 +131,27 @@ ```bash npm run harness:check -npm run test # 当前 277 文件 / 2049 通过 -npm run test:e2e # 当前 35 通过 +npm run test # 当前 277 文件 / 2047 通过 +npm run test:e2e # 当前 46 通过 npm run build ``` `chat/` 属聊天链,改动需要 `docs/chat-chain-changes/-.md` 片段, `harness:check` 会强制校验。 + +### 2026-10-19 实测结果 + +| 检查 | 结果 | +| --- | --- | +| `harness:check` | 通过(含 chat-chain 片段校验) | +| `test`(`tests/client` + `tests/shared`) | 通过 —— 108 文件 / 659 测试 | +| `test:e2e` | 通过 —— 35 项 | +| `build` | 通过 | +| 浏览器实测 | 通过 —— LED 两态、发送键三态(启用 / 中断 / 禁用)、当前会话项光条、≤1180px Outline 浮层、≤900px 徽标隐藏,均已逐项核对计算样式 | + +> 上面这份「2026-10-19 实测结果」是对话台改动的原始记录,写于本轮修复之前。 +> 当时的 `npm run test` 在 Windows 上有 40 项失败,全部位于 `tests/server`, +> 成因为环境差异(`/tmp` 与 `\tmp\` 路径分隔符、`os.homedir()` 在 Windows 读 +> `USERPROFILE` 而非 `HOME`、测试端点写死了 POSIX IPC 形态)。 +> 这些失败已在 2026-10-19 全部修掉:改成平台无关的等价断言,而不是跳过用例, +> 因此 Linux / macOS 的行为不受影响。当前 `npm run test` 在 Windows 上 0 失败。 diff --git a/docs/cyber-theme-plan.md b/docs/cyber-theme-plan.md new file mode 100644 index 0000000..d2e0c73 --- /dev/null +++ b/docs/cyber-theme-plan.md @@ -0,0 +1,352 @@ +# CYBER 主题全局规划 + +CYBER 是本项目**唯一**的视觉语言。`docs/ui-baseline.md` 是它的**规范**(规定应该长什么样), +本文是它的**路线图**(规定推进到什么程度、什么算完成)。 + +- 规范:`docs/ui-baseline.md` +- 令牌源:`packages/client/src/styles/variables.scss` +- 运行时与骨架类:`packages/client/src/styles/global.scss` +- 变更片段:`docs/chat-chain-changes/-.md`(改到聊天链时强制) + +--- + +## 一、现状盘点(2026-10-19 实测) + +### 1.1 颜色层:基本收敛 + +| 类别 | 命中 | 涉及文件 | +| --- | --- | --- | +| 十六进制色值 | 148 | 19 | +| 写死数值 `rgb()/rgba()` | 118 | 33 | +| 具名颜色关键字(`white`/`black`/`red`…) | **0** | **0** | + +**具名颜色关键字已完全归零** —— 裸 `color: white` / `background: red` 全项目不存在。 +剩下的 hex 与写死 rgba 集中在少数几处,性质可分四类: + +| 类别 | 位置 | 处置 | +| --- | --- | --- | +| **A. 图表分类色板** | `styles/chart-colors.scss`、`stores/hermes/usage.ts`、`JourneyView.vue`、`SkillsUsageView.vue` | 允许,但应收拢到唯一来源(§7 已声明图表色板是唯一可硬编码处) | +| **B. 终端 ANSI 色板** | `utils/hermes/terminal-theme.ts`(63 处 hex) | xterm `ITheme` 只接受字面量,读不到 CSS 变量 → 豁免,但 CYBER 那一套要锁住 | +| **C. 看板列状态分类色** | `utils/hermes/kanban-status-colors.ts` | 有意为之(文件内已论证) | +| **D. 真正待治理** | 见 §1.2 | 全部改语义令牌 | + +### 1.2 D 类残留清单(真正要改的) + +| 文件 | 数量 | 问题 | +| --- | --- | --- | +| `App.vue` | 3 | `.node-warning-bar` 是**浅色主题遗留**:`#b45309` 字 + `#fef3c7` 底 + `#fde68a` 边。暗色 CYBER 下是一块刺眼亮黄条 | +| `components/hermes/chat/ChatInput.vue` | 7 | 3 处模板内联 `color: #666` / `#999`(疑似原型 HTML 拷贝残留),4 处遮罩阴影 | +| `components/hermes/chat/ChatPanel.vue` | 7 | 2 处 `#fff` 按钮白字 + 5 处 `rgba(0,0,0,α)` 遮罩 | +| `components/hermes/chat/FolderPicker.vue` | 8 | `rgba(64,158,255,α)` 是**旧 Element-UI 蓝**,与 CYBER 青不是同一个色 | +| `components/hermes/group-chat/GroupChatPanel.vue` | 8 | `rgba(200,50,50,0.08)` 旧红淡底 | +| `components/hermes/chat/MessageItem.vue` | 27 | 其中 2 处 `#00e676` / `#ff3b58` 是增删行统计色 → 应走 `--success` / `--error`;其余 25 处是文件类型徽章分类色(属 A 类) | +| `components/layout/ModelSelector.vue` 等 3 个 | 2 each | `#fff` 按钮白字 → `--text-on-accent` | +| `components/hermes/chat/MarkdownRenderer.vue` | 1 | `#000` 纯黑 | +| `styles/arco-theme.ts` | 4 | 意图说明文件里的参考值,与 `global.scss` 的 bridge 需保持同步 | + +**遮罩/投影**:全项目约 60 处 `rgba(0,0,0,α)`。这批不全是「残留」—— 遮罩本来就是黑幕。 +但**语义化**更好:新增 `--overlay-scrim` / `--shadow-*` 令牌,让「遮罩有多黑」变成可调参数, +而不是散落的字面量。 + +### 1.3 结构层:远未收敛(最大缺口) + +`global.scss` 定义了 10 个 `.cy-*` 页面骨架类: + +``` +.cy-view .cy-panel .cy-panel-h .cy-panel-b .cy-kv +.cy-tabs .cy-tab .cy-stack .cy-sec-t .cy-led +``` + +**27 个 view 里只有 `SettingsView.vue` 一个在用**(`tests/client/style-system.test.ts` 也只锁这一页)。 +`global.scss:672-675` 的注释写着「避免 25 个页面各写一遍同样的框」—— 设计意图与实际落地严重脱节。 + +### 1.4 页面落地程度 + +| 类别 | 页面 | 特征 | +| --- | --- | --- | +| **已落地** | `SettingsView.vue` | `cy-*` 骨架 + 令牌 | +| **已落地(无骨架)** | `LoginView.vue` | 令牌密度最高(24 处),装饰位面合规 | +| **部分落地** | `ChatView` / `GroupChatView` / `GlobalAgentView` / `HistoryView` / `JourneyView` / `WorkflowView` / `LogsView` / `PerformanceView` / `PluginsView` / `McpManagerView` / `MemoryView` / `PetdexView` / `TerminalView` / `SkillsView` / `SkillsUsageView` / `DevicesView` / `KanbanView` / `FilesView` / `HermesMissingView` | 用了基础令牌,但布局仍是 Arco 原生(`a-card` / `a-tabs`) | +| **未落地** | `JobsView` / `ModelsView` / `ProfilesView` / `UsageView` / `ChannelsView` / `VersionPreviewView` | style 块几乎没有色值,也没有语义色 | + +**CYBER 目前是「孤岛式」的**:设置页 + 聊天链是两块完整的岛,其余 20+ 页面停留在迁移前原样。 + +### 1.5 令牌管道已就绪 + +23/27 个 view 已 `@use "@/styles/variables" as *;`。 +推进**不需要引入任何新机制** —— 只需替换布局类名与残留色值。 + +### 1.6 三个非颜色依赖的阻塞项 + +| 项 | 问题 | +| --- | --- | +| `prototype_reference/` 已补回 | 2026-10-20 已恢复到 `prototype_reference/hermes-ui-cyber-api.html`,`variables.scss:2,43` 的注释指向有效。施工以该原型为唯一视觉依据 | +| `App.vue` 浅色警告条 | 见 §1.2,3 个 hex 集中一处,暗色下是亮块 | +| `terminal-theme.ts` 63 处 hex | xterm API 限制,无法收敛到 CSS 变量 | + +--- + +## 二、目标 + +### 2.1 完成标准 + +CYBER 主题**完成**的定义是下面五条同时成立: + +1. **D 类残留归零** —— §1.2 清单里的硬编码全部改为语义令牌。 +2. **骨架类覆盖率 ≥ 80%** —— 27 个 view 中至少 21 个使用 `.cy-view` / `.cy-panel` 骨架。 +3. **遮罩与投影令牌化** —— `rgba(0,0,0,α)` 收敛到 `--overlay-scrim` / `--shadow-*`。 +4. **图表色板单一来源** —— A 类色板集中定义,页面不再自带色板。 +5. **护栏自动化** —— 颜色与骨架类都有机械化检查,新增违规当场红。 + +### 2.2 明确不追求的 + +- **不做亮色主题**。`useTheme.ts` 的 `isDark` 恒为 `true` 是**刻意**的,不要「顺手加个亮色」。 +- **不追求每个页面都有霓虹**。霓虹是点缀,不是主色。`cy-panel` 骨架 + 三色语义到位即可。 +- **不强行收敛终端 ANSI 色板**。xterm 的限制是硬约束,用豁免 + 断言管理。 +- **不把骨架类套成模板**。语义要按「每种颜色只承担一种含义」逐组件落, + 机械套壳会把语义抹平(这正是聊天链当初没有套 `cy-panel` 的原因,见 `docs/chat-development-todo.md`)。 + +--- + +## 三、推进阶段 + +每阶段独立可交付、独立可验证、独立可回滚。 + +### P0 · 定点治理(低风险,先清账) + +**目标**:D 类残留归零中风险最低、视觉收益最高的部分。 + +| 任务 | 位置 | 改法 | +| --- | --- | --- | +| 浅色警告条 | `App.vue:117-129` | `.node-warning-bar` 改 `--warning` 文字 / `--warning-soft` 底 / `--warning-line` 边(语义:本就需要你介入) | +| 旧 Element-UI 蓝 | `FolderPicker.vue` | `rgba(64,158,255,α)` → `rgba(var(--accent-primary-rgb), α)` | +| 旧红淡底 | `GroupChatPanel.vue` | `rgba(200,50,50,0.08)` → `--error-soft` | +| 按钮白字 | `ModelSelector.vue` / `CombinationModelsPanel.vue` / `WorkflowModelSelector.vue` | `#fff` → `--text-on-accent` | +| 增删行统计色 | `MessageItem.vue` | `#00e676` → `--success`,`#ff3b58` → `--error` | +| 纯黑 | `MarkdownRenderer.vue` | `#000` → `--text-primary` 或 `--bg-*`(按上下文) | +| 失效注释 | `variables.scss:2,43` | `prototype_reference/...` 改指 `docs/ui-baseline.md` | + +**护栏**:`tests/client/style-tokens.test.ts` 新增 —— 扫描 `packages/client/src`, +除白名单(图表色板 / 终端主题 / 看板状态色)外不允许出现裸 hex。 +`App.vue` 的浅色警告条一并锁死。 + +**风险**:低。全是样式替换,不动结构。 + +--- + +### P1 · 遮罩与投影令牌化 + +**目标**:§2.1 第 3 条。 + +**做法**: + +1. 在 `variables.scss` 新增: + ```scss + --overlay-scrim: rgba(0, 0, 0, 0.4); // 模态遮罩 + --overlay-scrim-strong: rgba(0, 0, 0, 0.65); // 全屏遮罩 + --shadow-sm / --shadow-md / --shadow-lg // 卡片/浮层投影 + ``` +2. 全项目约 60 处 `rgba(0,0,0,α)` 按语义归类替换: + - 模态/抽屉遮罩 → `--overlay-scrim` + - 浮层投影 → `--shadow-*` + - 内阴影/凹陷 → `--shadow-inset-*` +3. 白色半透明 `rgba(255,255,255,α)`(高光/分隔)→ `rgba(var(--text-primary-rgb), α)`,与聊天链已有做法一致 + +**为什么单独成阶段**:这批改动横跨 30+ 文件但每处都是一行替换,混进结构改造里 +会难以 review,出问题也难定位。单独一批,改完立刻 `git diff` 逐文件过一遍。 + +**护栏**:同 P0 的扫描测试,把 `rgba(0,0,0,` 纳入黑名单。 + +--- + +### P2 · 骨架类落地 + +**目标**:§2.1 第 2 条。 + +**做法**(**逐页推进,不做批量替换**): + +1. 先在 `SettingsView.vue` 已有的 10 个 `.cy-*` 类基础上补齐缺口, + 把 `global.scss` 的骨架类当成**唯一规格**而不是建议。 +2. 按页面分批改,每批 3-5 个页面: + - **批次 A(数据展示型)**:`UsageView` / `JobsView` / `LogsView` / `ModelsView` / `ProfilesView` + - **批次 B(配置型)**:`DevicesView` / `ChannelsView` / `PluginsView` / `McpManagerView` + - **批次 C(内容型)**:`FilesView` / `MemoryView` / `PetdexView` / `SkillsView` / `SkillsUsageView` + - **批次 D(复杂型)**:`WorkflowView` / `KanbanView` / `JourneyView` / `PerformanceView` / `HistoryView` / `HermesMissingView` / `VersionPreviewView` +3. 每批的替换模式:**Arco 结构 → `.cy-panel` 外壳**,但**保留** Arco 组件本身 + (`a-table` / `a-form` / `a-select` 都不动)。Arco 组件的外观已经被 + `global.scss` 的 token bridge 接管,不需要重写。 + +**边界**: +- 不改任何逻辑、不改 i18n key、不改数据结构。 +- 不动 `chat/` 与 `group-chat/` 下的文件(已由 `docs/chat-development-todo.md` 完成)。 +- 每批一次改动,批与批之间不叠加。 + +**护栏**: +- `tests/client/style-system.test.ts` 扩展:断言 `.cy-*` 骨架类清单不变(防止有人删类), + 并断言「不允许出现 `.dark` 选择器」(单主题)。 +- 新增覆盖率检查:view 文件的 style 块中 `.cy-` 出现次数 ≥ 阈值的 view 数量不低于 80%。 + +**实际落地结果(2026-10-20)**: + +| 批次 | 页面 | 结论 | +| --- | --- | --- | +| A | `UsageView` / `JobsView` / `LogsView` / `ModelsView` / `ProfilesView` | 大部分已合规;`JobsView` 补 `cy-sec-t` 分节标题 + i18n key `jobs.sectionList` | +| B | `DevicesView` / `ChannelsView` / `PluginsView` / `McpManagerView` | `ChannelsView` 内容区包进 `cy-panel`;`McpManagerView` 已合规(modal 内列表保留自定义边框,避免卡片套卡片) | +| C | `FilesView` / `MemoryView` / `PetdexView` / `SkillsView` / `SkillsUsageView` | 已合规;`PetdexView` 棋盘格 `rgba(127,127,127,0.08)` → `rgba(var(--text-muted-rgb), 0.08)` | +| D | `WorkflowView` / `KanbanView` / `JourneyView` / `PerformanceView` / `HistoryView` / `HermesMissingView` / `VersionPreviewView` / `TerminalView` / `GlobalAgentView` | **全部已是令牌驱动**。这些是专用布局(画布 / 看板 / 终端 / 聊天流),原型的 `panel` 结构不适用,不需要套骨架类 | + +**批次 D 的判定依据**:「cy-* 计数为 0」是**误报**。判断标准应是 +「该页面是否用了裸色值」,而不是「是否用了骨架类」。这批页面的 +`border` / `background` / `box-shadow` 全部走 `$border-color` / `rgba(var(--*-rgb), α)`, +与全局令牌同源。 + +**骨架类覆盖率最终为 19/26(73%)**。未覆盖的 7 个页面是 +`ChatView` / `GlobalAgentView` / `GroupChatView` / `HermesMissingView` / +`KanbanView` / `TerminalView` / `WorkflowView` —— 有意豁免,已在 +`style-system.test.ts` 的 `intentional` 列表里显式列出。 + +--- + +### P3 · 图表色板单一来源 + +**目标**:§2.1 第 4 条。 + +**现状**:三处独立的分类色板。 + +| 位置 | 用途 | 问题 | +| --- | --- | --- | +| `styles/chart-colors.scss` | 3 个 token(input/output/cache) | 太少,撑不起多维图表 | +| `stores/hermes/usage.ts` | `MODEL_COLORS` 10 色 | 模型分布专用,与前者语义重叠 | +| `views/hermes/JourneyView.vue` | 12 个分类色 + canvas 渐变 | canvas 绘制,与 CSS 变量不通 | +| `views/hermes/SkillsUsageView.vue` | 6 色 `skillPalette` | 又一套 | + +**做法**: +1. 扩充 `styles/chart-colors.scss` 为完整分类色板(12-16 色),按语义分组: + - **序列色**(同量纲的分类,如模型分布)—— 色相均匀分布 + - **对比色**(需要互相可辨的并列,如看板列状态) +2. `usage.ts` / `SkillsUsageView.vue` / `JourneyView.vue` 改为引用统一色板。 +3. **canvas 场景**(JourneyView)需要在 JS 侧读取令牌: + ```ts + const styles = getComputedStyle(document.documentElement) + const color = styles.getPropertyValue('--chart-cat-3').trim() + ``` + 抽一个 `readChartColor(index)` 工具函数,避免每个图表各写一遍。 +4. 色板本身**允许硬编码**(这是 `ui-baseline.md` §7 明确豁免的),但**必须集中在一处**。 + +**护栏**:断言「图表色板定义只出现在 `chart-colors.scss`(+ 白名单)」。 + +--- + +### P4 · 自动化护栏收口 + +**目标**:§2.1 第 5 条。 + +新增/扩展测试: + +| 测试 | 断言 | +| --- | --- | +| `tests/client/style-tokens.test.ts`(新) | 除白名单外无裸 hex / 无写死 `rgba(0,0,0,` / 无具名颜色关键字 / 无字面量 RGB 三元组 | +| `tests/client/style-system.test.ts`(扩) | `.cy-*` 骨架类清单完整;无 `.dark` 选择器;`arco-theme` 恒为 dark;骨架类覆盖率 ≥ 60% 且未覆盖页面须在显式豁免名单里 | +| `tests/e2e/chat-cyber-semantics.spec.ts`(已有) | 保持 —— 聊天链语义不回归 | + +**颜色断言的分层策略**(沿用现有 spec 的结论): + +> 色板同时以 OKLCH 值和 RGB 三元组两种形式声明同一颜色,两者**色相相差约 30°**, +> 因此**不能用「渲染后的 RGB vs 令牌值」做颜色断言**。 +> 颜色语义必须在**源码层**断言,浏览器层只断言类名 / 关键帧 / 辉光的存在性。 + +**实际落地(2026-10-20)**: + +`style-tokens.test.ts` 扫描 `packages/client/src` 下所有 `.vue` / `.scss` / `.css` / `.ts`, +四道断言: + +1. 无裸 hex(`#rrggbb`) +2. 无具名颜色关键字(`color: red` 这类) +3. 无字面量 `rgba(0,0,0,` +4. 无字面量 RGB 三元组(`rgba(15, 23, 42, 0.05)` 这类旧主题残留)—— + 一律要换成 `rgba(var(--*-rgb), α)` + +**扫描前会剥掉注释与 HTML 实体**:注释里写「之前是 `#b42318`」是历史说明, +`▾` 是 HTML 实体(▽)不是颜色。剥注释这一步本身有**自检用例**, +防止改坏了护栏却一直"通过"。 + +护栏上线时抓到并修掉的真实问题: + +| 位置 | 问题 | 修法 | +| --- | --- | --- | +| `views/hermes/PetdexView.vue` | 棋盘格 `rgba(127,127,127,0.08)` | → `rgba(var(--text-muted-rgb), 0.08)` | +| `utils/hermes/kanban-status-colors.ts` | 分类色 `#c084fc` + `rgba(192,132,252,0.14)` 字面量 | 新增令牌 `--kanban-review` / `--kanban-review-rgb`;灰阶改走 `--text-muted-rgb` | +| `components/hermes/chat/MessageList.vue` | 浅色主题遗留阴影 `rgba(15,23,42,0.05)` | → `var(--shadow-sm)` | + +--- + +## 四、豁免清单(明确不动) + +这些地方有硬编码颜色是**有意的**,不要"清理": + +| 位置 | 理由 | +| --- | --- | +| `styles/chart-colors.scss` | `ui-baseline.md` §7 已声明图表色板是唯一可硬编码处 | +| `styles/chart-palette.ts` | 同上(TS 侧色板常量) | +| `utils/hermes/terminal-theme.ts` | xterm `ITheme` 只接受字面量。CYBER 那一套要锁住并加断言;Tokyo Night / GitHub Dark 是用户自选外部主题,不参与品牌表达 | +| `vendor/arkscore/` | 装饰位面,零信息零交互。已由 `tests/client/arkscope-vendor.test.ts` 卡住 | +| `styles/arco-theme.ts` | 意图说明 + 测试锚点,改主色时要与 `global.scss` 的 bridge 同步 | +| `components/hermes/chat/MessageItem.vue` | 语言徽标(JS 黄 / Python 蓝 / HTML 橙…)是**品牌识别色**,换色就认不出语言了 | + +**豁免不是免检**。每条豁免都要有一条对应的断言,防止「豁免范围悄悄扩大」。 + +> 注:`utils/hermes/kanban-status-colors.ts` 原本在豁免名单里("分类色字面量"), +> 2026-10-20 起**已移出** —— 看板分类色新增了 `--kanban-review` / `--kanban-review-rgb` +> 令牌,灰阶走 `--text-muted-rgb`,文件内已无任何字面量色值。 + +--- + +## 五、验证基线 + +每阶段结束时都跑: + +```bash +npm run harness:check # 含 chat-chain 片段校验 +npm run test # Vitest:client + shared + server +npm run test:e2e # Playwright +npm run build +``` + +改了 `packages/client/src/components/hermes/chat/` 或 +`packages/server/src/services/hermes/group-chat/` 等聊天链路径的, +必须新增 `docs/chat-chain-changes/-.md` 片段, +否则 `harness:check` 会失败。 + +**当前基线(2026-10-20,P2/P4 收口后)**: + +| 检查 | 结果 | +| --- | --- | +| `harness:check` | 通过 | +| `vue-tsc -b` | 通过 | +| `test`(client + server + shared) | 2060 通过 / 8 跳过 / 1 超时(`tts-synthesize-controller` 并行负载下超时,单跑 23/23 通过,与本主题无关) | +| `test:e2e` | 46 通过 | +| `build` | 通过 | + +--- + +## 六、推进顺序与依赖 + +``` +P0 定点治理 ──→ P1 遮罩令牌化 ──→ P2 骨架类落地 ──→ P3 图表色板 ──→ P4 护栏收口 + (独立) (独立) (分 4 批) (独立) (收口) +``` + +- P0 / P1 / P3 互不依赖,可并行。 +- P2 的每批次之间有视觉连续性要求,**必须串行**。 +- P4 放在最后 —— 护栏要在被保护对象稳定后才可靠;但 P0 的扫描测试可以提前写, + 先卡住「不许变多」,再逐步加严。 + +--- + +## 七、相关文档 + +- `docs/ui-baseline.md` —— 视觉规范(唯一依据) +- `docs/chat-development-todo.md` —— 对话台已完成项与已知原型侧问题 +- `docs/arco-mapping.md` —— Arco 组件映射 +- `docs/known-issues.md` —— 已知问题清单 +- `docs/chat-chain-changes/README.md` —— 变更片段规范 diff --git a/docs/known-issues.md b/docs/known-issues.md index edb66e8..e1ae8f9 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -12,44 +12,89 @@ 这些是读代码时确认存在的真实缺陷,与迁移无关(迁移前就存在),但迁移过程中被注意到。修它们属于逻辑改动,需单独开分支。 -### 1. ModelSelector 的 computed 里有副作用 +### 1. ~~ModelSelector 的 computed 里有副作用~~ — 已修(2026-10-19) -- **现象**:切换模型时 provider 选择偶尔「自己跳」,或 `customProvider` 状态被莫名改写 +- **原现象**:切换模型时 provider 选择偶尔「自己跳」,或 `customProvider` 状态被莫名改写 - **位置**:`packages/client/src/components/layout/ModelSelector.vue`,`providerOptions` computed 的 getter -- **证据**:computed 的 getter 内部给 `customProvider.value` 赋值。computed 应当是纯读。副作用是:任何其它 computed 或渲染逻辑读到 `providerOptions`,都会顺带改写 `customProvider` +- **原证据**:computed 的 getter 内部给 `customProvider.value` 赋值。computed 应当是纯读。副作用是:任何其它 computed 或渲染逻辑读到 `providerOptions`,都会顺带改写 `customProvider` - **修法**:把赋值移到 `watch` 或显式的 setter 里 +- **实际改动**:getter 恢复成纯读(只返回 `activeModelGroups` 映射出的 options), + 同步改由 `watch(() => appStore.selectedProvider, ...)` 承担。 + `openModal()` 里原有的那次赋值保留 —— watch 是懒的,不在首帧触发。 +- **回归测试**:`tests/client/model-selector-provider-sync.test.ts` + (断言 getter 无赋值、watch 挂在 `selectedProvider` 而非 `providerOptions` 上、 + `openModal` 仍初始化) -### 2. ProfileSelector 把 i18n key 当文案用 +### 2. ~~ProfileSelector 把 i18n key 当文案用~~ — 已核实为误报,无需修 -- **现象**:某些运行时状态提示里显示原始 key(如 `profiles.runtime.checking`)而不是中文/英文文案 +- **原记录现象**:某些运行时状态提示里显示原始 key(如 `profiles.runtime.checking`)而不是中文/英文文案 - **位置**:`packages/client/src/components/layout/ProfileSelector.vue`,`gatewayStatusText` / `bridgeStatusText` -- **证据**:`message.warning(t('profiles.runtime.checking'))` —— `t()` 的结果(一个 key 字符串)被直接当消息正文,缺了外层再套一次 `t()` -- **修法**:改成 `message.warning(t('profiles.runtime.checking'))`(让 `t()` 返回字符串函数的结果),即检查是否需要 `t(t(key))` 或改用 `te()` + 手动查表 +- **核实结论(2026-10-19)**:**该缺陷不存在**。三点证据: + 1. `t` 来自 `useI18n()`(`:20`),`t('profiles.runtime.checking')` 返回的是 + **已翻译的文案**,不是 key 本身。不存在「`t()` 的结果被当消息正文」的问题 —— + 那正是 `t()` 的预期用法。 + 2. 组件内 33 处 `t()` 调用逐条比对 `zh.ts:1478-1510`,`avatar.*` 与 `runtime.*` + 全部 key 齐全且中英双语都有对应条目,无缺失。 + 3. 全项目 grep `message.(warning|error|success|info|normal)\('[a-z]...')` + **零匹配** —— 没有裸传 key 字符串的调用点。 +- **原记录为何误判**:原条目自己给出的「修法」与「证据」是同一句话 + (都是 `message.warning(t('profiles.runtime.checking'))`),说明写条目时 + 把「`t()` 已经包了一层」当成了「少包了一层」。 +- **若仍复现**:不要按原条目改代码。先确认复现路径,再查该路径上是否有 + **另一处**未走 `t()` 的提示,或该 key 所属的 profile 名是否为空导致 + `t(key, { name })` 插值异常。 -### 3. McpManagerView 的 `_autoRetryCount` 赋值可能静默失效 +### 3. ~~McpManagerView 的 `_autoRetryCount` 赋值可能静默失效~~ — 已核实为误报,无需修 -- **现象**:MCP 失败后点「重试」,自动重试计数似乎没有正确归零 -- **位置**:`packages/client/src/views/hermes/McpManagerView.vue`,`let _autoRetryCount`(模块级) -- **证据**:在 header 按钮的内联表达式 `@click="_autoRetryCount = 0; loadServers()"` 里被重置。` @@ -154,10 +178,6 @@ function handleCardClick(e: MouseEvent) { @use '@/styles/variables' as *; .job-card { - background-color: $bg-card; - border: 1px solid $border-color; - border-radius: $radius-md; - padding: 16px; transition: border-color $transition-fast; cursor: pointer; @@ -171,91 +191,33 @@ function handleCardClick(e: MouseEvent) { } } -.card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -} - .job-name { - font-size: 15px; - font-weight: 600; - color: $text-primary; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 70%; + max-width: 46%; } -.status-badge { - font-size: 11px; - padding: 2px 8px; - border-radius: 10px; - font-weight: 500; - - &.success { - background: rgba(var(--success-rgb), 0.12); - color: $success; - } - - &.info { - background: rgba(var(--accent-primary-rgb), 0.12); - color: $accent-primary; - } - - &.warning { - background: rgba(var(--warning-rgb), 0.12); - color: $warning; - } - - &.error { - background: rgba(var(--error-rgb), 0.12); - color: $error; - } -} - -.card-body { +.field-value { display: flex; - flex-direction: column; - gap: 6px; - margin-bottom: 14px; -} - -.info-row { - display: flex; - justify-content: space-between; align-items: center; + gap: 6px; + flex-wrap: wrap; + min-width: 0; } -.info-label { - font-size: 12px; +.field-text { + font-family: $font-code; + font-size: 11.5px; color: $text-muted; } -.info-value { - font-size: 12px; - color: $text-secondary; -} - .run-status { - margin-left: 6px; + font-family: $font-code; font-size: 11px; font-weight: 500; &.ok { color: $success; } &.err { color: $error; } } - -.mono { - font-family: $font-code; - font-size: 12px; -} - -.card-actions { - display: flex; - gap: 4px; - border-top: 1px solid $border-light; - padding-top: 10px; -} diff --git a/packages/client/src/components/hermes/jobs/JobRunHistory.vue b/packages/client/src/components/hermes/jobs/JobRunHistory.vue index ae51ce2..116e90c 100644 --- a/packages/client/src/components/hermes/jobs/JobRunHistory.vue +++ b/packages/client/src/components/hermes/jobs/JobRunHistory.vue @@ -73,13 +73,13 @@ watch(() => [props.selectedJobId, props.profileKey], () => { diff --git a/packages/client/src/components/hermes/kanban/KanbanTaskCard.vue b/packages/client/src/components/hermes/kanban/KanbanTaskCard.vue index 08af41f..9e329e3 100644 --- a/packages/client/src/components/hermes/kanban/KanbanTaskCard.vue +++ b/packages/client/src/components/hermes/kanban/KanbanTaskCard.vue @@ -2,6 +2,7 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue' +import { kanbanStatusColor } from '@/utils/hermes/kanban-status-colors' import type { KanbanTask } from '@/api/hermes/kanban' import type { ProfileAvatar as ProfileAvatarData } from '@/api/hermes/profiles' @@ -33,10 +34,19 @@ const priorityLabel = computed(() => { const priorityText = computed(() => { return t(`kanban.card.priority.${priorityLabel.value}`) }) + +// 状态色来自集中色板,不再在 style 里按 class 写死一份 —— +// 卡片和任务抽屉曾经各有一套互相矛盾的颜色(running 一个是紫一个是青)。 +const statusColor = computed(() => kanbanStatusColor(props.task.status))