feat(chat): 调整聊天首页智能体切换入口,优化多智能体场景下的展示逻辑 Fixes: Error:智能体显示有误

Fixes #641
This commit is contained in:
Wenjie Zhang 2026-04-18 13:32:27 +08:00
parent c47a019270
commit dda3009611
3 changed files with 140 additions and 4 deletions

View File

@ -33,6 +33,7 @@
### 修复 ### 修复
- 调整聊天首页的智能体切换入口:在无历史对话时,智能体数量 `<= 3``chat-main` 宽度不小于 `380px` 时继续使用横向 segmented当智能体数量 `>= 4` 或内容区宽度小于 `380px` 时自动收敛为“当前智能体 + 下拉按钮”形式,避免多智能体或窄屏场景下入口被截断
- 发布前一致性修复:统一 0.6.0 版本号backend/package/web、更新 dev/prod 镜像标签语义(`0.6.0.dev` / `0.6.0`),并为 `/api/system/health` 补充 `version` 字段,提升部署可观测性与发版追溯能力 - 发布前一致性修复:统一 0.6.0 版本号backend/package/web、更新 dev/prod 镜像标签语义(`0.6.0.dev` / `0.6.0`),并为 `/api/system/health` 补充 `version` 字段,提升部署可观测性与发版追溯能力
- 收敛“状态工作台”自动弹出规则:前端不再因为共享 `workspace` 或文件系统天然存在内容而默认展开,改为仅在 `/home/gem/user-data/uploads``/home/gem/user-data/outputs` 下检测到实际文件时自动弹出;手动打开、关闭、刷新和伸缩交互保持不变 - 收敛“状态工作台”自动弹出规则:前端不再因为共享 `workspace` 或文件系统天然存在内容而默认展开,改为仅在 `/home/gem/user-data/uploads``/home/gem/user-data/outputs` 下检测到实际文件时自动弹出;手动打开、关闭、刷新和伸缩交互保持不变
- 调整智能体 todo 展示语义:待办状态不再作为 `capabilities` 前端开关,而是直接根据运行态 `agent_state.todos` 渲染;同时将 todo 入口从 Agent Panel 移到输入框内的轻量浮层,并让右侧“状态工作台”收敛为文件系统视图,输入框按钮文案同步由“状态”调整为“文件” - 调整智能体 todo 展示语义:待办状态不再作为 `capabilities` 前端开关,而是直接根据运行态 `agent_state.todos` 渲染;同时将 todo 入口从 Agent Panel 移到输入框内的轻量浮层,并让右侧“状态工作台”收敛为文件系统视图,输入框按钮文案同步由“状态”调整为“文件”

View File

@ -52,6 +52,7 @@
- 重写界面设计规范:参考 `DESIGN.md` 写法补充视觉气质、颜色 token、组件状态、布局层级、响应式与 Agent Prompt Guide并基于该规范收敛首页视觉表现移除装饰性渐变、重阴影、hover 位移和入场动画。 - 重写界面设计规范:参考 `DESIGN.md` 写法补充视觉气质、颜色 token、组件状态、布局层级、响应式与 Agent Prompt Guide并基于该规范收敛首页视觉表现移除装饰性渐变、重阴影、hover 位移和入场动画。
- 修复对话摘要中间件的工具结果卸载链路:摘要触发时改为将大体积 `ToolMessage` 写入当前 agent 可见的 sandbox outputs 路径,修正 `summary_offload` 路径拼接错误、`messages` 触发条件下不会真正裁剪历史的问题,并避免将 system message 重复纳入摘要与最终消息列表;补充对应单元测试覆盖。 - 修复对话摘要中间件的工具结果卸载链路:摘要触发时改为将大体积 `ToolMessage` 写入当前 agent 可见的 sandbox outputs 路径,修正 `summary_offload` 路径拼接错误、`messages` 触发条件下不会真正裁剪历史的问题,并避免将 system message 重复纳入摘要与最终消息列表;补充对应单元测试覆盖。
- 调整智能体对话中的工具调用展示:连续工具调用默认折叠为“调用了 N 个工具”的轻量摘要,展开后改为弱化时间线样式,减少工具结果卡片对正文阅读节奏的干扰。 - 调整智能体对话中的工具调用展示:连续工具调用默认折叠为“调用了 N 个工具”的轻量摘要,展开后改为弱化时间线样式,减少工具结果卡片对正文阅读节奏的干扰。
- 调整聊天首页的智能体切换入口:在无历史对话时,智能体数量 `<= 3``chat-main` 宽度不小于 `380px` 时继续使用横向 segmented当智能体数量 `>= 4` 或内容区宽度小于 `380px` 时自动收敛为“当前智能体 + 下拉按钮”形式。
--- ---

View File

@ -153,6 +153,37 @@
/> />
</div> </div>
<div v-else-if="showStartAgentDropdown" class="agent-switcher-wrapper">
<a-dropdown :trigger="['click']" placement="bottomCenter">
<button type="button" class="agent-switcher-btn">
<component :is="currentAgentIcon" size="16" class="agent-switcher-icon" />
<span class="agent-switcher-text">{{ currentAgentName }}</span>
<ChevronDown size="16" class="agent-switcher-chevron" />
</button>
<template #overlay>
<a-menu class="agent-switcher-menu">
<a-menu-item
v-for="agent in startAgents"
:key="agent.id"
@click="handleStartAgentChange(agent.id)"
>
<div class="agent-switcher-menu-item">
<component
:is="getAgentIconComponent(agent.id)"
size="16"
class="agent-switcher-menu-icon"
/>
<span class="agent-switcher-menu-text">{{ agent.name || 'Unknown' }}</span>
<span v-if="agent.id === currentAgentId" class="agent-switcher-menu-badge">
当前
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<AgentArtifactsCard <AgentArtifactsCard
:artifacts="currentArtifacts" :artifacts="currentArtifacts"
:thread-id="currentChatId" :thread-id="currentChatId"
@ -247,7 +278,14 @@ import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue' import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue'
import RefsComponent from '@/components/RefsComponent.vue' import RefsComponent from '@/components/RefsComponent.vue'
import ToolCallsGroupComponent from '@/components/ToolCallsGroupComponent.vue' import ToolCallsGroupComponent from '@/components/ToolCallsGroupComponent.vue'
import { PanelLeftOpen, MessageCirclePlus, LoaderCircle, Bot, Telescope } from 'lucide-vue-next' import {
PanelLeftOpen,
MessageCirclePlus,
LoaderCircle,
Bot,
Telescope,
ChevronDown
} from 'lucide-vue-next'
import { handleChatError, handleValidationError } from '@/utils/errorHandler' import { handleChatError, handleValidationError } from '@/utils/errorHandler'
import { ScrollController } from '@/utils/scrollController' import { ScrollController } from '@/utils/scrollController'
import { AgentValidator } from '@/utils/agentValidator' import { AgentValidator } from '@/utils/agentValidator'
@ -359,7 +397,8 @@ const threadAttachmentsMap = ref({})
// UI 使 // UI 使
const localUIState = reactive({ const localUIState = reactive({
isInitialRender: true isInitialRender: true,
chatMainWidth: typeof window !== 'undefined' ? window.innerWidth : 0
}) })
// Agent Panel State // Agent Panel State
@ -392,6 +431,7 @@ const currentAgent = computed(() => {
if (!currentAgentId.value || !agents.value || !agents.value.length) return null if (!currentAgentId.value || !agents.value || !agents.value.length) return null
return agents.value.find((a) => a.id === currentAgentId.value) || null return agents.value.find((a) => a.id === currentAgentId.value) || null
}) })
const startAgents = computed(() => agents.value || [])
const chatsList = computed(() => threads.value || []) const chatsList = computed(() => threads.value || [])
const currentChatId = computed(() => chatState.currentThreadId) const currentChatId = computed(() => chatState.currentThreadId)
const currentThread = computed(() => { const currentThread = computed(() => {
@ -409,6 +449,7 @@ const currentThreadAgentName = computed(() => {
} }
return currentAgentName.value return currentAgentName.value
}) })
const currentAgentIcon = computed(() => getAgentIconComponent(currentAgentId.value))
// //
const supportsFileUpload = computed(() => { const supportsFileUpload = computed(() => {
@ -530,7 +571,7 @@ const getAgentIconComponent = (agentId) => {
} }
const agentSegmentOptions = computed(() => { const agentSegmentOptions = computed(() => {
return (agents.value || []).map((agent) => { return startAgents.value.map((agent) => {
const IconComponent = getAgentIconComponent(agent.id) const IconComponent = getAgentIconComponent(agent.id)
return { return {
label: () => label: () =>
@ -543,8 +584,16 @@ const agentSegmentOptions = computed(() => {
}) })
}) })
const showStartAgentSelector = computed(() => {
return !props.singleMode && !conversations.value.length && startAgents.value.length > 1
})
const showStartAgentDropdown = computed(() => {
return showStartAgentSelector.value && (startAgents.value.length >= 4 || localUIState.chatMainWidth < 380)
})
const showStartAgentSegment = computed(() => { const showStartAgentSegment = computed(() => {
return !props.singleMode && !conversations.value.length && agentSegmentOptions.value.length > 1 return showStartAgentSelector.value && !showStartAgentDropdown.value
}) })
const handleStartAgentChange = async (agentId) => { const handleStartAgentChange = async (agentId) => {
@ -594,9 +643,11 @@ onMounted(() => {
} }
if (window.ResizeObserver && chatMainRef.value) { if (window.ResizeObserver && chatMainRef.value) {
localUIState.chatMainWidth = chatMainRef.value.clientWidth || window.innerWidth
chatMainResizeObserver = new ResizeObserver((entries) => { chatMainResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
const width = entry.contentRect.width const width = entry.contentRect.width
localUIState.chatMainWidth = width
const isTakingSpace = chatUIStore.isSidebarOpen && !isSidebarFloating.value const isTakingSpace = chatUIStore.isSidebarOpen && !isSidebarFloating.value
if (isTakingSpace) { if (isTakingSpace) {
@ -1969,6 +2020,80 @@ watch(currentChatId, (threadId, oldThreadId) => {
} }
} }
.agent-switcher-wrapper {
display: flex;
justify-content: center;
margin: 0 auto 18px;
}
.agent-switcher-btn {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
max-width: 100%;
padding: 4px 12px;
border: 1px solid var(--gray-150);
border-radius: 8px;
background: var(--gray-0);
color: var(--gray-900);
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
&:hover {
background: var(--gray-0);
border-color: var(--gray-200);
}
}
.agent-switcher-icon,
.agent-switcher-chevron {
flex-shrink: 0;
color: var(--gray-600);
}
.agent-switcher-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.agent-switcher-menu) {
min-width: 220px;
}
:deep(.agent-switcher-menu-item) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.agent-switcher-menu-icon) {
flex-shrink: 0;
color: var(--gray-600);
}
:deep(.agent-switcher-menu-text) {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.agent-switcher-menu-badge) {
flex-shrink: 0;
padding: 1px 8px;
border-radius: 999px;
background: var(--main-30);
color: var(--main-700);
font-size: 12px;
}
.example-questions { .example-questions {
margin-top: 16px; margin-top: 16px;
text-align: center; text-align: center;
@ -2184,6 +2309,15 @@ watch(currentChatId, (threadId, oldThreadId) => {
} }
} }
.agent-switcher-wrapper {
margin-bottom: 8px;
}
.agent-switcher-btn {
width: 100%;
justify-content: center;
}
.chat-header { .chat-header {
.header__left { .header__left {
.text { .text {