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