From e67d638e06f928f5b3938076c80e3b5e0052f56f Mon Sep 17 00:00:00 2001 From: thzxx Date: Sun, 28 Jun 2026 23:16:05 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=89=88=E6=9C=AC=E5=8F=B7=200.14.6?= =?UTF-8?q?=20=E2=86=92=200.14.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: onNewIteration 只在 THINKING 状态调用,消除每状态切换的冗余触发 fix: renderMessages 全量清空重建,去掉增量 diff 的边界 bug fix: 空消息跳过增加 think 判断,纯思考卡片不再被吞掉 fix: updateMessageToolRecord 改为空函数,工具卡片统一由 onDone 挂载 --- README.md | 6 +- package-lock.json | 4 +- package.json | 2 +- src/main/menu.ts | 2 +- src/renderer/components/chat-area.ts | 38 +++--- src/renderer/components/input-area.ts | 159 +++++++------------------- src/renderer/index.html | 2 +- src/renderer/services/agent-engine.ts | 4 +- 8 files changed, 68 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 1909a05..edd5129 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

- version + version electron typescript license @@ -253,7 +253,7 @@ npm start ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ``` -产出:`release/Metona Ollama Setup v0.14.6.exe` +产出:`release/Metona Ollama Setup v0.14.7.exe` ## 🛠️ 常用命令 @@ -501,7 +501,7 @@ npm start ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist ``` -Output: `release/Metona Ollama Setup v0.14.6.exe` +Output: `release/Metona Ollama Setup v0.14.7.exe` ## 🛠️ Common Commands diff --git a/package-lock.json b/package-lock.json index 81ffdb9..6e41b34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metona-ollama-desktop", - "version": "0.14.6", + "version": "0.14.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metona-ollama-desktop", - "version": "0.14.6", + "version": "0.14.7", "license": "MIT", "dependencies": { "ffmpeg-static": "^5.2.0", diff --git a/package.json b/package.json index 2a1ec92..0e73324 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metona-ollama-desktop", - "version": "0.14.6", + "version": "0.14.7", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "main": "dist/main/main.js", "author": "thzxx", diff --git a/src/main/menu.ts b/src/main/menu.ts index f282380..1f46e06 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -101,7 +101,7 @@ export function createMenu(): void { dialog.showMessageBox(mainWindow!, { type: 'info', title: '关于 Metona Ollama', - message: 'Metona Ollama Desktop v0.14.6', + message: 'Metona Ollama Desktop v0.14.7', detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama', icon: getIconPath() }); diff --git a/src/renderer/components/chat-area.ts b/src/renderer/components/chat-area.ts index 424a252..c73938a 100644 --- a/src/renderer/components/chat-area.ts +++ b/src/renderer/components/chat-area.ts @@ -115,37 +115,29 @@ export function renderMessages(): void { const currentSession = state.get(KEYS.CURRENT_SESSION); const msgs = currentSession ? currentSession.messages : []; + // 全量清空重建,避免增量 diff 的边界 bug + messagesContainerEl.innerHTML = ''; + currentPlaceholder = null; + _renderedMsgIndices.clear(); + if (msgs.length === 0) { emptyStateEl.style.display = ''; messagesContainerEl.style.display = 'none'; - _renderedMsgIndices.clear(); return; } emptyStateEl.style.display = 'none'; messagesContainerEl.style.display = ''; - // P1-1: 基于 Set 的稳定增量 diff - const wasExisting = _renderedMsgIndices.size > 0; - let isNewMessage = false; for (let i = 0; i < msgs.length; i++) { - if (!_renderedMsgIndices.has(i)) { - appendMessageDOM(msgs[i], i); - _renderedMsgIndices.add(i); - isNewMessage = true; - } + const msg = msgs[i]; + // 只跳过既无内容也无 think 也无工具的空消息 + if (msg.role === 'assistant' && !msg.content && !msg.think && (!msg.toolCalls || msg.toolCalls.length === 0)) continue; + appendMessageDOM(msgs[i], i); + _renderedMsgIndices.add(i); } - // 清理超出范围的索引(消息被 undo/retry 移除时) - for (const idx of _renderedMsgIndices) { - if (idx >= msgs.length) _renderedMsgIndices.delete(idx); - } - - if (isNewMessage) { - scrollToBottom(); - } else if (msgs.length > 0 && !wasExisting) { - chatAreaEl.scrollTop = chatAreaEl.scrollHeight; - } + scrollToBottom(); } export function appendMessageDOM(msg: ChatMessage, index: number): void { @@ -520,6 +512,14 @@ export function resetCurrentPlaceholder(): void { } } +/** 从 DOM 移除当前 placeholder 并清空引用 */ +export function removeCurrentPlaceholder(): void { + if (currentPlaceholder) { + currentPlaceholder.remove(); + currentPlaceholder = null; + } +} + export function enableAutoScroll(): void { autoScroll = true; if (scrollBtnEl) scrollBtnEl.style.display = 'none'; diff --git a/src/renderer/components/input-area.ts b/src/renderer/components/input-area.ts index 5ed941a..b265b2a 100644 --- a/src/renderer/components/input-area.ts +++ b/src/renderer/components/input-area.ts @@ -9,7 +9,7 @@ import { getSelectedModel, isThinkEnabled, isVisionAvailable, isToolCallingSuppo import { renderMessages, clearMessagesDOM, appendAssistantPlaceholder, appendSystemMessage, updateLastAssistantMessage, clearMessages, safeMarkdown, enableAutoScroll, resetAutoScroll, - appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder, resetCurrentPlaceholder + appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder, resetCurrentPlaceholder, removeCurrentPlaceholder } from './chat-area.js'; import { showToast } from './toast.js'; import { addToolCard, startToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExternal, getWorkspaceDirPath, hasActiveCards, showWorkingHint, clearWorkingHint } from './workspace-panel.js'; @@ -529,17 +529,14 @@ async function handleRetry(): Promise { }); }, onNewIteration: (toolCalls) => { - const hasToolCalls = toolCalls && toolCalls.length > 0; - if (retryContent || hasToolCalls) { + removeCurrentPlaceholder(); + const hasContent = !!retryContent?.trim(); + if (hasContent || retryIterations === 0) { const now = Date.now(); - const toolCallRecords: ToolCallRecord[] | undefined = toolCalls?.map(tc => ({ - name: tc.function.name, arguments: tc.function.arguments, - result: null, status: 'running' as const, timestamp: now - })); const prevMsg: ChatMessage = { - role: 'assistant', content: retryContent || '', timestamp: now, + role: 'assistant', content: retryContent || '', model: getSelectedModel(), + timestamp: now, ...(retryThinkContent && { think: retryThinkContent }), - ...(toolCallRecords?.length && { toolCalls: toolCallRecords }), }; state.update(KEYS.CURRENT_SESSION, (s: any) => ({ ...s, messages: [...s.messages, prevMsg], updatedAt: Date.now() @@ -548,7 +545,6 @@ async function handleRetry(): Promise { } appendAssistantPlaceholder(); retryContent = ''; - retryThinkContent = ''; retryIterations++; }, onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage(retryContent, thinking, null, getSelectedModel()); }, @@ -1090,27 +1086,7 @@ export async function sendMessage(): Promise { /** 更新当前会话消息中最近一个 'running' 状态的同名工具记录 */ function updateMessageToolRecord(toolName: string, status: 'success' | 'error', result: Record): void { - let updated = false; - state.update(KEYS.CURRENT_SESSION, (session: any) => { - if (!session) return session; - const messages = [...session.messages]; - // 从后往前找最近一条包含该工具 running 状态记录的消息 - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (!msg.toolCalls) continue; - const idx = msg.toolCalls.findIndex((tc: any) => tc.name === toolName && tc.status === 'running'); - if (idx !== -1) { - const updatedRecords = [...msg.toolCalls]; - updatedRecords[idx] = { ...updatedRecords[idx], status, result }; - messages[i] = { ...msg, toolCalls: updatedRecords }; - updated = true; - return { ...session, messages, updatedAt: Date.now() }; - } - } - return session; - }); - // 状态更新后重新渲染消息,使工具卡片状态即时生效 - if (updated) renderMessages(); + // 工具卡片统一由 onDone 挂载,不在执行中更新消息记录 } async function sendMessageWithAgentLoop(text: string, currentSession: ChatSession, model: string): Promise { @@ -1255,10 +1231,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio let assistantContent = ''; let thinkContent = ''; - let agentModeIterations = 0; // 跟踪 onNewIteration 调用次数 - state.set('_currentEvalCount', 0); // 重置累计 token 计数,防止跨会话残留 - let lastIterationEvalCount = 0; // 上一轮迭代结束时的累计 token 数 - let lastIterationStartTime = Date.now(); // 当前迭代开始时间 + state.set('_currentEvalCount', 0); // ── 监控定时器:流式输出期间持续显示工作提示 ── // 模型生成大参数 tool_call 可能需要几分钟,期间工作空间显示提示 @@ -1278,52 +1251,25 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio }); }, onNewIteration: (toolCalls) => { - // Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder - // 先移除旧 placeholder(防止被 renderMessages 当作已渲染消息计数) - const oldPlaceholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement; - if (oldPlaceholder) oldPlaceholder.remove(); + // 保存上一轮的卡片(不含工具记录,工具统一在 onDone 挂载) + const prevMsg: ChatMessage = { + role: 'assistant', + content: assistantContent || '', + model: getSelectedModel(), + timestamp: Date.now(), + ...(thinkContent && { think: thinkContent }), + }; + state.update(KEYS.CURRENT_SESSION, (session: any) => ({ + ...session, + messages: [...session.messages, prevMsg], + updatedAt: Date.now() + })); + renderMessages(); - // 有文本内容 或 有工具调用 → 保存为独立消息 - const hasToolCalls = toolCalls && toolCalls.length > 0; - if (assistantContent || hasToolCalls) { - const now = Date.now(); - // 计算本轮迭代的 token 和时长 - const currentEvalCount = state.get('_currentEvalCount', 0); - const iterationEvalCount = Math.max(0, currentEvalCount - lastIterationEvalCount); - const iterationDuration = (now - lastIterationStartTime) * 1e6; - - const toolCallRecords: ToolCallRecord[] | undefined = toolCalls?.map(tc => ({ - name: tc.function.name, - arguments: tc.function.arguments, - result: null, - status: 'running' as const, - timestamp: now - })); - - const prevMsg: ChatMessage = { - role: 'assistant', - content: assistantContent || '', - timestamp: now, - ...(thinkContent && { think: thinkContent }), - ...(toolCallRecords?.length && { toolCalls: toolCallRecords }), - ...(iterationEvalCount > 0 && { eval_count: iterationEvalCount }), - ...(iterationDuration > 0 && { total_duration: iterationDuration }), - }; - state.update(KEYS.CURRENT_SESSION, (session: any) => ({ - ...session, - messages: [...session.messages, prevMsg], - updatedAt: Date.now() - })); - renderMessages(); - - lastIterationEvalCount = currentEvalCount; - lastIterationStartTime = Date.now(); - } - // 创建新 placeholder,重置内容 + removeCurrentPlaceholder(); appendAssistantPlaceholder(); assistantContent = ''; thinkContent = ''; - agentModeIterations++; }, onThinking: (thinking) => { thinkContent = thinking; @@ -1372,50 +1318,23 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio } }, onDone: async (finalContent, toolRecords, loopStats) => { - assistantContent = finalContent; + const assistantMsg: ChatMessage = { + role: 'assistant', + content: finalContent || '', + model: getSelectedModel(), + timestamp: Date.now(), + ...(thinkContent && { think: thinkContent }), + ...(toolRecords?.length && { toolCalls: toolRecords }), + ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), + ...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }), + ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), + }; + state.update(KEYS.CURRENT_SESSION, (session: any) => ({ + ...session, + messages: [...session.messages, assistantMsg], + updatedAt: Date.now() + })); - // 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里创建新消息保存最终回复 - if (agentModeIterations > 0) { - if (assistantContent) { - const lastIterDuration = (Date.now() - lastIterationStartTime) * 1e6; - const lastMsg: ChatMessage = { - role: 'assistant', - content: assistantContent, - model: getSelectedModel(), - timestamp: Date.now(), - ...(thinkContent && { think: thinkContent }), - ...(toolRecords?.length && { toolCalls: toolRecords }), - ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), - ...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }), - ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), - }; - state.update(KEYS.CURRENT_SESSION, (session: any) => ({ - ...session, - messages: [...session.messages, lastMsg], - updatedAt: Date.now() - })); - } - } else { - // 单迭代模式:正常保存 - const assistantMsg: ChatMessage = { - role: 'assistant', - content: assistantContent || '', - model: getSelectedModel(), - timestamp: Date.now(), - ...(thinkContent && { think: thinkContent }), - ...(toolRecords?.length && { toolCalls: toolRecords }), - ...(loopStats?.eval_count && { eval_count: loopStats.eval_count }), - ...(loopStats?.prompt_eval_count && { prompt_eval_count: loopStats.prompt_eval_count }), - ...(loopStats?.total_duration && { total_duration: loopStats.total_duration }), - }; - state.update(KEYS.CURRENT_SESSION, (session: any) => ({ - ...session, - messages: [...session.messages, assistantMsg], - updatedAt: Date.now() - })); - } - // 清掉流式 placeholder,统一从 session 渲染(单条消息,Markdown 格式) - clearMessagesDOM(); renderMessages(); // ── 保存 Plan Mode 最终进度到会话(供下一轮 Loop 注入)── diff --git a/src/renderer/index.html b/src/renderer/index.html index 6db271e..6f88459 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -28,7 +28,7 @@

Metona Ollama - v0.14.6 + v0.14.7