From 836d0228d25ece235a5bc7f04b7755bb90e4a23b Mon Sep 17 00:00:00 2001 From: Jack Date: Sun, 18 Oct 2026 22:57:25 +0800 Subject: [PATCH] Phase 007: migrate every remaining view from naive-ui to Arco Design Vue The project now has exactly one UI library. Every naive-ui component in packages/client/src is replaced by its Arco equivalent across 24 views and roughly 80 components, and naive-ui is dropped from package.json along with its transitive vueuc / @css-render / vdirs / seemly dependencies. The migration is rendering-layer only. No store logic, no API contract, and no /chat-run payload was touched. Three classes of leftover were invisible to `grep naive-ui` and are now covered by tests/client/no-naive-ui.test.ts, which asserts all three: - dead .n-* class names in style blocks - dead --n-* custom properties - naive-ui in package.json Notable behavioural traps handled along the way, recorded in docs/arco-migration-rules.md: - a-input-password defaults to masked; passing default-visibility="false" would have exposed API keys in clear text - a-upload fires a real XHR unless auto-upload is false - nested Trigger components fight over the first element child, so the group-chat settings dropdown was flattened - Arco renders tooltip default-slot text inline, which was silently collapsing the chat and group-chat toolbars --- docs/arco-migration-rules.md | 101 ++++ ...2026-10-18-phase007-arco-full-migration.md | 73 +++ package-lock.json | 204 -------- package.json | 1 - .../auth/DefaultCredentialPrompt.vue | 18 +- .../src/components/hermes/chat/ChatPanel.vue | 6 +- .../hermes/files/FileBreadcrumb.vue | 13 +- .../hermes/files/FileContextMenu.vue | 65 ++- .../components/hermes/files/FileEditor.vue | 13 +- .../src/components/hermes/files/FileList.vue | 13 +- .../components/hermes/files/FilePreview.vue | 7 +- .../hermes/files/FileRenameModal.vue | 36 +- .../components/hermes/files/FileToolbar.vue | 26 +- .../src/components/hermes/files/FileTree.vue | 45 +- .../hermes/files/FileUploadModal.vue | 33 +- .../hermes/group-chat/CreateRoomForm.vue | 44 +- .../hermes/group-chat/GroupChatInput.vue | 123 ++--- .../hermes/group-chat/GroupChatPanel.vue | 272 +++++----- .../src/components/hermes/jobs/JobCard.vue | 32 +- .../components/hermes/jobs/JobFormModal.vue | 120 +++-- .../components/hermes/jobs/JobRunHistory.vue | 25 +- .../components/hermes/kanban/KanbanColumn.vue | 22 +- .../hermes/kanban/KanbanCreateForm.vue | 145 ++++-- .../hermes/kanban/KanbanTaskCard.vue | 28 +- .../hermes/kanban/KanbanTaskDrawer.vue | 81 +-- .../components/hermes/mcp/McpServerCard.vue | 27 +- .../hermes/models/AnthropicLoginModal.vue | 41 +- .../hermes/models/AuxiliaryModelsPanel.vue | 62 ++- .../hermes/models/CodexLoginModal.vue | 34 +- .../hermes/models/CombinationModelsPanel.vue | 88 ++-- .../hermes/models/CopilotLoginModal.vue | 34 +- .../hermes/models/GeminiLoginModal.vue | 30 +- .../hermes/models/NousLoginModal.vue | 34 +- .../components/hermes/models/ProviderCard.vue | 94 ++-- .../hermes/models/ProviderFormModal.vue | 122 +++-- .../hermes/models/XaiOAuthLoginModal.vue | 30 +- .../hermes/profiles/ProfileCard.vue | 36 +- .../hermes/profiles/ProfileCreateModal.vue | 62 ++- .../hermes/profiles/ProfileImportModal.vue | 34 +- .../hermes/profiles/ProfileRenameModal.vue | 52 +- .../hermes/settings/AccountSettings.vue | 93 ++-- .../hermes/settings/AgentSettings.vue | 27 +- .../hermes/settings/CompressionSettings.vue | 31 +- .../hermes/settings/DisplaySettings.vue | 35 +- .../settings/GatewayAutoStartSettings.vue | 27 +- .../hermes/settings/GithubPreviewSettings.vue | 87 ++-- .../hermes/settings/MemorySettings.vue | 17 +- .../hermes/settings/ModelSettings.vue | 33 +- .../hermes/settings/PlatformCard.vue | 9 +- .../hermes/settings/PlatformSettings.vue | 119 +++-- .../hermes/settings/PrivacySettings.vue | 3 +- .../hermes/settings/ProxySettings.vue | 13 +- .../hermes/settings/SessionSettings.vue | 41 +- .../settings/UserManagementSettings.vue | 156 +++--- .../hermes/settings/VoiceSettings.vue | 29 +- .../hermes/settings/voice/VoiceApiCard.vue | 68 ++- .../settings/voice/VoiceApiConfigurator.vue | 308 +++++------ .../settings/voice/VoiceApiFormModal.vue | 112 ++-- .../hermes/skills/PendingWriteApprovals.vue | 36 +- .../hermes/skills/SkillExternalDirsModal.vue | 28 +- .../hermes/skills/SkillImportModal.vue | 51 +- .../components/hermes/skills/SkillList.vue | 11 +- .../hermes/workflow/WorkflowAgentNode.vue | 67 ++- .../hermes/workflow/WorkflowModelSelector.vue | 15 +- .../src/components/hermes/workflow/types.ts | 4 +- .../src/components/layout/LanguageSwitch.vue | 15 +- .../src/components/layout/ModelSelector.vue | 27 +- .../src/components/layout/PageSidebarNav.vue | 114 ++--- .../src/components/layout/ProfileSelector.vue | 81 ++- .../layout/SettingsCircuitBadge.vue | 183 +++---- packages/client/src/styles/global.scss | 8 +- packages/client/src/styles/theme.ts | 176 ------- .../client/src/views/hermes/ChannelsView.vue | 5 +- .../client/src/views/hermes/DevicesView.vue | 212 ++++---- .../client/src/views/hermes/HistoryView.vue | 157 +++--- packages/client/src/views/hermes/JobsView.vue | 55 +- .../client/src/views/hermes/JourneyView.vue | 57 +-- .../client/src/views/hermes/KanbanView.vue | 64 ++- packages/client/src/views/hermes/LogsView.vue | 30 +- .../src/views/hermes/McpManagerView.vue | 113 +++-- .../client/src/views/hermes/MemoryView.vue | 29 +- .../client/src/views/hermes/ModelsView.vue | 43 +- .../src/views/hermes/PerformanceView.vue | 11 +- .../client/src/views/hermes/PetdexView.vue | 40 +- .../client/src/views/hermes/PluginsView.vue | 61 ++- .../client/src/views/hermes/ProfilesView.vue | 13 +- .../client/src/views/hermes/SettingsView.vue | 64 ++- .../src/views/hermes/SkillsUsageView.vue | 13 +- .../client/src/views/hermes/SkillsView.vue | 53 +- .../client/src/views/hermes/TerminalView.vue | 94 ++-- .../client/src/views/hermes/UsageView.vue | 13 +- .../client/src/views/hermes/WorkflowView.vue | 477 +++++++++--------- .../client/default-credential-prompt.test.ts | 47 +- tests/client/display-settings.test.ts | 123 +++-- tests/client/file-context-menu.test.ts | 97 ++-- .../gateway-auto-start-settings.test.ts | 84 +-- tests/client/job-form-modal.test.ts | 164 ++++-- tests/client/kanban-create-form.test.ts | 208 +++++--- tests/client/kanban-task-card.test.ts | 23 +- tests/client/kanban-task-drawer.test.ts | 133 ++--- tests/client/kanban-view.test.ts | 165 +++--- tests/client/no-naive-ui.test.ts | 77 +++ tests/client/pending-write-approvals.test.ts | 49 +- tests/client/plugins-view.test.ts | 44 +- tests/client/profile-card-config-edit.test.ts | 31 +- .../provider-card-built-in-action.test.ts | 108 ++-- tests/client/session-settings.test.ts | 129 ++--- tests/client/skills-usage-view.test.ts | 72 ++- tests/client/stt-settings-ui.test.ts | 291 +++++------ tests/client/style-system.test.ts | 36 +- tests/client/usage-view-period.test.ts | 26 +- tests/client/workspace-long-names.test.ts | 47 +- tests/e2e/fixtures.ts | 33 ++ tests/e2e/provider-models.spec.ts | 7 +- vite.config.ts | 10 +- 115 files changed, 4067 insertions(+), 3891 deletions(-) create mode 100644 docs/arco-migration-rules.md create mode 100644 docs/chat-chain-changes/2026-10-18-phase007-arco-full-migration.md delete mode 100644 packages/client/src/styles/theme.ts create mode 100644 tests/client/no-naive-ui.test.ts diff --git a/docs/arco-migration-rules.md b/docs/arco-migration-rules.md new file mode 100644 index 0000000..9336b49 --- /dev/null +++ b/docs/arco-migration-rules.md @@ -0,0 +1,101 @@ +# Phase 007 迁移规约(每个 agent 必读) + +目标:`grep -rn "naive-ui\|vueuc\|@css-render" packages/client/src` 零命中, +且项目里只有 Arco 一个 UI 库。`tests/client/no-naive-ui.test.ts` 会卡住回归。 + +## 铁律 + +1. **只换渲染层。** 不改业务逻辑、不改 store、不改 API 调用、不改事件语义。 + 看到逻辑 bug 记下来但不要动手。 +2. **不引入新依赖。** Arco 已在 `main.ts` 全量 `app.use(ArcoVue)`, + 模板里直接写 `` 等 kebab-case 标签即可,**不要** import Arco 组件。 +3. **Arco 不会对拼错的 prop 报错** —— 未声明的 prop 会作为普通 attribute 落到根元素上。 + 所以「弹层能打开但内容是空的」几乎总是 prop 写错了。改完每个弹层都要确认 + prop 在 `node_modules/@arco-design/web-vue/es/<组件>/` 的 props 定义里存在。 +4. **`grep naive-ui` 匹配不到 CSS 里的类名。** 必须另外用这个命令自查: + ```bash + grep -n "\.n-[a-z]\|:deep(\.n-" <你改过的每个文件> + ``` + 这些是死样式,在 Arco 下静默失效。**这是最容易漏、且测试测不出来的一类改动。** + +## 组件映射(Arco 全局已注册,直接用 kebab-case 标签) + +| Naive | Arco | 必须改的差异 | +|---|---|---| +| `NButton` | `a-button` | `block`→`long`;`size="tiny"`→`size="mini"`;`quaternary`/`secondary`→`type="text"` / `type="secondary"`;`type="warning"`→`type="primary" status="warning"`;`circle`/`round` 形状用 `shape` prop | +| `NInput` | `a-input` | `type="textarea"` → 独立 `a-textarea`;`a-input` 不透传 attrs,`autofocus` 无效,要用 `ref` + `focus()` | +| `NInputPassword` / `show-password-on` | `a-input-password` | **不要传 `default-visibility`**。它的默认值 `true` 表示"处于可见状态",而 `input-password.js:77` 是 `type: mergedVisible ? "password" : "text"` —— 所以默认就是**遮蔽**显示。传 `:default-visibility="false"` 反而会让 API Key **明文显示**。不传即可 | +| `NModal` | `a-modal` | `v-model:show`→`v-model:visible`;`preset="dialog"`/`preset="card"` 无对应 → `:footer="false"`;**`#action` 插槽要改名 `#footer`**(Arco 只有 `#footer`);`positive-text`→`ok-text`;`@positive-click`→`@ok` | +| `NDrawer`/`NDrawerContent` | `a-drawer` | Arco 没有独立 content 组件,`NDrawerContent` 的 `:title` 直接给 `a-drawer`;`v-model:show`→`v-model:visible`;`placement` 保留 | +| `NForm` | `a-form` | **本仓库 `:rules` 全仓零使用,校验都是手写 `if`,直接替换即可,不需要写规则适配** | +| `NFormItem` | `a-form-item` | `label-placement="top"`→`layout="vertical"`;`NFormItem` 没有 `feedback` prop,错误文案自己渲染一个元素 | +| `NSelect` | `a-select` | `options` 的 `{label,value,disabled}` 语义一致;`consistent-menu-width` 等无对应,删掉 | +| `NSpin` | `a-spin` | `show`→`loading` | +| `NTooltip` | `a-tooltip` | `placement`→`position`;**`#trigger` 模板插槽必须拆掉**,触发元素放默认插槽 | +| `NPopconfirm` | `a-popconfirm` | `positive-text`→`ok-text`;`@positive-click`→`@ok`;**`#trigger` 必须改默认插槽** | +| `NDropdown` | `a-dropdown` | **Arco 没有 `droplist` prop**,菜单必须写进 `#content` 插槽用 `a-doption`;`@select` 的 key 结构一致 | +| `NTag` | `a-tag` | `type="info"`→`color="arcoblue"` 等内建色;`bordered` 存在;**naive 的 `round` 没有对应 prop**(Arco `a-tag` 只有 `color/size/bordered/visible/loading/closable/checkable/checked/nowrap`),传 `shape="round"` 会静默失效,要圆角就自己写 `border-radius: 999px` | +| `NSwitch` | `a-switch` | `value`/`v-model:value` 一致 | +| `NSpace` | `a-space` | `size` 语义一致 | +| `NInputNumber` | `a-input-number` | 一致 | +| `NAlert` | `a-alert` | `type` 一致 | +| `NEmpty` | `a-empty` | 一致 | +| `NTabs`/`NTabPane` | `a-tabs`/`a-tab-pane` | `NTabPane` 的 `name`+`:tab` → `a-tab-pane` 的 `key`+`title`;`animated` 无对应,删;`v-model:value`→`v-model:active-key`,`@update:value`→`@change` | +| `NCollapse`/`NCollapseItem` | `a-collapse`/`a-collapse-item` | `arrow` 语义不同 | +| `NRadioGroup`/`NRadio`/`NRadioButton` | `a-radio-group`/`a-radio` | `NRadioButton` → ``。**注意是 `"button"` 不是 `"radio-button"`**(`radio.js:115` 判 `mergedType === "button"`,写错会静默失效) | +| `NCheckbox`/`NCheckboxGroup` | `a-checkbox`/`a-checkbox-group` | `options` 一致 | +| `NUpload` | `a-upload` | `file-list` 结构不同;`UploadFileInfo` 类型是 naive 专有 → 改成原生 `File[]` 或 Arco 的 `FileItem`;`directory-dnd` 无对应,删 | +| `NDataTable` | `a-table` | **列定义 API 差异最大**:`columns` 的 `title/key/render/width/minWidth/ellipsis` 要逐个对;naive 的 `render(row)` 返回 vnode,Arco 用 `#columns` 插槽或同样的 `render` | +| `NBreadcrumb`/`NBreadcrumbItem` | `a-breadcrumb`/`a-breadcrumb-item` | 一致 | +| `NDescriptions`/`NDescriptionsItem` | `a-descriptions`/`a-descriptions-item` | 一致 | +| `NPopover` | `a-popover` | `placement`→`position` | +| `NBadge` | `a-badge` | `value`→`count` | +| `NScrollbar` | `a-scrollbar` | Arco 的 `a-scrollbar` 需要显式 `height`,用 `max-height` 会高度塌陷 | +| `NIcon` | `` | Arco 图标是独立组件,`import { IconXxx } from '@arco-design/web-vue/es/icon'` | +| `NText` | 原生 `span` / `a-typography` | 多数场景直接换原生元素 + 原有 class | +| `NTree` | `a-tree` | **懒加载语义不同**:naive `:on-load="fn"` 直接改 `node.children`;Arco 是 `@load="(node, done) => ..."`,改写回调 | + +## 类型导入 + +```ts +import type { SelectOption } from 'naive-ui' // 删 +export interface X extends SelectOption {} // 改成本地 interface: + // { label: string; value: string; disabled?: boolean } +``` + +## CSS 类名对照(`:deep()` 里也要改) + +`.n-button`→`.arco-btn` / `.n-input`→`.arco-input` / `.n-input__*`→`.arco-input-inner` +/ `.n-switch`→`.arco-switch` / `.n-tag`→`.arco-tag` / `.n-spin-container`→`.arco-spin-container` +/ `.n-spin-content`→`.arco-spin` / `.n-collapse`→`.arco-collapse` +/ `.n-drawer-body-content-wrapper`→`.arco-drawer-body` / `.n-modal-body-wrapper`→`.arco-modal` +/ `.n-data-table*`→`.arco-table*` + +**结构差异(Arco 没有的 naive 嵌套层)**: +- `a-button` **没有** content 包裹层 → `gap`/`min-width` 直接写在 button 上 +- `a-modal` 没有 `body-wrapper` 分层 → 用 `.arco-modal` / `.arco-modal-body` +- `a-drawer` 没有 `content` / `body-content-wrapper` 分层 → 用 `.arco-drawer` / `.arco-drawer-body` +- Arco **Trigger 不能嵌套**:`a-tooltip`/`a-dropdown`/`a-popconfirm` 内部都用 + `mergeFirstChild` 接管首个元素的��件,互相嵌套时菜单**点不开**。 + 同一元素既要 tooltip 又要 dropdown 时,tooltip 改用原生 `:title`。 +- **tooltip/dropdown 的默认插槽必须只含一个元素**。尾随的纯文本会被当可见内容 + 渲染进布局流(变成 0 宽的 flex item),文案要放 `content` prop。 + +## 红线 + +- 消息气泡 / 群聊气泡内部**禁用任何 Arco 组件**(流式性能), + 由 `tests/e2e/chat-rendering-smoke.spec.ts` 卡住 +- `/chat-run` Socket.IO 事件契约:事件名、字段名、字段类型、发射时机都不许改(只允许加可选字段) +- `packages/server/src/services/hermes/agent-bridge/python/` 只读 +- 不要重排 `