Phase 006: migrate the chat page from naive-ui to Arco Design Vue
Swaps the rendering layer only. Chat store logic, keyboard handling, and the /chat-run Socket.IO contract are untouched, so a mixed UI cannot hide a chat regression behind a library swap. Arco's dropdowns take their items from a #content slot of a-doption children rather than a droplist prop, and Arco Triggers cannot be nested, so the two toolbar menus lost their a-tooltip in favour of a native title. Both rules are recorded in docs/arco-mapping.md, and tests/e2e/chat-input-dropdown.spec.ts plus tests/e2e/chat-rendering-smoke.spec.ts keep the menus, the streaming markdown rendering, and the no-Arco-inside-the-bubble red line from regressing silently.
This commit is contained in:
+23
-4
@@ -21,13 +21,15 @@ Phase 005 起,新代码与迁移代码一律使用 Arco Design Vue。本文表
|
||||
|
||||
| Naive | Arco | 用量 | 备注 |
|
||||
|---|---|---|---|
|
||||
| `NButton` | `a-button` | 732 | `block`→`long`、`size="tiny"`→`size="mini"`、`type="warning"`→`type="primary" status="warning"`、`quaternary`→`status="secondary"` |
|
||||
| `NButton` | `a-button` | 732 | `block`→`long`、`size="tiny"`→`size="mini"`、`type="warning"`→`type="primary" status="warning"`、`quaternary`/`secondary`→`type="secondary"`(Arco `status` 只有 `normal\|success\|warning\|danger`,没有 `secondary`) |
|
||||
| `NInput` | `a-input` / `a-textarea` | 160 | `type="textarea"` 用独立 `a-textarea`;`a-input` 关闭了 attrs 透传,`autofocus` 无效,需 `ref` + `focus()` |
|
||||
| `NModal` | `a-modal` | 138 | `v-model:show`→`v-model:visible`;`preset="dialog"` 无对应,信息型弹窗用 `:footer="false"`(`footer` 为 Boolean prop,默认 true) |
|
||||
| `NFormItem` | `a-form-item` | 136 | 校验规则 API 不同(`rules` 结构、`required` 表达式)需适配 |
|
||||
| `NSelect` | `a-select` | 97 | `options` 的 `label/value` 语义一致;`consistent-menu-width` 等 prop 无对应 |
|
||||
| `NSpin` | `a-spin` | 86 | `show`→`loading` |
|
||||
| `NTooltip` | `a-tooltip` | 76 | `trigger="hover"` 默认一致;`placement` 语义基本一致 |
|
||||
| `NTooltip` | `a-tooltip` | 76 | `trigger="hover"` 默认一致;`placement`→`position`;**触发元素走默认插槽**,naive 的 `<template #trigger>` 必须拆掉 |
|
||||
| `NPopconfirm` / `NTooltip` 的 `#trigger` 槽 | Arco 默认插槽 | — | Arco 的 `a-tooltip` / `a-popconfirm` / `a-dropdown` 都用默认插槽承载触发元素,naive 的 `<template #trigger>` 写法会静默丢内容 |
|
||||
| Arco Trigger 嵌套 | 不可嵌套,无论谁包谁 | — | `a-tooltip` / `a-dropdown` / `a-popconfirm` 内部都用 `mergeFirstChild` 找首个元素节点并各自接管事件;嵌套时两个 popup 容器互相踩,菜单**点不开**(`.arco-trigger-popup-wrapper` 卡在过渡中间态、选项数为 0)。同一触发元素要同时有说明文案和下拉时,说明文案改用原生 `:title`,不要套 `a-tooltip` |
|
||||
| `NTag` | `a-tag` | 57 | `bordered` prop 存在(默认 true);`size="small"` 一致;naive `type="info"`→`color="arcoblue"` 等内建色(`red/orange/green/arcoblue/...`),自定义色用 `color` 传 CSS 颜色 |
|
||||
| `NSwitch` | `a-switch` | 53 | `value`/`v-model` 一致 |
|
||||
| `NSpace` | `a-space` | 51 | `size` 语义一致(数字/预设名) |
|
||||
@@ -36,7 +38,7 @@ Phase 005 起,新代码与迁移代码一律使用 Arco Design Vue。本文表
|
||||
| `NForm` | `a-form` | 32 | 校验规则 API 不同,需适配 |
|
||||
| `NTabPane` | `a-tab-pane` | 30 | `NTabs/NTabPane` → `a-tabs/a-tab-pane` |
|
||||
| `NDrawer` / `NDrawerContent` | `a-drawer` | 27 | Arco 无独立 content 组件;`footer` slot 用 `a-drawer` 的 `footer` |
|
||||
| `NDropdown` | `a-dropdown` | 25 | `options` 结构不同(`label`→`content`,需转换函数) |
|
||||
| `NDropdown` | `a-dropdown` | 25 | **Arco 没有 `droplist` prop**:菜单内容必须写进 `#content` 插槽、用 `a-doption` / `a-dsubmenu` 声明。传 `:droplist` 会被静默忽略,弹层能打开但 `<ul class="arco-dropdown-list">` 是空的;子菜单用 `a-dsubmenu` + 自己的 `#content` 插槽 |
|
||||
| `NDescriptions` / `NDescriptionsItem` | `a-descriptions` / `a-descriptions-item` | 19 | 基本一致 |
|
||||
| `NAlert` | `a-alert` | 19 | `type` 一致;`show-icon` 一致 |
|
||||
| `NRadioButton` / `NRadioGroup` / `NRadio` | `a-radio`(`type="radio-button"`)/ `a-radio-group` | 32 | Arco radio-button 是 `a-radio` 的 `type` prop |
|
||||
@@ -50,7 +52,7 @@ Phase 005 起,新代码与迁移代码一律使用 Arco Design Vue。本文表
|
||||
| `NDataTable` | `a-table` | 4 | **列定义 API 差异最大**(`columns`/`render`/`slots`),逐例处理 |
|
||||
| `NSlider` | `a-slider` | 3 | 基本一致 |
|
||||
| `NScrollbar` | `a-scrollbar` | 3 | Arco 全局可用,无需组件包裹注册 |
|
||||
| `NPopselect` | `a-select`(`trigger-element`) | 3 | 无直接对应,改用 `a-dropdown` 或带 trigger 的 `a-select` |
|
||||
| `NPopselect` | `a-dropdown` | 3 | 无直接对应。实测 `a-dropdown` + `:droplist`(`key`/`content`)可完整替代 |
|
||||
| `NPopover` | `a-popover` | 3 | `trigger` 语义有差异 |
|
||||
| `NIcon` | `<icon-*>`(`@arco-design/web-vue/es/icon`) | 3 | Arco 图标是独立组件,需 import |
|
||||
| `NBadge` | `a-badge` | 3 | `value`→`count` |
|
||||
@@ -63,3 +65,20 @@ Phase 005 起,新代码与迁移代码一律使用 Arco Design Vue。本文表
|
||||
|
||||
- Phase 006:聊天页(`components/hermes/chat/`)——消息气泡内部**不用** Arco 组件(性能红线)
|
||||
- Phase 007:其余视图 + `stores/` + `composables/` 批量替换 + 删除 naive-ui 依赖
|
||||
|
||||
## 例外
|
||||
|
||||
- 聊天虚拟滚动区不用 `a-scrollbar` 包裹。`VirtualMessageList.vue` 直接用
|
||||
`vue-virtual-scroller` 的 `DynamicScroller` 独占滚动(原本也没有 `NScrollbar`),
|
||||
额外包一层滚动容器会破坏高度测量与 sticky 定位。
|
||||
- 测试里 Arco 组件的 stub 必须用 Arco 内部组件名(`Dropdown` / `Modal` / `Spin`),
|
||||
而不是模板里的 `a-*` 标签名:`app.use(ArcoVue)` 之后组件已全局注册,VTU 的
|
||||
`stubs` 只按内部名匹配,用 `ADropdown` 会静默不生效。
|
||||
- `ChatInput.vue` 的推理强度按钮、输入设置按钮放弃 `a-tooltip`,改用原生 `:title`。
|
||||
这两个按钮必须挂 `a-dropdown`,而 Trigger 不能嵌套(见上表同名行),因此失去
|
||||
Arco 的悬浮气泡;文案与 `aria-label` 都保留,信息量不变。
|
||||
- Arco(和 Vue 本身)不会对拼错的 prop 报错,未声明的 prop 会作为普通 attribute
|
||||
落到根元素上。所以「弹层能打开、但内容是空的」几乎总是 prop 写错而不是数据为空。
|
||||
排查手法:`el.__vueParentComponent.props` 看目标组件到底认不认这个 prop,
|
||||
再看 `el.outerHTML` 确认渲染出来的骨架。回归由
|
||||
`tests/e2e/chat-input-dropdown.spec.ts` 卡住。
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
date: 2026-10-17
|
||||
pr: pending
|
||||
feature: Phase 006 - chat page naive-ui to Arco Design Vue rendering migration
|
||||
impact: No chat-chain behavior change; chat components only swap naive-ui components for Arco equivalents. Chat run flow, chat store logic, keyboard handling, and the /chat-run Socket.IO contract are untouched.
|
||||
---
|
||||
|
||||
# Phase 006 — Chat page Arco migration
|
||||
|
||||
Phase: `docs/phases/phase_006.md`
|
||||
|
||||
## What Changed
|
||||
|
||||
Rendering-layer only, across `packages/client/src/components/hermes/chat/`:
|
||||
|
||||
- `ChatInput.vue` — `NPopselect` reasoning-effort picker and `NDropdown`
|
||||
settings menu became `a-dropdown` whose `#content` slot declares `a-doption`
|
||||
items; `NModal` became `a-modal` (`:footer="false"` for informational
|
||||
dialogs, custom `#footer` for confirm dialogs); `NInputNumber` became
|
||||
`a-input-number hide-button`. The two menu buttons use a native `:title`
|
||||
instead of `a-tooltip` because Arco Triggers cannot be nested.
|
||||
`useKeyboard` wiring and the textarea height auto-resize logic were not
|
||||
touched.
|
||||
- `ChatPanel.vue` — session context menu moved from `NDropdown trigger="manual"`
|
||||
to `a-dropdown trigger="contextMenu" align-point` wrapping `.session-items`
|
||||
(the three-level export submenu became nested `a-dsubmenu` / `a-doption`);
|
||||
4 dialogs, the new-chat drawer, 5 `NSelect`, 6 `NTooltip`, and the
|
||||
`NPopconfirm` confirmations became Arco equivalents.
|
||||
- `MarkdownRenderer.vue` — the text preview `NDrawer`/`NDrawerContent` pair
|
||||
became a single `a-drawer`, and it is now lazily mounted behind
|
||||
`v-if="textPreviewVisible"`. The component's public interface is unchanged
|
||||
because it is reused by 8 call sites (including Phase 007's GroupMessageItem).
|
||||
- `MessageItem.vue` — the tool-change `NDrawer`/`NDrawerContent` became a
|
||||
single `a-drawer`. It uses `:visible` + `@update:visible` rather than
|
||||
`v-model:visible` so the existing "refuse to close with unsaved changes"
|
||||
guard still runs.
|
||||
- `MessageList.vue` — approval / clarification overlay buttons and inputs.
|
||||
- `FolderPicker.vue` — folder tree context menu moved to
|
||||
`a-trigger trigger="contextMenu" align-point` with an `#content` slot;
|
||||
`NModal preset="dialog"` became `a-modal` with a custom `#footer`.
|
||||
- `SessionListItem.vue`, `SessionSearchModal.vue`, `TerminalPanel.vue`,
|
||||
`FilesPanel.vue` — remaining buttons, checkboxes, tooltips, popconfirms,
|
||||
selects, and spinners.
|
||||
|
||||
`VirtualMessageList.vue` already had no `NScrollbar` and keeps
|
||||
`vue-virtual-scroller` as the sole scroll owner (recorded as an exception in
|
||||
`docs/arco-mapping.md`).
|
||||
|
||||
`stores/hermes/chat.ts`, `stores/hermes/files.ts`, and `ChatView.vue` needed no
|
||||
changes.
|
||||
|
||||
## Contract Check
|
||||
|
||||
`/chat-run` event names, field names, field types, and emission timing are
|
||||
unchanged. No file under `packages/server/`, `services/hermes/group-chat/`,
|
||||
`lib/context-compressor/`, or `services/hermes/context-engine/` was modified,
|
||||
so no bridge, compressor, or context-engine behavior changed.
|
||||
|
||||
The §1 red line (no Arco components inside the message bubble) is enforced by
|
||||
`tests/client/message-item-arko.test.ts`, which registers the real Arco plugin
|
||||
and asserts that no `.arco-*` element appears in the rendered bubble for
|
||||
assistant, user, and tool messages, including across streaming re-renders.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
grep -cE "from ['\"]naive-ui['\"]" packages/client/src/components/hermes/chat/*.vue \
|
||||
packages/client/src/views/hermes/ChatView.vue \
|
||||
packages/client/src/stores/hermes/chat.ts
|
||||
npm run harness:check
|
||||
npm run test
|
||||
npm run test:e2e
|
||||
npm run build
|
||||
```
|
||||
|
||||
Result: grep 0 across all chat files; 274 test files / 2026 tests passing;
|
||||
35 e2e tests passing; harness check and build clean.
|
||||
|
||||
## Follow-up Fix
|
||||
|
||||
Arco's `a-dropdown` has no `droplist` prop — its menu content must be declared
|
||||
in the `#content` slot with `a-doption` / `a-dsubmenu` children. The first
|
||||
migration pass used `:droplist`, which Arco silently ignored (unknown props
|
||||
fall through as plain attributes), so all three dropdowns opened an empty
|
||||
popup. Fixed by declaring the options in the `#content` slot, and locked down
|
||||
by `tests/e2e/chat-input-dropdown.spec.ts`, which asserts the reasoning
|
||||
effort menu (7 options, selection applied), the input settings menu
|
||||
(2 options, toggle applied), and the session context menu plus its export
|
||||
submenu.
|
||||
|
||||
The §9.2 manual checklist rows that need a live run are covered by
|
||||
`tests/e2e/chat-rendering-smoke.spec.ts`: streamed deltas rendering bold text,
|
||||
inline code, a highlighted code block, KaTeX math, and a Mermaid diagram; the
|
||||
Stop button appearing on run start and clearing on `run.completed`; the
|
||||
"no Arco inside the bubble" red line; and the session model dialog plus session
|
||||
search opening from the chat UI.
|
||||
@@ -193,7 +193,7 @@ const message = useMessage() // 我们的 composable,签名一致
|
||||
```bash
|
||||
cd /Users/jack/source/MyProject/js/hermes-ui
|
||||
|
||||
grep -c "from 'naive-ui'" packages/client/src/components/hermes/chat/*.vue \
|
||||
grep -cE "from ['\"]naive-ui['\"]" packages/client/src/components/hermes/chat/*.vue \
|
||||
packages/client/src/views/hermes/ChatView.vue \
|
||||
packages/client/src/stores/hermes/chat.ts
|
||||
# 期望:全部 0
|
||||
|
||||
Reference in New Issue
Block a user