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:
2026-10-17 22:17:32 +08:00
parent 4baccd5d88
commit effc29085c
20 changed files with 1356 additions and 819 deletions
@@ -5,7 +5,6 @@ import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import "@xterm/xterm/css/xterm.css";
import { getApiKey, getBaseUrlValue } from "@/api/client";
import { NButton, NPopconfirm, NTooltip, NSelect } from "naive-ui";
import { useMessage } from "@/composables/useMessage";
import { useI18n } from "vue-i18n";
import type { ITheme } from "@xterm/xterm";
@@ -417,7 +416,8 @@ function handleTerminalTouchEnd() {
// ─── Theme ───────────────────────────────────────────────────────
function applyTheme(themeName: string) {
function applyTheme(value: unknown) {
const themeName = String(value);
selectedTheme.value = themeName;
localStorage.setItem(STORAGE_KEY_THEME, themeName);
const themeObj = TERMINAL_THEMES[themeName]?.theme;
@@ -473,19 +473,17 @@ onUnmounted(() => {
>
<div class="sidebar-header">
<span class="sidebar-title">{{ t("terminal.sessions") }}</span>
<NTooltip trigger="hover">
<template #trigger>
<NButton quaternary size="tiny" @click="createSession" circle>
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</template>
</NButton>
</template>
<a-tooltip trigger="hover">
<a-button type="secondary" size="mini" @click="createSession" shape="circle">
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</template>
</a-button>
{{ t("terminal.newTab") }}
</NTooltip>
</a-tooltip>
</div>
<div class="session-list">
<div v-if="connectionError" class="session-error">
@@ -495,7 +493,7 @@ onUnmounted(() => {
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span>{{ connectionError }}</span>
<NButton size="tiny" @click="connect">{{ t("common.retry") }}</NButton>
<a-button size="mini" @click="connect">{{ t("common.retry") }}</a-button>
</div>
<div v-else-if="sessions.length === 0" class="session-empty">
<template v-if="isConnecting">
@@ -524,17 +522,18 @@ onUnmounted(() => {
}}</span>
</span>
</div>
<NPopconfirm v-if="sessions.length > 1" @positive-click="closeSession(s.id)">
<template #trigger>
<button class="session-item-delete" @click.stop>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</template>
{{ t("terminal.closeSession") }}
</NPopconfirm>
<a-popconfirm
v-if="sessions.length > 1"
:content="t('terminal.closeSession')"
@ok="closeSession(s.id)"
>
<button class="session-item-delete" @click.stop>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</a-popconfirm>
</button>
</div>
</div>
@@ -545,7 +544,7 @@ onUnmounted(() => {
activeSession.title
}}</span>
<div class="header-actions">
<NButton
<a-button
size="small"
@click="showSidebar = !showSidebar"
class="sidebar-toggle"
@@ -557,16 +556,15 @@ onUnmounted(() => {
</svg>
</template>
{{ t("terminal.sessions") }}
</NButton>
<NSelect
:value="selectedTheme"
</a-button>
<a-select
:model-value="selectedTheme"
:options="themeOptions"
size="small"
:consistent-menu-width="false"
class="theme-select"
@update:value="applyTheme"
@change="applyTheme"
/>
<NButton size="small" @click="createSession">
<a-button size="small" @click="createSession">
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19" />
@@ -574,7 +572,7 @@ onUnmounted(() => {
</svg>
</template>
{{ t("terminal.newTab") }}
</NButton>
</a-button>
</div>
</header>
<div class="terminal-container">