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:
2026-10-18 03:11:35 +08:00
parent effc29085c
commit c3eeea4187
10 changed files with 181 additions and 85 deletions
@@ -1048,11 +1048,10 @@ function isImage(type: string): boolean {
<div v-if="showContextUsage" class="context-usage-row">
<span class="context-info" :class="{ 'context-warning': usagePercent > 80 }">
{{ 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">
{{ formatTokens(contextLength) }}
</span>
<span>{{ t('chat.contextClickToEdit') }}</span>
</a-tooltip>
· {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
</span>
@@ -1083,13 +1082,16 @@ function isImage(type: string): boolean {
<div class="input-toolbar">
<!-- Bottom bar: attach + input settings + actions -->
<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">
<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>
</template>
</a-button>
{{ t('chat.attachFiles') }}
</a-tooltip>
<a-dropdown
@@ -1159,7 +1161,11 @@ function isImage(type: string): boolean {
</template>
</a-dropdown>
<a-tooltip trigger="hover" :disabled="isMobileViewport">
<a-tooltip
trigger="hover"
:disabled="isMobileViewport"
:content="props.modelLabel || t('models.selectModel')"
>
<a-button
type="secondary"
size="mini"
@@ -1193,7 +1199,6 @@ function isImage(type: string): boolean {
<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>
</a-button>
{{ props.modelLabel || t('models.selectModel') }}
</a-tooltip>
</div>
@@ -1386,8 +1391,8 @@ function isImage(type: string): boolean {
}
}
:deep(.n-switch),
:deep(.n-switch__rail) {
:deep(.arco-switch),
:deep(.arco-switch-track) {
margin-right: 0;
}
}
@@ -1405,12 +1410,6 @@ function isImage(type: string): boolean {
background: transparent !important;
opacity: 1;
:deep(.n-button__state-border),
:deep(.n-button__border),
:deep(.n-button__ripple) {
display: none;
}
.tool-trace-icon {
display: block;
flex: 0 0 16px;
@@ -1433,16 +1432,8 @@ function isImage(type: string): boolean {
color: $text-secondary;
border-radius: 999px;
padding: 0 7px 0 6px;
:deep(.n-button__content) {
gap: 4px;
}
:deep(.n-button__state-border),
:deep(.n-button__border),
:deep(.n-button__ripple) {
display: none;
}
gap: 4px;
border-color: transparent;
}
.input-model-button {
@@ -1450,17 +1441,8 @@ function isImage(type: string): boolean {
border-radius: 999px;
max-width: 190px;
padding: 0 4px 0 6px;
:deep(.n-button__content) {
gap: 4px;
min-width: 0;
}
:deep(.n-button__state-border),
:deep(.n-button__border),
:deep(.n-button__ripple) {
display: none;
}
gap: 4px;
border-color: transparent;
}
.input-model-label {
@@ -1476,15 +1458,12 @@ function isImage(type: string): boolean {
color: $text-secondary;
border-radius: 999px;
padding: 0 4px 0 6px;
gap: 4px;
border-color: transparent;
&.active {
color: #4caf50;
}
:deep(.n-button__content) {
gap: 4px;
min-width: 0;
}
}
.reasoning-effort-label {
@@ -1625,14 +1604,7 @@ function isImage(type: string): boolean {
.input-settings-button {
min-width: 36px;
padding: 0 4px 0 6px;
:deep(.n-button__content) {
gap: 2px;
}
:deep(.n-button__icon) {
margin: 0;
}
gap: 2px;
}
.input-settings-label {
@@ -1820,6 +1792,7 @@ function isImage(type: string): boolean {
gap: 12px;
margin-top: auto;
min-height: 32px;
flex-wrap: nowrap;
}
.toolbar-icon-button {
@@ -1833,22 +1806,12 @@ function isImage(type: string): boolean {
padding: 0 !important;
border: 0 !important;
box-shadow: none !important;
gap: 0;
:deep(.n-button__icon) {
:deep(.arco-btn-icon) {
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 {
color: var(--text-on-overlay);
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 TOOL_PANEL_MIN_WIDTH = 360;
const TOOL_PANEL_DEFAULT_WIDTH = 560;
// 工具栏要单行放下:附件 + 推理强度 + 设置 + 模型下拉(最长 190px)+ 语音 + 发送,
// 再加输入框左右内边距。窄于此值 Arco 按钮会被挤成竖排。
const CHAT_MAIN_MIN_WIDTH = 620;
const TOOL_PANEL_STORAGE_KEY = "hermes.chat.toolPanelWidth";
const toolPanelWidth = ref(loadToolPanelWidth());
const toolResizeStart = ref<{ x: number; width: number } | null>(null);
@@ -102,7 +105,9 @@ function toolPanelMaxWidth() {
if (typeof window === "undefined") return 1180;
if (isMobile.value) return 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) {
@@ -2068,7 +2073,7 @@ async function handleSessionModelCustomSubmit() {
gap: 4px;
height: 22px;
.n-button {
.arco-btn {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -2150,24 +2155,20 @@ async function handleSessionModelCustomSubmit() {
gap: 14px;
}
:deep(.new-chat-drawer .n-drawer-content) {
:deep(.new-chat-drawer .arco-drawer) {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.new-chat-drawer .n-drawer-header),
:deep(.new-chat-drawer .n-drawer-footer) {
:deep(.new-chat-drawer .arco-drawer-header),
:deep(.new-chat-drawer .arco-drawer-footer) {
flex-shrink: 0;
}
:deep(.new-chat-drawer .n-drawer-body) {
:deep(.new-chat-drawer .arco-drawer-body) {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
:deep(.new-chat-drawer .n-drawer-body-content-wrapper) {
height: 100%;
overflow-y: auto;
}
@@ -2181,7 +2182,7 @@ async function handleSessionModelCustomSubmit() {
}
@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-bottom: 12px;
}
@@ -2112,7 +2112,7 @@ onBeforeUnmount(() => {
}
}
:deep(.n-drawer-body-content-wrapper) {
:deep(.arco-drawer-body) {
height: 100%;
}
@@ -1017,12 +1017,12 @@ defineExpose({
padding: 10px 4px 0;
border-top: 1px solid $border-color;
:deep(.n-input) {
:deep(.arco-input) {
flex: 1 1 auto;
min-width: 0;
}
:deep(.n-button) {
:deep(.arco-btn) {
flex: 0 0 auto;
}
}
@@ -1200,7 +1200,7 @@ defineExpose({
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
:deep(.n-button) {
:deep(.arco-btn) {
width: 100%;
}
}
@@ -1208,7 +1208,7 @@ defineExpose({
.clarify-float-input-row {
flex-direction: column;
:deep(.n-button) {
:deep(.arco-btn) {
width: 100%;
}
}
@@ -445,7 +445,7 @@ onUnmounted(() => {
}
@media (max-width: $breakpoint-mobile) {
:deep(.n-modal-body-wrapper) {
:deep(.arco-modal) {
width: calc(100vw - 24px);
}
@@ -473,7 +473,7 @@ onUnmounted(() => {
>
<div class="sidebar-header">
<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">
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -482,7 +482,6 @@ onUnmounted(() => {
</svg>
</template>
</a-button>
{{ t("terminal.newTab") }}
</a-tooltip>
</div>
<div class="session-list">
@@ -36,12 +36,14 @@ export function useKeyboard() {
closeSessionSearch()
return
}
// Close any open modals — naive-ui handles this internally
const modal = document.querySelector('.n-modal-mask')
if (modal) {
const closeBtn = modal.querySelector('.n-base-close') as HTMLElement
closeBtn?.click()
}
// 聊天页的弹窗已全部迁到 Arco(且都带 unmount-on-close),Esc 兜底要查
// Arco 的类名,否则模型选择、会话搜索这些弹窗在键盘下关不掉。
const openModal = Array.from(
document.querySelectorAll<HTMLElement>('.arco-modal'),
).find((el) => el.offsetParent !== null)
openModal
?.querySelector<HTMLElement>('.arco-modal-close-btn')
?.click()
}
}