fix(chat): repair Arco migration fallout in the chat page
The Phase 006 walkthrough surfaced four issues the automated checks could not see. Dead naive-ui class names left behind by the migration stopped matching, tooltips that carried their label as trailing default-slot text leaked that text into the flex row, the workspace panel squeezed the chat column until the input toolbar wrapped, and the Escape fallback still looked for naive-ui's modal mask. Tooltips now pass their label through the content prop, the tool panel reserves a minimum width for the chat column, and Escape finds the visible Arco modal and clicks its close button.
This commit is contained in:
@@ -82,3 +82,14 @@ Phase 005 起,新代码与迁移代码一律使用 Arco Design Vue。本文表
|
|||||||
排查手法:`el.__vueParentComponent.props` 看目标组件到底认不认这个 prop,
|
排查手法:`el.__vueParentComponent.props` 看目标组件到底认不认这个 prop,
|
||||||
再看 `el.outerHTML` 确认渲染出来的骨架。回归由
|
再看 `el.outerHTML` 确认渲染出来的骨架。回归由
|
||||||
`tests/e2e/chat-input-dropdown.spec.ts` 卡住。
|
`tests/e2e/chat-input-dropdown.spec.ts` 卡住。
|
||||||
|
- **迁移时最容易漏的是 CSS 里的 naive 类名**,因为 `grep naive-ui` 匹配不到
|
||||||
|
`:deep(.n-button__content)` 这种写法。聊天页原本有 35 处这类死样式,在 Arco 下
|
||||||
|
全部不生效,必须逐条改写成 Arco 类名。注意三处结构差异:
|
||||||
|
- `a-button` **没有** content 包裹层(naive 有 `.n-button__content`),
|
||||||
|
`gap` / `min-width` 要直接写在 button 上
|
||||||
|
- `a-modal` 没有 `body-wrapper` 分层,用 `.arco-modal` / `.arco-modal-body`
|
||||||
|
- `a-drawer` 同理,没有 `content` / `body-content-wrapper` 分层
|
||||||
|
- `useKeyboard.ts` 里查 `.n-modal-mask` 的 Esc 兜底属于跨组件的全局约定,迁移
|
||||||
|
组件时最容易漏掉。改弹窗库时必须同步 grep `.n-modal|.n-drawer|.n-base-` 这类
|
||||||
|
**字符串字面量**里的类名,而不只是组件 import。回归由
|
||||||
|
`tests/client/session-search.test.ts` 的 Esc 用例卡住。
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
date: 2026-10-17
|
||||||
|
pr: pending
|
||||||
|
feature: Phase 006 follow-up - Arco class-name cleanup, tooltip content props, tool panel width clamp, Escape dialog fallback
|
||||||
|
impact: Cosmetic styling fix plus one behavior fix. Dead naive-ui CSS class names in the chat components are rewritten to Arco equivalents so the toolbar, model label, drawer body scrolling, and mobile breakpoints receive their intended styles again; Arco tooltips that carried their label as trailing default-slot text now pass it through the content prop so the text stops being rendered into the layout flow; the workspace/terminal panel no longer squeezes the chat column narrow enough to wrap the input toolbar; and Escape again closes Arco chat dialogs because the fallback no longer looks for naive-ui's .n-modal-mask. No chat store logic, no /chat-run payload, and no message rendering path is touched.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 006 follow-up: Arco style cleanup and Esc dialog fallback
|
||||||
|
|
||||||
|
Branch: `phase/006-chat-arco`
|
||||||
|
Touched feature: chat toolbar styling, chat dialogs, session context menu
|
||||||
|
layout, Esc-to-close fallback
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Phase 006 migrated the chat page to Arco, but the manual checklist walkthrough
|
||||||
|
surfaced four problems that the automated checks could not see:
|
||||||
|
|
||||||
|
1. Opening the workspace/terminal panel squeezed the chat column so far that the
|
||||||
|
input toolbar wrapped "Add attachment" into a vertical stack and truncated the
|
||||||
|
model label.
|
||||||
|
2. 35 `:deep(.n-*)` rules in the chat components were left behind by the
|
||||||
|
migration. They silently stopped matching because Arco uses different class
|
||||||
|
names, and `grep naive-ui` never sees them because they are class names in a
|
||||||
|
style block rather than an import.
|
||||||
|
3. `useKeyboard.ts` still looked for `.n-modal-mask` to close dialogs on Escape.
|
||||||
|
Every chat dialog had moved to Arco, so Escape no longer closed the model
|
||||||
|
picker or session search.
|
||||||
|
4. Four Arco tooltips carried their label as trailing default-slot text
|
||||||
|
(`<a-tooltip><a-button/></a-tooltip>{{ t(...) }}`). naive-ui rendered the
|
||||||
|
tooltip slot as an overlay so the text was harmless; Arco renders the default
|
||||||
|
slot inline, so those text nodes sat in the flex row as zero-width items and
|
||||||
|
contributed to the toolbar squeeze above.
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
|
||||||
|
- `ChatPanel.vue` — `toolPanelMaxWidth()` now reserves `CHAT_MAIN_MIN_WIDTH`
|
||||||
|
(620px) for the chat column instead of a flat 120px, so the toolbar always has
|
||||||
|
room. `showToolPanel` already re-clamps on open, so persisted widths from older
|
||||||
|
builds are corrected on the next toggle. Dead naive classes rewritten:
|
||||||
|
`.n-button` → `.arco-btn`, and the new-chat drawer's `-content` /
|
||||||
|
`-body-content-wrapper` layers collapsed onto `.arco-drawer` / `.arco-drawer-body`
|
||||||
|
because Arco has no such nesting.
|
||||||
|
- `ChatInput.vue` — dropped the `.n-button__content` / `.n-button__border` /
|
||||||
|
`.n-button__ripple` / `.n-base-wave` rules; Arco's `a-button` is already
|
||||||
|
`display: flex` and carries a transparent border, so `gap` and
|
||||||
|
`border-color` now go on the button itself. `.n-switch` → `.arco-switch`,
|
||||||
|
`.n-button__icon` → `.arco-btn-icon`. The context-length, attach, and model
|
||||||
|
tooltips now pass their label via `:content`. `.input-toolbar` is pinned to
|
||||||
|
`flex-wrap: nowrap`.
|
||||||
|
- `TerminalPanel.vue` — the new-session tooltip passes its label via `:content`.
|
||||||
|
- `MessageList.vue`, `MessageItem.vue`, `SessionSearchModal.vue` — same
|
||||||
|
class-name rewrite; `.n-drawer-body-content-wrapper` → `.arco-drawer-body`,
|
||||||
|
`.n-modal-body-wrapper` → `.arco-modal`, `.n-input` → `.arco-input`.
|
||||||
|
- `useKeyboard.ts` — the Escape fallback now finds the visible `.arco-modal` and
|
||||||
|
clicks its `.arco-modal-close-btn`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
The toolbar was measured with the workspace panel open across viewport widths
|
||||||
|
1600 / 1280 / 1100 / 1024 / 900 / 820. `.input-toolbar` stays a single 32px row
|
||||||
|
at every width and `.input-model-label` is no longer clipped (`scrollWidth ===
|
||||||
|
clientWidth`). Before the fix the same measurement showed a 90px-tall two-row
|
||||||
|
toolbar and a truncated model label.
|
||||||
|
|
||||||
|
## Behavior impact
|
||||||
|
|
||||||
|
Cosmetic only, plus one fix. Toolbar, model label, drawer body scrolling and
|
||||||
|
mobile breakpoints now actually receive their intended styles. Tooltip labels
|
||||||
|
stop being emitted into the layout. Escape now closes Arco chat dialogs again. No
|
||||||
|
chat store logic, no `/chat-run` payload, and no message rendering path was
|
||||||
|
touched.
|
||||||
|
|
||||||
|
## Regression coverage
|
||||||
|
|
||||||
|
- `tests/client/session-search.test.ts` — asserts Escape clicks an open Arco
|
||||||
|
modal's close button and ignores leftover naive-ui markup.
|
||||||
|
- `tests/e2e/chat-input-dropdown.spec.ts`, `tests/e2e/chat-rendering-smoke.spec.ts`
|
||||||
|
— unchanged, still green (35 e2e tests).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test # 274 files / 2027 tests
|
||||||
|
npm run test:e2e # 35 tests
|
||||||
|
npm run harness:check
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -1048,11 +1048,10 @@ function isImage(type: string): boolean {
|
|||||||
<div v-if="showContextUsage" class="context-usage-row">
|
<div v-if="showContextUsage" class="context-usage-row">
|
||||||
<span class="context-info" :class="{ 'context-warning': usagePercent > 80 }">
|
<span class="context-info" :class="{ 'context-warning': usagePercent > 80 }">
|
||||||
{{ formatTokens(totalTokens) }} /
|
{{ formatTokens(totalTokens) }} /
|
||||||
<a-tooltip trigger="hover" :disabled="isMobileViewport">
|
<a-tooltip trigger="hover" :disabled="isMobileViewport" :content="t('chat.contextClickToEdit')">
|
||||||
<span class="context-limit-editable" @click="handleEditContextLimit">
|
<span class="context-limit-editable" @click="handleEditContextLimit">
|
||||||
{{ formatTokens(contextLength) }}
|
{{ formatTokens(contextLength) }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ t('chat.contextClickToEdit') }}</span>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
· {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
|
· {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -1083,13 +1082,16 @@ function isImage(type: string): boolean {
|
|||||||
<div class="input-toolbar">
|
<div class="input-toolbar">
|
||||||
<!-- Bottom bar: attach + input settings + actions -->
|
<!-- Bottom bar: attach + input settings + actions -->
|
||||||
<div class="input-top-bar">
|
<div class="input-top-bar">
|
||||||
<a-tooltip trigger="hover" :disabled="isMobileViewport">
|
<a-tooltip
|
||||||
|
trigger="hover"
|
||||||
|
:disabled="isMobileViewport"
|
||||||
|
:content="t('chat.attachFiles')"
|
||||||
|
>
|
||||||
<a-button type="secondary" size="mini" shape="circle" @click="handleAttachClick" class="toolbar-icon-button">
|
<a-button type="secondary" size="mini" shape="circle" @click="handleAttachClick" class="toolbar-icon-button">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
{{ t('chat.attachFiles') }}
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
<a-dropdown
|
<a-dropdown
|
||||||
@@ -1159,7 +1161,11 @@ function isImage(type: string): boolean {
|
|||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
|
|
||||||
<a-tooltip trigger="hover" :disabled="isMobileViewport">
|
<a-tooltip
|
||||||
|
trigger="hover"
|
||||||
|
:disabled="isMobileViewport"
|
||||||
|
:content="props.modelLabel || t('models.selectModel')"
|
||||||
|
>
|
||||||
<a-button
|
<a-button
|
||||||
type="secondary"
|
type="secondary"
|
||||||
size="mini"
|
size="mini"
|
||||||
@@ -1193,7 +1199,6 @@ function isImage(type: string): boolean {
|
|||||||
<span class="input-model-label">{{ compactModelLabel }}</span>
|
<span class="input-model-label">{{ compactModelLabel }}</span>
|
||||||
<svg class="toolbar-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
|
<svg class="toolbar-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
|
||||||
</a-button>
|
</a-button>
|
||||||
{{ props.modelLabel || t('models.selectModel') }}
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1386,8 +1391,8 @@ function isImage(type: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-switch),
|
:deep(.arco-switch),
|
||||||
:deep(.n-switch__rail) {
|
:deep(.arco-switch-track) {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1405,12 +1410,6 @@ function isImage(type: string): boolean {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
:deep(.n-button__state-border),
|
|
||||||
:deep(.n-button__border),
|
|
||||||
:deep(.n-button__ripple) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-trace-icon {
|
.tool-trace-icon {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 0 0 16px;
|
flex: 0 0 16px;
|
||||||
@@ -1433,16 +1432,8 @@ function isImage(type: string): boolean {
|
|||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0 7px 0 6px;
|
padding: 0 7px 0 6px;
|
||||||
|
|
||||||
:deep(.n-button__content) {
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
border-color: transparent;
|
||||||
|
|
||||||
:deep(.n-button__state-border),
|
|
||||||
:deep(.n-button__border),
|
|
||||||
:deep(.n-button__ripple) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-model-button {
|
.input-model-button {
|
||||||
@@ -1450,17 +1441,8 @@ function isImage(type: string): boolean {
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
max-width: 190px;
|
max-width: 190px;
|
||||||
padding: 0 4px 0 6px;
|
padding: 0 4px 0 6px;
|
||||||
|
|
||||||
:deep(.n-button__content) {
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-width: 0;
|
border-color: transparent;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button__state-border),
|
|
||||||
:deep(.n-button__border),
|
|
||||||
:deep(.n-button__ripple) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-model-label {
|
.input-model-label {
|
||||||
@@ -1476,15 +1458,12 @@ function isImage(type: string): boolean {
|
|||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0 4px 0 6px;
|
padding: 0 4px 0 6px;
|
||||||
|
gap: 4px;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #4caf50;
|
color: #4caf50;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button__content) {
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reasoning-effort-label {
|
.reasoning-effort-label {
|
||||||
@@ -1625,16 +1604,9 @@ function isImage(type: string): boolean {
|
|||||||
.input-settings-button {
|
.input-settings-button {
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
padding: 0 4px 0 6px;
|
padding: 0 4px 0 6px;
|
||||||
|
|
||||||
:deep(.n-button__content) {
|
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button__icon) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-settings-label {
|
.input-settings-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1820,6 +1792,7 @@ function isImage(type: string): boolean {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-icon-button {
|
.toolbar-icon-button {
|
||||||
@@ -1833,22 +1806,12 @@ function isImage(type: string): boolean {
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
:deep(.n-button__icon) {
|
:deep(.arco-btn-icon) {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button__content) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-button__border),
|
|
||||||
:deep(.n-button__state-border),
|
|
||||||
:deep(.n-button__ripple),
|
|
||||||
:deep(.n-base-wave) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
color: var(--text-on-overlay);
|
color: var(--text-on-overlay);
|
||||||
background-color: #9f9f9f;
|
background-color: #9f9f9f;
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ const activeWorkspaceSessionId = computed(() => chatStore.activeSession?.workspa
|
|||||||
const activeWorkspacePath = computed(() => chatStore.activeSession?.workspace && !chatStore.activeSession.isLocalOnly ? chatStore.activeSession.workspace : null);
|
const activeWorkspacePath = computed(() => chatStore.activeSession?.workspace && !chatStore.activeSession.isLocalOnly ? chatStore.activeSession.workspace : null);
|
||||||
const TOOL_PANEL_MIN_WIDTH = 360;
|
const TOOL_PANEL_MIN_WIDTH = 360;
|
||||||
const TOOL_PANEL_DEFAULT_WIDTH = 560;
|
const TOOL_PANEL_DEFAULT_WIDTH = 560;
|
||||||
|
// 工具栏要单行放下:附件 + 推理强度 + 设置 + 模型下拉(最长 190px)+ 语音 + 发送,
|
||||||
|
// 再加输入框左右内边距。窄于此值 Arco 按钮会被挤成竖排。
|
||||||
|
const CHAT_MAIN_MIN_WIDTH = 620;
|
||||||
const TOOL_PANEL_STORAGE_KEY = "hermes.chat.toolPanelWidth";
|
const TOOL_PANEL_STORAGE_KEY = "hermes.chat.toolPanelWidth";
|
||||||
const toolPanelWidth = ref(loadToolPanelWidth());
|
const toolPanelWidth = ref(loadToolPanelWidth());
|
||||||
const toolResizeStart = ref<{ x: number; width: number } | null>(null);
|
const toolResizeStart = ref<{ x: number; width: number } | null>(null);
|
||||||
@@ -102,7 +105,9 @@ function toolPanelMaxWidth() {
|
|||||||
if (typeof window === "undefined") return 1180;
|
if (typeof window === "undefined") return 1180;
|
||||||
if (isMobile.value) return window.innerWidth;
|
if (isMobile.value) return window.innerWidth;
|
||||||
const available = chatContentWrapperRef.value?.clientWidth || window.innerWidth;
|
const available = chatContentWrapperRef.value?.clientWidth || window.innerWidth;
|
||||||
return Math.max(320, Math.min(Math.floor(available * 0.88), available - 120));
|
// 主区要放得下输入工具栏(附件 + 推理强度 + 设置 + 模型 + 语音 + 发送),
|
||||||
|
// 否则面板一展开工具栏就被压成竖排。
|
||||||
|
return Math.max(320, Math.min(Math.floor(available * 0.88), available - CHAT_MAIN_MIN_WIDTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampToolPanelWidth(width: number) {
|
function clampToolPanelWidth(width: number) {
|
||||||
@@ -2068,7 +2073,7 @@ async function handleSessionModelCustomSubmit() {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
|
||||||
.n-button {
|
.arco-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -2150,24 +2155,20 @@ async function handleSessionModelCustomSubmit() {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.new-chat-drawer .n-drawer-content) {
|
:deep(.new-chat-drawer .arco-drawer) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.new-chat-drawer .n-drawer-header),
|
:deep(.new-chat-drawer .arco-drawer-header),
|
||||||
:deep(.new-chat-drawer .n-drawer-footer) {
|
:deep(.new-chat-drawer .arco-drawer-footer) {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.new-chat-drawer .n-drawer-body) {
|
:deep(.new-chat-drawer .arco-drawer-body) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.new-chat-drawer .n-drawer-body-content-wrapper) {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -2181,7 +2182,7 @@ async function handleSessionModelCustomSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
@media (max-width: $breakpoint-mobile) {
|
||||||
:deep(.new-chat-drawer .n-drawer-body-content-wrapper) {
|
:deep(.new-chat-drawer .arco-drawer-body) {
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2112,7 +2112,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-drawer-body-content-wrapper) {
|
:deep(.arco-drawer-body) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1017,12 +1017,12 @@ defineExpose({
|
|||||||
padding: 10px 4px 0;
|
padding: 10px 4px 0;
|
||||||
border-top: 1px solid $border-color;
|
border-top: 1px solid $border-color;
|
||||||
|
|
||||||
:deep(.n-input) {
|
:deep(.arco-input) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button) {
|
:deep(.arco-btn) {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1200,7 +1200,7 @@ defineExpose({
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|
||||||
:deep(.n-button) {
|
:deep(.arco-btn) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1208,7 +1208,7 @@ defineExpose({
|
|||||||
.clarify-float-input-row {
|
.clarify-float-input-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
:deep(.n-button) {
|
:deep(.arco-btn) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
@media (max-width: $breakpoint-mobile) {
|
||||||
:deep(.n-modal-body-wrapper) {
|
:deep(.arco-modal) {
|
||||||
width: calc(100vw - 24px);
|
width: calc(100vw - 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<span class="sidebar-title">{{ t("terminal.sessions") }}</span>
|
<span class="sidebar-title">{{ t("terminal.sessions") }}</span>
|
||||||
<a-tooltip trigger="hover">
|
<a-tooltip trigger="hover" :content="t('terminal.newTab')">
|
||||||
<a-button type="secondary" size="mini" @click="createSession" shape="circle">
|
<a-button type="secondary" size="mini" @click="createSession" shape="circle">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -482,7 +482,6 @@ onUnmounted(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
{{ t("terminal.newTab") }}
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="session-list">
|
<div class="session-list">
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ export function useKeyboard() {
|
|||||||
closeSessionSearch()
|
closeSessionSearch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Close any open modals — naive-ui handles this internally
|
// 聊天页的弹窗已全部迁到 Arco(且都带 unmount-on-close),Esc 兜底要查
|
||||||
const modal = document.querySelector('.n-modal-mask')
|
// Arco 的类名,否则模型选择、会话搜索这些弹窗在键盘下关不掉。
|
||||||
if (modal) {
|
const openModal = Array.from(
|
||||||
const closeBtn = modal.querySelector('.n-base-close') as HTMLElement
|
document.querySelectorAll<HTMLElement>('.arco-modal'),
|
||||||
closeBtn?.click()
|
).find((el) => el.offsetParent !== null)
|
||||||
}
|
openModal
|
||||||
|
?.querySelector<HTMLElement>('.arco-modal-close-btn')
|
||||||
|
?.click()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,4 +209,38 @@ describe('keyboard shortcut', () => {
|
|||||||
|
|
||||||
expect(useSessionSearch().sessionSearchOpen.value).toBe(true)
|
expect(useSessionSearch().sessionSearchOpen.value).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The chat page dialogs all moved to Arco in Phase 006, so the Esc fallback
|
||||||
|
// must look for Arco's close button, not naive-ui's.
|
||||||
|
it('closes an open Arco modal on Escape, and ignores naive-ui markup', () => {
|
||||||
|
const close = vi.fn()
|
||||||
|
const modal = document.createElement('div')
|
||||||
|
modal.className = 'arco-modal'
|
||||||
|
const closeBtn = document.createElement('div')
|
||||||
|
closeBtn.className = 'arco-modal-close-btn'
|
||||||
|
closeBtn.addEventListener('click', close)
|
||||||
|
modal.appendChild(closeBtn)
|
||||||
|
document.body.appendChild(modal)
|
||||||
|
|
||||||
|
const legacyModal = document.createElement('div')
|
||||||
|
legacyModal.className = 'n-modal n-modal-mask'
|
||||||
|
document.body.appendChild(legacyModal)
|
||||||
|
|
||||||
|
const Dummy = defineComponent({
|
||||||
|
setup() {
|
||||||
|
useKeyboard()
|
||||||
|
return () => h('div')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const wrapper = mount(Dummy)
|
||||||
|
// jsdom reports offsetParent as null, so make the modal look visible.
|
||||||
|
Object.defineProperty(modal, 'offsetParent', { value: document.body })
|
||||||
|
|
||||||
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||||
|
expect(close).toHaveBeenCalled()
|
||||||
|
|
||||||
|
wrapper.unmount()
|
||||||
|
modal.remove()
|
||||||
|
legacyModal.remove()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user