Commit 9b1c3f148315835e1c8a47e95f251ffbfffefa0b
1 parent
d48457ac
fix(agent-chat): 聊天状态按 agent 隔离,切换不再串台/互相阻塞
AgentChat.vue 之前把 sending/streaming/streamContent/thinkingText/ thinkingContent/subAgents/acpLog/messages/activeConvId 全部做成页面级单例 ref, 不区分是哪个 agent 的。切 agent 时既不取消上一个还在跑的读流循环,也不重置这些 状态,导致三个连环症状:①思考中/等待回复横幅带着切过去;②只要有任意一个 agent 在跑,所有 agent 的输入框都被同一个 sending 锁成"等待回复中";③A 流式输出时切去 看 B,A 的内容被当成 B 的渲染,A 说完那句话直接被 push 进 B 的消息列表。 改为按 agentId 存一份 reactive Map(chatStates),模板读"当前选中 agent"那一份 slice;handleSend/handleEvent 认发起时捕获的 requestAgentId 写自己的 slice,不认 "用户现在选中的是谁"。switchAgent 不再无条件清空目标 agent 状态——只有它空闲时 才去刷新/定位会话,正在生成就原样展示缓存,回切能看到完整后台进度。真机验证: 向 Master 发消息流式生成期间切到另一个 agent,输入框未被锁定、无状态污染,切 回 Master 能看到完整正确的输出。 顺带把已经在这个文件里的一处未提交小改动(agent 列表排序:Master 置顶)一并 收进这次提交,不是本次改动的一部分。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
1 changed file
with
161 additions
and
125 deletions
web/src/views/AgentChat.vue
| @@ -58,7 +58,7 @@ | @@ -58,7 +58,7 @@ | ||
| 58 | v-for="conv in filteredConversations" | 58 | v-for="conv in filteredConversations" |
| 59 | :key="conv.conversationId || conv.id" | 59 | :key="conv.conversationId || conv.id" |
| 60 | class="conv-card" | 60 | class="conv-card" |
| 61 | - :class="{ active: activeConvId === (conv.conversationId || conv.id) }" | 61 | + :class="{ active: chatState.activeConvId === (conv.conversationId || conv.id) }" |
| 62 | @click="switchConversation(conv.conversationId || conv.id)" | 62 | @click="switchConversation(conv.conversationId || conv.id)" |
| 63 | > | 63 | > |
| 64 | <div class="conv-card-top"> | 64 | <div class="conv-card-top"> |
| @@ -86,7 +86,7 @@ | @@ -86,7 +86,7 @@ | ||
| 86 | 86 | ||
| 87 | <!-- 第3列: 消息区 --> | 87 | <!-- 第3列: 消息区 --> |
| 88 | <section class="chat-panel"> | 88 | <section class="chat-panel"> |
| 89 | - <div v-if="messages.length === 0 && !streaming" class="welcome-screen"> | 89 | + <div v-if="chatState.messages.length === 0 && !chatState.streaming" class="welcome-screen"> |
| 90 | <div class="welcome-content"> | 90 | <div class="welcome-content"> |
| 91 | <div class="welcome-emoji">{{ agentInfo.emoji || '🏗️' }}</div> | 91 | <div class="welcome-emoji">{{ agentInfo.emoji || '🏗️' }}</div> |
| 92 | <h2 class="welcome-title">{{ agentInfo.name || 'Agent' }}</h2> | 92 | <h2 class="welcome-title">{{ agentInfo.name || 'Agent' }}</h2> |
| @@ -99,7 +99,7 @@ | @@ -99,7 +99,7 @@ | ||
| 99 | </div> | 99 | </div> |
| 100 | </div> | 100 | </div> |
| 101 | <div v-else ref="messageListRef" class="message-list"> | 101 | <div v-else ref="messageListRef" class="message-list"> |
| 102 | - <div v-for="(msg, idx) in messages" :key="idx" class="msg-row" :class="`msg-${msg.role}`"> | 102 | + <div v-for="(msg, idx) in chatState.messages" :key="idx" class="msg-row" :class="`msg-${msg.role}`"> |
| 103 | <div class="msg-avatar" :class="`avatar-${msg.role}`">{{ msg.role === 'user' ? '👤' : (agentInfo.emoji || '🏗️') }}</div> | 103 | <div class="msg-avatar" :class="`avatar-${msg.role}`">{{ msg.role === 'user' ? '👤' : (agentInfo.emoji || '🏗️') }}</div> |
| 104 | <div class="msg-body"> | 104 | <div class="msg-body"> |
| 105 | <div class="msg-header"> | 105 | <div class="msg-header"> |
| @@ -117,24 +117,24 @@ | @@ -117,24 +117,24 @@ | ||
| 117 | </div> | 117 | </div> |
| 118 | </div> | 118 | </div> |
| 119 | </div> | 119 | </div> |
| 120 | - <div v-if="streaming" class="msg-row msg-assistant streaming"> | 120 | + <div v-if="chatState.streaming" class="msg-row msg-assistant streaming"> |
| 121 | <div class="msg-avatar avatar-assistant">{{ agentInfo.emoji || '🏗️' }}</div> | 121 | <div class="msg-avatar avatar-assistant">{{ agentInfo.emoji || '🏗️' }}</div> |
| 122 | <div class="msg-body"> | 122 | <div class="msg-body"> |
| 123 | <div class="msg-header"><span class="msg-role">{{ agentInfo.name }}</span><span class="typing-indicator"><span></span><span></span><span></span></span></div> | 123 | <div class="msg-header"><span class="msg-role">{{ agentInfo.name }}</span><span class="typing-indicator"><span></span><span></span><span></span></span></div> |
| 124 | - <div class="msg-content" v-html="renderMarkdown(streamContent)"></div> | 124 | + <div class="msg-content" v-html="renderMarkdown(chatState.streamContent)"></div> |
| 125 | </div> | 125 | </div> |
| 126 | </div> | 126 | </div> |
| 127 | - <div v-if="sending && thinkingContent" class="thinking-block"> | ||
| 128 | - <div class="thinking-header" @click="showThinking = !showThinking"> | 127 | + <div v-if="chatState.sending && chatState.thinkingContent" class="thinking-block"> |
| 128 | + <div class="thinking-header" @click="chatState.showThinking = !chatState.showThinking"> | ||
| 129 | <div class="thinking-pulse"></div> | 129 | <div class="thinking-pulse"></div> |
| 130 | - <span class="thinking-label">{{ streaming ? '💭 思考完成' : '💭 思考中...' }}</span> | ||
| 131 | - <span class="thinking-toggle">{{ showThinking ? '▼' : '▶' }}</span> | 130 | + <span class="thinking-label">{{ chatState.streaming ? '💭 思考完成' : '💭 思考中...' }}</span> |
| 131 | + <span class="thinking-toggle">{{ chatState.showThinking ? '▼' : '▶' }}</span> | ||
| 132 | </div> | 132 | </div> |
| 133 | - <div v-if="showThinking" class="thinking-content">{{ thinkingContent }}</div> | 133 | + <div v-if="chatState.showThinking" class="thinking-content">{{ chatState.thinkingContent }}</div> |
| 134 | </div> | 134 | </div> |
| 135 | - <div v-if="sending && !streaming && !thinkingContent" class="thinking-row"> | 135 | + <div v-if="chatState.sending && !chatState.streaming && !chatState.thinkingContent" class="thinking-row"> |
| 136 | <div class="thinking-pulse"></div> | 136 | <div class="thinking-pulse"></div> |
| 137 | - <span class="thinking-text">{{ thinkingText }}</span> | 137 | + <span class="thinking-text">{{ chatState.thinkingText }}</span> |
| 138 | </div> | 138 | </div> |
| 139 | </div> | 139 | </div> |
| 140 | <div class="chat-input-area" @dragover.prevent="dragOver = true" @dragleave.prevent="dragOver = false" @drop.prevent="handleDrop"> | 140 | <div class="chat-input-area" @dragover.prevent="dragOver = true" @dragleave.prevent="dragOver = false" @drop.prevent="handleDrop"> |
| @@ -160,28 +160,28 @@ | @@ -160,28 +160,28 @@ | ||
| 160 | <div class="input-box" :class="{ focused: inputFocused }"> | 160 | <div class="input-box" :class="{ focused: inputFocused }"> |
| 161 | <button class="attach-btn" @click="triggerFileInput" title="上传文件">📎</button> | 161 | <button class="attach-btn" @click="triggerFileInput" title="上传文件">📎</button> |
| 162 | <input type="file" ref="fileInputRef" multiple style="display:none" @change="handleFileSelect" /> | 162 | <input type="file" ref="fileInputRef" multiple style="display:none" @change="handleFileSelect" /> |
| 163 | - <textarea ref="inputRef" v-model="inputText" :disabled="sending" :placeholder="sending ? '等待回复中...' : '跟 ' + agentInfo.name + ' 对话...'" class="msg-textarea" rows="1" @keydown="handleKeydown" @focus="inputFocused = true" @blur="inputFocused = false" @input="autoResize" @paste="handlePaste"></textarea> | ||
| 164 | - <button v-if="sending" class="send-btn stop-btn" @click="handleStop">⏹</button> | ||
| 165 | - <button v-else class="send-btn" :class="{ ready: (inputText.trim() || pendingFiles.length) && !sending }" :disabled="!inputText.trim() && !pendingFiles.length" @click="handleSend">➤</button> | 163 | + <textarea ref="inputRef" v-model="inputText" :disabled="chatState.sending" :placeholder="chatState.sending ? '等待回复中...' : '跟 ' + agentInfo.name + ' 对话...'" class="msg-textarea" rows="1" @keydown="handleKeydown" @focus="inputFocused = true" @blur="inputFocused = false" @input="autoResize" @paste="handlePaste"></textarea> |
| 164 | + <button v-if="chatState.sending" class="send-btn stop-btn" @click="handleStop">⏹</button> | ||
| 165 | + <button v-else class="send-btn" :class="{ ready: (inputText.trim() || pendingFiles.length) && !chatState.sending }" :disabled="!inputText.trim() && !pendingFiles.length" @click="handleSend">➤</button> | ||
| 166 | </div> | 166 | </div> |
| 167 | - <div class="input-hint">Shift+Enter 换行 · {{ messages.length }} 条消息<template v-if="pendingFiles.length"> · {{ pendingFiles.length }} 个待上传文件</template><template v-if="uploadedFiles.length"> · 📎 {{ uploadedFiles.length }} 个附件</template></div> | 167 | + <div class="input-hint">Shift+Enter 换行 · {{ chatState.messages.length }} 条消息<template v-if="pendingFiles.length"> · {{ pendingFiles.length }} 个待上传文件</template><template v-if="uploadedFiles.length"> · 📎 {{ uploadedFiles.length }} 个附件</template></div> |
| 168 | </div> | 168 | </div> |
| 169 | </section> | 169 | </section> |
| 170 | 170 | ||
| 171 | <!-- 第4列: ACP 事件 --> | 171 | <!-- 第4列: ACP 事件 --> |
| 172 | <aside class="acp-panel"> | 172 | <aside class="acp-panel"> |
| 173 | - <div class="col-header"><span>⚡ ACP Events</span><span class="acp-count">{{ acpLog.length }}</span></div> | 173 | + <div class="col-header"><span>⚡ ACP Events</span><span class="acp-count">{{ chatState.acpLog.length }}</span></div> |
| 174 | <div class="acp-list"> | 174 | <div class="acp-list"> |
| 175 | - <div v-if="acpLog.length > 0" class="acp-card"> | ||
| 176 | - <div class="acp-card-header"><span>⚡ Event Stream</span><span v-if="sending" class="badge-live">LIVE</span></div> | 175 | + <div v-if="chatState.acpLog.length > 0" class="acp-card"> |
| 176 | + <div class="acp-card-header"><span>⚡ Event Stream</span><span v-if="chatState.sending" class="badge-live">LIVE</span></div> | ||
| 177 | <div class="acp-log-stream"> | 177 | <div class="acp-log-stream"> |
| 178 | - <div v-for="(evt, i) in acpLog" :key="i" class="acp-event-line" :class="'acp-' + evt.type"> | 178 | + <div v-for="(evt, i) in chatState.acpLog" :key="i" class="acp-event-line" :class="'acp-' + evt.type"> |
| 179 | <span class="acp-icon">{{ evt.icon }}</span> | 179 | <span class="acp-icon">{{ evt.icon }}</span> |
| 180 | <span class="acp-text">{{ evt.text }}</span> | 180 | <span class="acp-text">{{ evt.text }}</span> |
| 181 | </div> | 181 | </div> |
| 182 | </div> | 182 | </div> |
| 183 | </div> | 183 | </div> |
| 184 | - <div v-for="(sub, callId) in subAgents" :key="'live-'+callId" class="sub-log-card" :class="sub.status"> | 184 | + <div v-for="(sub, callId) in chatState.subAgents" :key="'live-'+callId" class="sub-log-card" :class="sub.status"> |
| 185 | <div class="sub-log-card-header" @click="sub.collapsed = !sub.collapsed"> | 185 | <div class="sub-log-card-header" @click="sub.collapsed = !sub.collapsed"> |
| 186 | <span class="sub-log-icon">{{ sub.status === 'running' ? '🚀' : (sub.status === 'success' ? '✅' : '❌') }}</span> | 186 | <span class="sub-log-icon">{{ sub.status === 'running' ? '🚀' : (sub.status === 'success' ? '✅' : '❌') }}</span> |
| 187 | <span class="sub-log-agent">{{ sub.agent }}</span> | 187 | <span class="sub-log-agent">{{ sub.agent }}</span> |
| @@ -190,7 +190,7 @@ | @@ -190,7 +190,7 @@ | ||
| 190 | </div> | 190 | </div> |
| 191 | <div v-if="!sub.collapsed" class="sub-log-content terminal">{{ sub.content || '等待输出...' }}</div> | 191 | <div v-if="!sub.collapsed" class="sub-log-content terminal">{{ sub.content || '等待输出...' }}</div> |
| 192 | </div> | 192 | </div> |
| 193 | - <div v-for="log in subLogList" :key="log.file" class="sub-log-card" :class="log.status"> | 193 | + <div v-for="log in chatState.subLogList" :key="log.file" class="sub-log-card" :class="log.status"> |
| 194 | <div class="sub-log-card-header" @click="log.collapsed = !log.collapsed"> | 194 | <div class="sub-log-card-header" @click="log.collapsed = !log.collapsed"> |
| 195 | <span class="sub-log-icon">{{ log.status === 'success' ? '✅' : (log.status === 'error' ? '❌' : '⏳') }}</span> | 195 | <span class="sub-log-icon">{{ log.status === 'success' ? '✅' : (log.status === 'error' ? '❌' : '⏳') }}</span> |
| 196 | <span class="sub-log-agent">{{ log.agent }}</span> | 196 | <span class="sub-log-agent">{{ log.agent }}</span> |
| @@ -198,7 +198,7 @@ | @@ -198,7 +198,7 @@ | ||
| 198 | </div> | 198 | </div> |
| 199 | <div v-if="!log.collapsed" class="sub-log-content terminal">{{ log.content }}</div> | 199 | <div v-if="!log.collapsed" class="sub-log-content terminal">{{ log.content }}</div> |
| 200 | </div> | 200 | </div> |
| 201 | - <div v-if="acpLog.length === 0 && subLogList.length === 0 && Object.keys(subAgents).length === 0" class="acp-empty"> | 201 | + <div v-if="chatState.acpLog.length === 0 && chatState.subLogList.length === 0 && Object.keys(chatState.subAgents).length === 0" class="acp-empty"> |
| 202 | 发送消息后,ACP 事件<br>将在这里实时显示 | 202 | 发送消息后,ACP 事件<br>将在这里实时显示 |
| 203 | </div> | 203 | </div> |
| 204 | </div> | 204 | </div> |
| @@ -326,7 +326,7 @@ | @@ -326,7 +326,7 @@ | ||
| 326 | </template> | 326 | </template> |
| 327 | 327 | ||
| 328 | <script setup> | 328 | <script setup> |
| 329 | -import { ref, computed, nextTick, watch, onMounted } from 'vue' | 329 | +import { ref, reactive, computed, nextTick, watch, onMounted } from 'vue' |
| 330 | import { useRoute, useRouter } from 'vue-router' | 330 | import { useRoute, useRouter } from 'vue-router' |
| 331 | import AppNav from '@/components/AppNav.vue' | 331 | import AppNav from '@/components/AppNav.vue' |
| 332 | import FileNode from '@/components/FileNode.vue' | 332 | import FileNode from '@/components/FileNode.vue' |
| @@ -356,9 +356,11 @@ const groupedAgents = computed(() => { | @@ -356,9 +356,11 @@ const groupedAgents = computed(() => { | ||
| 356 | groups[comp].push(a) | 356 | groups[comp].push(a) |
| 357 | }) | 357 | }) |
| 358 | 358 | ||
| 359 | - // Sort each group by lastActiveAt descending | 359 | + // Master agent 置顶,其余按 lastActiveAt 降序 |
| 360 | Object.keys(groups).forEach(k => { | 360 | Object.keys(groups).forEach(k => { |
| 361 | groups[k].sort((a, b) => { | 361 | groups[k].sort((a, b) => { |
| 362 | + if (a.type === 'master') return -1 | ||
| 363 | + if (b.type === 'master') return 1 | ||
| 362 | const ta = a.lastActiveAt ? new Date(a.lastActiveAt).getTime() : 0 | 364 | const ta = a.lastActiveAt ? new Date(a.lastActiveAt).getTime() : 0 |
| 363 | const tb = b.lastActiveAt ? new Date(b.lastActiveAt).getTime() : 0 | 365 | const tb = b.lastActiveAt ? new Date(b.lastActiveAt).getTime() : 0 |
| 364 | return tb - ta | 366 | return tb - ta |
| @@ -378,6 +380,25 @@ const groupedAgents = computed(() => { | @@ -378,6 +380,25 @@ const groupedAgents = computed(() => { | ||
| 378 | }) | 380 | }) |
| 379 | const activeTab = ref('chat') // 'chat' | 'detail' | 381 | const activeTab = ref('chat') // 'chat' | 'detail' |
| 380 | 382 | ||
| 383 | +// 每个 agent 独立一份聊天态(messages/sending/streaming/思考栏/ACP 面板…), | ||
| 384 | +// 键是 agentId。切换 agent 只是换了 chatState 指向哪个 slice 来渲染, | ||
| 385 | +// 后台的 fetch 读流循环(handleSend/handleEvent) 认准发起时捕获的 agentId 写自己那份, | ||
| 386 | +// 不认"当前选中的 agent 是谁"——这样 A 在流式输出时切去看 B,A 不会被打断, | ||
| 387 | +// B 也不会被 A 的内容/思考栏/等待态污染,回切 A 能看到它这期间的完整进度。 | ||
| 388 | +const chatStates = reactive(new Map()) | ||
| 389 | +function ensureChatState(id) { | ||
| 390 | + if (!chatStates.has(id)) { | ||
| 391 | + chatStates.set(id, { | ||
| 392 | + messages: [], activeConvId: null, | ||
| 393 | + sending: false, streaming: false, streamContent: '', | ||
| 394 | + subAgents: {}, subLogList: [], acpLog: [], | ||
| 395 | + thinkingText: '🏗️ 启动中...', thinkingContent: '', showThinking: true, | ||
| 396 | + }) | ||
| 397 | + } | ||
| 398 | + return chatStates.get(id) | ||
| 399 | +} | ||
| 400 | +const chatState = computed(() => ensureChatState(agentId.value)) | ||
| 401 | + | ||
| 381 | const agentInfo = ref({ name: 'Agent Creator', emoji: '🏗️', type: 'meta', status: 'ready', greeting: '', bootstrapFiles: [] }) | 402 | const agentInfo = ref({ name: 'Agent Creator', emoji: '🏗️', type: 'meta', status: 'ready', greeting: '', bootstrapFiles: [] }) |
| 382 | 403 | ||
| 383 | // 状态映射:英文 -> 中文 | 404 | // 状态映射:英文 -> 中文 |
| @@ -429,21 +450,10 @@ const filteredConversations = computed(() => { | @@ -429,21 +450,10 @@ const filteredConversations = computed(() => { | ||
| 429 | if (convFilter.value === 'all') return conversations.value | 450 | if (convFilter.value === 'all') return conversations.value |
| 430 | return conversations.value.filter(c => getConvType(c.conversationId || c.id) === convFilter.value) | 451 | return conversations.value.filter(c => getConvType(c.conversationId || c.id) === convFilter.value) |
| 431 | }) | 452 | }) |
| 432 | -const messages = ref([]) | ||
| 433 | const inputText = ref('') | 453 | const inputText = ref('') |
| 434 | const inputFocused = ref(false) | 454 | const inputFocused = ref(false) |
| 435 | -const sending = ref(false) | ||
| 436 | -const streaming = ref(false) | ||
| 437 | -const streamContent = ref('') | ||
| 438 | -const subAgents = ref({}) | ||
| 439 | -const subLogList = ref([]) | ||
| 440 | -const acpLog = ref([]) | ||
| 441 | -const thinkingText = ref('🏗️ 启动中...') | ||
| 442 | -const thinkingContent = ref('') | ||
| 443 | -const showThinking = ref(true) | ||
| 444 | const messageListRef = ref(null) | 455 | const messageListRef = ref(null) |
| 445 | const inputRef = ref(null) | 456 | const inputRef = ref(null) |
| 446 | -const activeConvId = ref(null) | ||
| 447 | 457 | ||
| 448 | // Detail mode state | 458 | // Detail mode state |
| 449 | const fileTree = ref(null) | 459 | const fileTree = ref(null) |
| @@ -516,16 +526,16 @@ async function uploadPendingFiles() { | @@ -516,16 +526,16 @@ async function uploadPendingFiles() { | ||
| 516 | for (const pf of pendingFiles.value) { | 526 | for (const pf of pendingFiles.value) { |
| 517 | formData.append('files', pf.file) | 527 | formData.append('files', pf.file) |
| 518 | } | 528 | } |
| 519 | - if (activeConvId.value) { | ||
| 520 | - formData.append('conversationId', activeConvId.value) | 529 | + if (chatState.value.activeConvId) { |
| 530 | + formData.append('conversationId', chatState.value.activeConvId) | ||
| 521 | } | 531 | } |
| 522 | try { | 532 | try { |
| 523 | const r = await fetch(`/api/agent/${agentId.value}/upload`, { method: 'POST', body: formData }) | 533 | const r = await fetch(`/api/agent/${agentId.value}/upload`, { method: 'POST', body: formData }) |
| 524 | if (r.ok) { | 534 | if (r.ok) { |
| 525 | const data = await r.json() | 535 | const data = await r.json() |
| 526 | // Auto-set convId if server created one | 536 | // Auto-set convId if server created one |
| 527 | - if (data.conversationId && !activeConvId.value) { | ||
| 528 | - activeConvId.value = data.conversationId | 537 | + if (data.conversationId && !chatState.value.activeConvId) { |
| 538 | + chatState.value.activeConvId = data.conversationId | ||
| 529 | } | 539 | } |
| 530 | uploadedFiles.value.push(...(data.uploaded || [])) | 540 | uploadedFiles.value.push(...(data.uploaded || [])) |
| 531 | pendingFiles.value = [] | 541 | pendingFiles.value = [] |
| @@ -536,9 +546,9 @@ async function uploadPendingFiles() { | @@ -536,9 +546,9 @@ async function uploadPendingFiles() { | ||
| 536 | } | 546 | } |
| 537 | 547 | ||
| 538 | async function loadUploadedFiles() { | 548 | async function loadUploadedFiles() { |
| 539 | - if (!agentId.value || !activeConvId.value) { uploadedFiles.value = []; return } | 549 | + if (!agentId.value || !chatState.value.activeConvId) { uploadedFiles.value = []; return } |
| 540 | try { | 550 | try { |
| 541 | - const r = await fetch(`/api/agent/${agentId.value}/uploads/${activeConvId.value}`) | 551 | + const r = await fetch(`/api/agent/${agentId.value}/uploads/${chatState.value.activeConvId}`) |
| 542 | if (r.ok) uploadedFiles.value = await r.json() | 552 | if (r.ok) uploadedFiles.value = await r.json() |
| 543 | else uploadedFiles.value = [] | 553 | else uploadedFiles.value = [] |
| 544 | } catch { uploadedFiles.value = [] } | 554 | } catch { uploadedFiles.value = [] } |
| @@ -652,16 +662,17 @@ async function loadConversations() { | @@ -652,16 +662,17 @@ async function loadConversations() { | ||
| 652 | 662 | ||
| 653 | async function loadConversation(convId) { | 663 | async function loadConversation(convId) { |
| 654 | if (!agentId.value) return | 664 | if (!agentId.value) return |
| 665 | + const st = chatState.value | ||
| 655 | try { | 666 | try { |
| 656 | const r = await fetch(`/api/agent/${agentId.value}/conversations/${convId}`) | 667 | const r = await fetch(`/api/agent/${agentId.value}/conversations/${convId}`) |
| 657 | if (r.ok) { | 668 | if (r.ok) { |
| 658 | const conv = await r.json() | 669 | const conv = await r.json() |
| 659 | - messages.value = conv.messages || [] | 670 | + st.messages = conv.messages || [] |
| 660 | if (conv.subLogs && conv.subLogs.length > 0) { | 671 | if (conv.subLogs && conv.subLogs.length > 0) { |
| 661 | - subLogList.value = conv.subLogs.map(l => ({ ...l, collapsed: true })) | ||
| 662 | - } else { subLogList.value = [] } | 672 | + st.subLogList = conv.subLogs.map(l => ({ ...l, collapsed: true })) |
| 673 | + } else { st.subLogList = [] } | ||
| 663 | } | 674 | } |
| 664 | - } catch { messages.value = []; subLogList.value = [] } | 675 | + } catch { st.messages = []; st.subLogList = [] } |
| 665 | } | 676 | } |
| 666 | 677 | ||
| 667 | async function loadFileTree() { | 678 | async function loadFileTree() { |
| @@ -724,15 +735,16 @@ async function deleteFile(filePath) { | @@ -724,15 +735,16 @@ async function deleteFile(filePath) { | ||
| 724 | } | 735 | } |
| 725 | 736 | ||
| 726 | function newConversation() { | 737 | function newConversation() { |
| 727 | - activeConvId.value = null | ||
| 728 | - messages.value = [] | ||
| 729 | - streamContent.value = '' | ||
| 730 | - subAgents.value = {} | ||
| 731 | - subLogList.value = [] | ||
| 732 | - acpLog.value = [] | 738 | + const st = chatState.value |
| 739 | + st.activeConvId = null | ||
| 740 | + st.messages = [] | ||
| 741 | + st.streamContent = '' | ||
| 742 | + st.subAgents = {} | ||
| 743 | + st.subLogList = [] | ||
| 744 | + st.acpLog = [] | ||
| 733 | pendingFiles.value = [] | 745 | pendingFiles.value = [] |
| 734 | uploadedFiles.value = [] | 746 | uploadedFiles.value = [] |
| 735 | - | 747 | + |
| 736 | if (route.params.id) { | 748 | if (route.params.id) { |
| 737 | router.replace({ params: { id: route.params.id, agentId: agentId.value, convId: undefined }}) | 749 | router.replace({ params: { id: route.params.id, agentId: agentId.value, convId: undefined }}) |
| 738 | } else { | 750 | } else { |
| @@ -740,22 +752,34 @@ function newConversation() { | @@ -740,22 +752,34 @@ function newConversation() { | ||
| 740 | } | 752 | } |
| 741 | } | 753 | } |
| 742 | 754 | ||
| 755 | +// 切 agent:不清空目标 agent 已经攒下的后台状态(可能正在生成中,或者刚刚生成完毕 | ||
| 756 | +// 还没被看到)——只有它是空闲态时才去定位/刷新它的会话,正在 sending 就原样展示缓存。 | ||
| 743 | async function switchAgent() { | 757 | async function switchAgent() { |
| 744 | - newConversation() | ||
| 745 | selectedFile.value = null | 758 | selectedFile.value = null |
| 746 | previewContent.value = null | 759 | previewContent.value = null |
| 747 | fileTree.value = null | 760 | fileTree.value = null |
| 761 | + const st = chatState.value | ||
| 762 | + if (route.params.id) { | ||
| 763 | + router.replace({ params: { id: route.params.id, agentId: agentId.value, convId: st.activeConvId || undefined }}) | ||
| 764 | + } else { | ||
| 765 | + router.replace({ params: { agentId: agentId.value, convId: st.activeConvId || undefined }}) | ||
| 766 | + } | ||
| 748 | await loadAgentInfo() | 767 | await loadAgentInfo() |
| 749 | await loadConversations() | 768 | await loadConversations() |
| 750 | - if (filteredConversations.value.length > 0) { | ||
| 751 | - const firstId = filteredConversations.value[0].conversationId || filteredConversations.value[0].id | ||
| 752 | - await switchConversation(firstId) | 769 | + if (!st.sending) { |
| 770 | + if (st.activeConvId) { | ||
| 771 | + await loadConversation(st.activeConvId) | ||
| 772 | + } else if (filteredConversations.value.length > 0) { | ||
| 773 | + const firstId = filteredConversations.value[0].conversationId || filteredConversations.value[0].id | ||
| 774 | + await switchConversation(firstId) | ||
| 775 | + } | ||
| 753 | } | 776 | } |
| 754 | await loadFileTree() | 777 | await loadFileTree() |
| 778 | + scrollToBottom() | ||
| 755 | } | 779 | } |
| 756 | 780 | ||
| 757 | async function switchConversation(convId) { | 781 | async function switchConversation(convId) { |
| 758 | - activeConvId.value = convId | 782 | + chatState.value.activeConvId = convId |
| 759 | if (route.params.id) { | 783 | if (route.params.id) { |
| 760 | router.replace({ params: { id: route.params.id, agentId: agentId.value, convId } }) | 784 | router.replace({ params: { id: route.params.id, agentId: agentId.value, convId } }) |
| 761 | } else { | 785 | } else { |
| @@ -774,13 +798,14 @@ async function deleteConversation(convId) { | @@ -774,13 +798,14 @@ async function deleteConversation(convId) { | ||
| 774 | const r = await fetch(`/api/agent/${agentId.value}/conversations/${convId}`, { method: 'DELETE' }) | 798 | const r = await fetch(`/api/agent/${agentId.value}/conversations/${convId}`, { method: 'DELETE' }) |
| 775 | if (r.ok) { | 799 | if (r.ok) { |
| 776 | conversations.value = conversations.value.filter(c => (c.conversationId || c.id) !== convId) | 800 | conversations.value = conversations.value.filter(c => (c.conversationId || c.id) !== convId) |
| 777 | - if (activeConvId.value === convId) { | ||
| 778 | - activeConvId.value = null | ||
| 779 | - messages.value = [] | ||
| 780 | - streamContent.value = '' | ||
| 781 | - subAgents.value = {} | ||
| 782 | - subLogList.value = [] | ||
| 783 | - acpLog.value = [] | 801 | + const st = chatState.value |
| 802 | + if (st.activeConvId === convId) { | ||
| 803 | + st.activeConvId = null | ||
| 804 | + st.messages = [] | ||
| 805 | + st.streamContent = '' | ||
| 806 | + st.subAgents = {} | ||
| 807 | + st.subLogList = [] | ||
| 808 | + st.acpLog = [] | ||
| 784 | } | 809 | } |
| 785 | } | 810 | } |
| 786 | } catch (e) { console.error('Delete conv error:', e) } | 811 | } catch (e) { console.error('Delete conv error:', e) } |
| @@ -810,9 +835,14 @@ async function copyText(text) { | @@ -810,9 +835,14 @@ async function copyText(text) { | ||
| 810 | 835 | ||
| 811 | // ─── Chat (SSE) ─── | 836 | // ─── Chat (SSE) ─── |
| 812 | 837 | ||
| 838 | +// requestAgentId 是发起这次发送时的 agent——从这里往下,全部认这个快照,不认 | ||
| 839 | +// "用户现在选中的是谁"。这样切走了也不会打断/污染,切回来能看到完整的后台进度。 | ||
| 813 | async function handleSend() { | 840 | async function handleSend() { |
| 814 | const text = inputText.value.trim() | 841 | const text = inputText.value.trim() |
| 815 | - if ((!text && pendingFiles.value.length === 0) || sending.value || !agentId.value) return | 842 | + const requestAgentId = agentId.value |
| 843 | + const st = ensureChatState(requestAgentId) | ||
| 844 | + if ((!text && pendingFiles.value.length === 0) || st.sending || !requestAgentId) return | ||
| 845 | + const isCurrent = () => agentId.value === requestAgentId | ||
| 816 | 846 | ||
| 817 | // Upload pending files first | 847 | // Upload pending files first |
| 818 | if (pendingFiles.value.length > 0) { | 848 | if (pendingFiles.value.length > 0) { |
| @@ -821,27 +851,26 @@ async function handleSend() { | @@ -821,27 +851,26 @@ async function handleSend() { | ||
| 821 | 851 | ||
| 822 | const finalText = text || '请处理我上传的文件。' | 852 | const finalText = text || '请处理我上传的文件。' |
| 823 | const attachments = uploadedFiles.value.length > 0 ? [...uploadedFiles.value] : [] | 853 | const attachments = uploadedFiles.value.length > 0 ? [...uploadedFiles.value] : [] |
| 824 | - messages.value.push({ role: 'user', content: finalText, timestamp: new Date().toISOString(), attachments }) | 854 | + st.messages.push({ role: 'user', content: finalText, timestamp: new Date().toISOString(), attachments }) |
| 825 | inputText.value = '' | 855 | inputText.value = '' |
| 826 | if (inputRef.value) inputRef.value.style.height = 'auto' | 856 | if (inputRef.value) inputRef.value.style.height = 'auto' |
| 827 | - sending.value = true; streaming.value = false; streamContent.value = '' | ||
| 828 | - subAgents.value = {}; acpLog.value = []; thinkingContent.value = '' | ||
| 829 | - showThinking.value = true | ||
| 830 | - thinkingText.value = `${agentInfo.value.emoji} ${agentInfo.value.name} 思考中...` | ||
| 831 | - activeTab.value = 'chat' // ensure we're in chat mode | ||
| 832 | - scrollToBottom() | ||
| 833 | - | ||
| 834 | - if (activeConvId.value) { | ||
| 835 | - const existsInList = conversations.value.some(c => (c.conversationId || c.id) === activeConvId.value) | 857 | + st.sending = true; st.streaming = false; st.streamContent = '' |
| 858 | + st.subAgents = {}; st.acpLog = []; st.thinkingContent = '' | ||
| 859 | + st.showThinking = true | ||
| 860 | + st.thinkingText = `${agentInfo.value.emoji} ${agentInfo.value.name} 思考中...` | ||
| 861 | + if (isCurrent()) { activeTab.value = 'chat'; scrollToBottom() } | ||
| 862 | + | ||
| 863 | + if (st.activeConvId && isCurrent()) { | ||
| 864 | + const existsInList = conversations.value.some(c => (c.conversationId || c.id) === st.activeConvId) | ||
| 836 | if (!existsInList) { | 865 | if (!existsInList) { |
| 837 | - conversations.value.unshift({ conversationId: activeConvId.value, title: finalText.slice(0, 50), updatedAt: new Date().toISOString(), messageCount: messages.value.length, status: 'running' }) | 866 | + conversations.value.unshift({ conversationId: st.activeConvId, title: finalText.slice(0, 50), updatedAt: new Date().toISOString(), messageCount: st.messages.length, status: 'running' }) |
| 838 | } | 867 | } |
| 839 | } | 868 | } |
| 840 | 869 | ||
| 841 | try { | 870 | try { |
| 842 | - const r = await fetch(`/api/agent/${agentId.value}/chat`, { | 871 | + const r = await fetch(`/api/agent/${requestAgentId}/chat`, { |
| 843 | method: 'POST', headers: { 'Content-Type': 'application/json' }, | 872 | method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| 844 | - body: JSON.stringify({ message: finalText, conversationId: activeConvId.value }), | 873 | + body: JSON.stringify({ message: finalText, conversationId: st.activeConvId }), |
| 845 | }) | 874 | }) |
| 846 | const reader = r.body.getReader() | 875 | const reader = r.body.getReader() |
| 847 | const decoder = new TextDecoder() | 876 | const decoder = new TextDecoder() |
| @@ -855,86 +884,93 @@ async function handleSend() { | @@ -855,86 +884,93 @@ async function handleSend() { | ||
| 855 | for (const line of lines) { | 884 | for (const line of lines) { |
| 856 | if (line.startsWith('event: ')) lastEventType = line.slice(7) | 885 | if (line.startsWith('event: ')) lastEventType = line.slice(7) |
| 857 | else if (line.startsWith('data: ')) { | 886 | else if (line.startsWith('data: ')) { |
| 858 | - try { handleEvent(lastEventType, JSON.parse(line.slice(6))) } catch {} | 887 | + try { handleEvent(st, requestAgentId, lastEventType, JSON.parse(line.slice(6))) } catch {} |
| 859 | } | 888 | } |
| 860 | } | 889 | } |
| 861 | } | 890 | } |
| 862 | - if (streamContent.value) { | ||
| 863 | - messages.value.push({ role: 'assistant', content: streamContent.value, timestamp: new Date().toISOString() }) | 891 | + if (st.streamContent) { |
| 892 | + st.messages.push({ role: 'assistant', content: st.streamContent, timestamp: new Date().toISOString() }) | ||
| 864 | } | 893 | } |
| 865 | - await loadConversations() | ||
| 866 | - if (activeConvId.value) await loadSubLogs() | 894 | + if (isCurrent()) await loadConversations() |
| 895 | + if (st.activeConvId) await loadSubLogsFor(requestAgentId, st) | ||
| 867 | } catch (err) { | 896 | } catch (err) { |
| 868 | - messages.value.push({ role: 'assistant', content: `❌ 错误: ${err.message}`, timestamp: new Date().toISOString() }) | 897 | + st.messages.push({ role: 'assistant', content: `❌ 错误: ${err.message}`, timestamp: new Date().toISOString() }) |
| 869 | } finally { | 898 | } finally { |
| 870 | - sending.value = false; streaming.value = false; streamContent.value = ''; thinkingContent.value = '' | 899 | + st.sending = false; st.streaming = false; st.streamContent = ''; st.thinkingContent = '' |
| 871 | } | 900 | } |
| 872 | } | 901 | } |
| 873 | 902 | ||
| 874 | -async function loadSubLogs() { | 903 | +async function loadSubLogsFor(id, st) { |
| 875 | try { | 904 | try { |
| 876 | - const r = await fetch(`/api/agent/${agentId.value}/conversations/${activeConvId.value}/sub-logs`) | ||
| 877 | - if (r.ok) { const logs = await r.json(); subLogList.value = logs.map(l => ({ ...l, collapsed: true })) } | 905 | + const r = await fetch(`/api/agent/${id}/conversations/${st.activeConvId}/sub-logs`) |
| 906 | + if (r.ok) { const logs = await r.json(); st.subLogList = logs.map(l => ({ ...l, collapsed: true })) } | ||
| 878 | } catch {} | 907 | } catch {} |
| 879 | } | 908 | } |
| 880 | 909 | ||
| 881 | async function handleStop() { | 910 | async function handleStop() { |
| 882 | - if (!activeConvId.value) return | ||
| 883 | - try { await fetch(`/api/agent/${agentId.value}/chat/${activeConvId.value}/stop`, { method: 'POST' }) } catch(e) { console.error('Stop error:', e) } | ||
| 884 | - sending.value = false; streaming.value = false | ||
| 885 | - messages.value.push({ role: 'assistant', content: `*[已终止]*`, timestamp: new Date().toISOString() }) | 911 | + const st = chatState.value |
| 912 | + if (!st.activeConvId) return | ||
| 913 | + try { await fetch(`/api/agent/${agentId.value}/chat/${st.activeConvId}/stop`, { method: 'POST' }) } catch(e) { console.error('Stop error:', e) } | ||
| 914 | + st.sending = false; st.streaming = false | ||
| 915 | + st.messages.push({ role: 'assistant', content: `*[已终止]*`, timestamp: new Date().toISOString() }) | ||
| 886 | scrollToBottom() | 916 | scrollToBottom() |
| 887 | } | 917 | } |
| 888 | 918 | ||
| 889 | -function handleEvent(type, data) { | 919 | +// st/requestAgentId 是 handleSend 传下来的快照;只有 requestAgentId 仍是当前选中 agent 时 |
| 920 | +// 才动 DOM(滚动)/URL/全局对话列表——避免把某个后台 agent 的进度"带"到当前正看的页面上。 | ||
| 921 | +function handleEvent(st, requestAgentId, type, data) { | ||
| 922 | + const isCurrent = agentId.value === requestAgentId | ||
| 890 | switch (type) { | 923 | switch (type) { |
| 891 | case 'session': | 924 | case 'session': |
| 892 | - if (data.conversationId && !activeConvId.value) { | ||
| 893 | - activeConvId.value = data.conversationId | ||
| 894 | - if (route.params.id) { | ||
| 895 | - router.replace({ params: { id: route.params.id, agentId: agentId.value, convId: data.conversationId } }) | ||
| 896 | - } else { | ||
| 897 | - router.replace({ params: { agentId: agentId.value, convId: data.conversationId } }) | ||
| 898 | - } | ||
| 899 | - const existsInList = conversations.value.some(c => (c.conversationId || c.id) === data.conversationId) | ||
| 900 | - if (!existsInList) { | ||
| 901 | - conversations.value.unshift({ conversationId: data.conversationId, title: messages.value.find(m => m.role === 'user')?.content?.slice(0, 50) || '新对话', updatedAt: new Date().toISOString(), messageCount: 1, status: 'running' }) | 925 | + if (data.conversationId && !st.activeConvId) { |
| 926 | + st.activeConvId = data.conversationId | ||
| 927 | + if (isCurrent) { | ||
| 928 | + if (route.params.id) { | ||
| 929 | + router.replace({ params: { id: route.params.id, agentId: requestAgentId, convId: data.conversationId } }) | ||
| 930 | + } else { | ||
| 931 | + router.replace({ params: { agentId: requestAgentId, convId: data.conversationId } }) | ||
| 932 | + } | ||
| 933 | + const existsInList = conversations.value.some(c => (c.conversationId || c.id) === data.conversationId) | ||
| 934 | + if (!existsInList) { | ||
| 935 | + conversations.value.unshift({ conversationId: data.conversationId, title: st.messages.find(m => m.role === 'user')?.content?.slice(0, 50) || '新对话', updatedAt: new Date().toISOString(), messageCount: 1, status: 'running' }) | ||
| 936 | + } | ||
| 902 | } | 937 | } |
| 903 | } | 938 | } |
| 904 | break | 939 | break |
| 905 | case 'thinking': | 940 | case 'thinking': |
| 906 | - if (data.message && !data.message.startsWith('🏗️')) { thinkingContent.value += data.message; scrollToBottom() } | ||
| 907 | - else { thinkingText.value = data.message || '思考中...' } | 941 | + if (data.message && !data.message.startsWith('🏗️')) { st.thinkingContent += data.message; if (isCurrent) scrollToBottom() } |
| 942 | + else { st.thinkingText = data.message || '思考中...' } | ||
| 908 | break | 943 | break |
| 909 | case 'agent_output': | 944 | case 'agent_output': |
| 910 | - streaming.value = true; streamContent.value += (data.message || ''); scrollToBottom(); break | 945 | + st.streaming = true; st.streamContent += (data.message || ''); if (isCurrent) scrollToBottom(); break |
| 911 | case 'tool_call': | 946 | case 'tool_call': |
| 912 | - thinkingText.value = data.message || `🔧 ${data.tool}` | ||
| 913 | - acpLog.value.push({ type: 'tool', icon: '🔧', text: `${data.tool}`, ts: Date.now() }); scrollAcpLog(); break | 947 | + st.thinkingText = data.message || `🔧 ${data.tool}` |
| 948 | + st.acpLog.push({ type: 'tool', icon: '🔧', text: `${data.tool}`, ts: Date.now() }); if (isCurrent) scrollAcpLog(); break | ||
| 914 | case 'tool_input': | 949 | case 'tool_input': |
| 915 | - acpLog.value.push({ type: 'input', icon: '📥', text: data.input?.slice(0, 300) || '', ts: Date.now() }); scrollAcpLog(); break | 950 | + st.acpLog.push({ type: 'input', icon: '📥', text: data.input?.slice(0, 300) || '', ts: Date.now() }); if (isCurrent) scrollAcpLog(); break |
| 916 | case 'tool_result': | 951 | case 'tool_result': |
| 917 | - acpLog.value.push({ type: 'result', icon: '📤', text: data.result?.slice(0, 300) || '', ts: Date.now() }); scrollAcpLog(); break | 952 | + st.acpLog.push({ type: 'result', icon: '📤', text: data.result?.slice(0, 300) || '', ts: Date.now() }); if (isCurrent) scrollAcpLog(); break |
| 918 | case 'session_cost': | 953 | case 'session_cost': |
| 919 | - acpLog.value.push({ type: 'cost', icon: '💰', text: `$${data.cost?.toFixed(4)} (${((data.duration||0)/1000).toFixed(1)}s)`, ts: Date.now() }); break | 954 | + st.acpLog.push({ type: 'cost', icon: '💰', text: `$${data.cost?.toFixed(4)} (${((data.duration||0)/1000).toFixed(1)}s)`, ts: Date.now() }); break |
| 920 | case 'acp_raw': | 955 | case 'acp_raw': |
| 921 | if (data.method && !data.method.includes('content_block_delta') && !data.method.includes('message_start')) { | 956 | if (data.method && !data.method.includes('content_block_delta') && !data.method.includes('message_start')) { |
| 922 | - acpLog.value.push({ type: 'raw', icon: '⚡', text: data.method, ts: Date.now() }); scrollAcpLog() | 957 | + st.acpLog.push({ type: 'raw', icon: '⚡', text: data.method, ts: Date.now() }); if (isCurrent) scrollAcpLog() |
| 923 | } | 958 | } |
| 924 | break | 959 | break |
| 925 | case 'sub_agent': | 960 | case 'sub_agent': |
| 926 | - if (!subAgents.value[data.callId]) subAgents.value[data.callId] = { agent: data.agent, status: 'running', content: '', collapsed: false } | 961 | + if (!st.subAgents[data.callId]) st.subAgents[data.callId] = { agent: data.agent, status: 'running', content: '', collapsed: false } |
| 927 | if (data.event === 'chunk' && data.chunk) { | 962 | if (data.event === 'chunk' && data.chunk) { |
| 928 | - subAgents.value[data.callId].content += data.chunk | ||
| 929 | - setTimeout(() => { document.querySelectorAll('.sub-log-content').forEach(el => el.scrollTop = el.scrollHeight) }, 50) | ||
| 930 | - } else if (data.event === 'done') subAgents.value[data.callId].status = data.status | ||
| 931 | - else if (data.event === 'start') subAgents.value[data.callId].status = 'running' | ||
| 932 | - scrollToBottom(); break | 963 | + st.subAgents[data.callId].content += data.chunk |
| 964 | + if (isCurrent) setTimeout(() => { document.querySelectorAll('.sub-log-content').forEach(el => el.scrollTop = el.scrollHeight) }, 50) | ||
| 965 | + } else if (data.event === 'done') st.subAgents[data.callId].status = data.status | ||
| 966 | + else if (data.event === 'start') st.subAgents[data.callId].status = 'running' | ||
| 967 | + if (isCurrent) scrollToBottom(); break | ||
| 933 | case 'error': | 968 | case 'error': |
| 934 | - messages.value.push({ role: 'assistant', content: `❌ ${data.message}`, timestamp: new Date().toISOString() }) | ||
| 935 | - acpLog.value.push({ type: 'error', icon: '🚨', text: data.message, ts: Date.now() }) | ||
| 936 | - thinkingText.value = ''; thinkingContent.value = '' | ||
| 937 | - scrollToBottom(); scrollAcpLog(); break | 969 | + st.messages.push({ role: 'assistant', content: `❌ ${data.message}`, timestamp: new Date().toISOString() }) |
| 970 | + st.acpLog.push({ type: 'error', icon: '🚨', text: data.message, ts: Date.now() }) | ||
| 971 | + st.thinkingText = ''; st.thinkingContent = '' | ||
| 972 | + if (isCurrent) { scrollToBottom(); scrollAcpLog() } | ||
| 973 | + break | ||
| 938 | } | 974 | } |
| 939 | } | 975 | } |
| 940 | 976 | ||
| @@ -957,7 +993,7 @@ function renderMarkdown(text) { | @@ -957,7 +993,7 @@ function renderMarkdown(text) { | ||
| 957 | catch { return (text || '').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>') } | 993 | catch { return (text || '').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>') } |
| 958 | } | 994 | } |
| 959 | 995 | ||
| 960 | -watch([messages, streamContent], () => scrollToBottom(), { deep: true }) | 996 | +watch(chatState, () => scrollToBottom(), { deep: true }) |
| 961 | // Load file tree when switching to detail tab | 997 | // Load file tree when switching to detail tab |
| 962 | watch(activeTab, async (tab) => { if (tab === 'detail' && !fileTree.value) await loadFileTree() }) | 998 | watch(activeTab, async (tab) => { if (tab === 'detail' && !fileTree.value) await loadFileTree() }) |
| 963 | 999 | ||
| @@ -975,7 +1011,7 @@ onMounted(async () => { | @@ -975,7 +1011,7 @@ onMounted(async () => { | ||
| 975 | await loadAgentInfo() | 1011 | await loadAgentInfo() |
| 976 | await loadConversations() | 1012 | await loadConversations() |
| 977 | if (route.params.convId) { | 1013 | if (route.params.convId) { |
| 978 | - activeConvId.value = route.params.convId | 1014 | + chatState.value.activeConvId = route.params.convId |
| 979 | await loadConversation(route.params.convId) | 1015 | await loadConversation(route.params.convId) |
| 980 | scrollToBottom() | 1016 | scrollToBottom() |
| 981 | } else if (filteredConversations.value.length > 0) { | 1017 | } else if (filteredConversations.value.length > 0) { |
-
mentioned in commit 4d23f5068d5e29461c135f670a03f97d92564d26