v0.16.7: 引擎修复 + 工具面板统一 + AGENT.md 改为仅工作空间加载

核心引擎修复:
- 状态转换表补全 THINKING/PARSING/EXECUTING -> COMPRESSING,修复紧急压缩成为死代码的 P1 问题
- 新增跨轮次死循环检测器(软性提示 + 硬性熔断),防止模型陷入重复工具调用死循环
- handleCompressing 空响应回到 THINKING 而非 REFLECTING,避免错误终止
- executeHooks 添加 .catch() 防止未处理的 Promise 拒绝
- ALWAYS_PARALLEL 移除 git 和 browser_evaluate(有副作用的工具不应并行)
- thinking fallback:content 为空但有 thinking 时,用 [推理过程] 作为 content 保留上下文
- 8个写类工具添加专用格式化器(含 success + message 字段)
- 清理死代码:3个未使用函数 + 3个未使用 import

工具面板统一:
- 10个工具独立下拉框统一为1个全局执行模式选择器
- FIFO 队列防止并行 showToolConfirm 导致静默取消
- delete_file 支持 paths 数组参数批量删除

工具定义与实现一致性修复:
- run_command 移除未使用的 timeout 参数,描述改为"超时可配置"
- list_directory 添加 2000 条截断逻辑 + filter_extension 参数
- calculator 正则移除 ^ 字符(parser 用 ** 替代)
- search_files/tree/web_search/fetch_top 描述与实现对齐

消息传递修复:
- trimByTokenLimit 改为原子组选择(assistant+tool_calls 与后续 tool 消息作为一组)
- 历史工具结果复用 formatToolResultForModel,与当前格式一致

AGENT.md 加载策略变更:
- 删除内置 AGENT.md 文件
- 仅从工作空间加载:有则注入,无则跳过

其他修复:
- 修复初始化失败 "Cannot convert undefined or null to object"(saveSetting null 导致 JSON.parse 陷阱)
- 修复工作空间命令行标签页 idle 状态残留导致样式错乱

版本号: 0.16.5 -> 0.16.7
This commit is contained in:
紫影233
2026-07-14 16:26:43 +08:00
parent 4326fcce42
commit bd3a06bfaf
31 changed files with 909 additions and 986 deletions
+2 -2
View File
@@ -5,7 +5,7 @@
import { state, KEYS } from '../state/state.js';
import { formatSize } from '../utils/utils.js';
import { OllamaAPI } from '../api/ollama.js';
import { logConnection, logError, logDebug } from '../services/log-service.js';
import { logConnection, logError, logDebug, logInfo } from '../services/log-service.js';
let connStatusEl: HTMLElement;
@@ -54,7 +54,7 @@ export async function updateConnectionInfo(): Promise<void> {
badge.className = 'status-badge success';
verEl.textContent = `v${version.version}`;
corsHint.style.display = 'none';
logConnection('info', `Ollama v${version.version}`);
logInfo(`Ollama 服务器版本: v${version.version}`);
} catch (err) {
badge.textContent = '✗ 未连接';
badge.className = 'status-badge error';
+13 -8
View File
@@ -15,9 +15,10 @@ import { showToast } from './toast.js';
import { addToolCard, startToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExternal, getWorkspaceDirPath, hasActiveCards, showWorkingHint, clearWorkingHint } from './workspace-panel.js';
import { ChatDB } from '../db/chat-db.js';
import { OllamaAPI } from '../api/ollama.js';
import { runAgentLoop } from '../services/agent-engine.js';
import { runAgentLoop, formatToolResultForModel } from '../services/agent-engine.js';
import { estimateTokens } from '../services/context-manager.js';
import { showToolConfirm } from './tool-confirm-modal.js';
import { showConfirm } from './prompt-modal.js';
import { logInfo, logStream, logError, logSuccess, logWarn, resetVideoProgress, updateVideoProgress } from '../services/log-service.js';
import type { ChatSession, ChatMessage, OllamaStreamChunk, OllamaMessage, FileContent, ChatFile, ToolCallRecord, AgentMode } from '../types.js';
@@ -95,9 +96,9 @@ export function initInputArea(): void {
// Ctrl+K: 清空聊天(需要确认)
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
if (confirm('确定要清空当前对话吗?')) {
clearMessages();
}
showConfirm('确定要清空当前对话吗?', '清空对话').then(ok => {
if (ok) clearMessages();
});
}
// Escape: 停止生成(当正在流式输出时)
if (e.key === 'Escape' && state.get<boolean>(KEYS.IS_STREAMING)) {
@@ -1023,17 +1024,21 @@ function buildHistoryMessages(msgs: ChatMessage[], maxCount = 20): OllamaMessage
result.push(assistantMsg);
// ── 注入 tool 结果消息(role: 'tool')──
// 复用 formatToolResultForModel 保持与当前 Loop 格式一致
if (msg.toolCalls?.length) {
for (const tc of msg.toolCalls) {
if (!tc.result) continue;
if (result.length >= maxCount) break;
const resultContent = tc.status === 'success'
? JSON.stringify(tc.result)
: JSON.stringify({ success: false, error: tc.result.error || '工具执行失败' });
const formattedResult = formatToolResultForModel(tc.name, tc.result);
// R92: 工具结果格式标准化 — 添加统一头信息 + 数据边界标记
const resultDuration = Date.now() - tc.timestamp;
const resultSize = formattedResult.length;
const sizeCategory = resultSize > 10000 ? 'large' : resultSize > 2000 ? 'medium' : 'small';
const r92Header = `[工具:${tc.name} 状态:${tc.status} 耗时:${resultDuration}ms 大小:${sizeCategory}(${resultSize}字符)]`;
result.push({
role: 'tool',
tool_name: tc.name,
content: resultContent,
content: `<<<TOOL_RESULT_START name="${tc.name}">>>\n${r92Header}\n${formattedResult}\n<<<TOOL_RESULT_END>>>`,
});
}
}
+45 -44
View File
@@ -285,6 +285,51 @@ export function initSettingsModal(): void {
}
});
}
// ── 子代理相关设置(原顶层事件监听器移入此处,确保 DOM 已就绪)──
// 子代理模型设置保存
document.querySelector('#selectSubAgentModel')?.addEventListener('change', async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return;
const val = (document.querySelector('#selectSubAgentModel') as HTMLSelectElement).value;
await db.saveSetting('subAgentModel', val);
logSetting('子代理默认模型', val || '跟随当前模型');
});
// 子代理最大轮次
const saveSubAgentMaxLoops = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputSubAgentMaxLoops') as HTMLInputElement).value.trim();
const maxLoops = val ? parseInt(val) : 10;
state.set('subAgentMaxLoops', maxLoops);
if (db) await db.saveSetting('subAgentMaxLoops', maxLoops);
logSetting('子代理最大轮次', `${maxLoops}`);
}, 500);
document.querySelector('#inputSubAgentMaxLoops')!.addEventListener('input', saveSubAgentMaxLoops);
// 子代理超时(秒)
const saveSubAgentTimeout = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputSubAgentTimeout') as HTMLInputElement).value.trim();
const sec = val ? parseInt(val) : -1; // -1=默认
const ms = sec >= 0 ? sec * 1000 : 300_000;
state.set('subAgentTimeout', ms);
if (db) await db.saveSetting('subAgentTimeout', sec);
logSetting('子代理超时', sec >= 0 ? (sec === 0 ? '已禁用' : `${sec}s`) : '默认(300s)');
}, 500);
document.querySelector('#inputSubAgentTimeout')!.addEventListener('input', saveSubAgentTimeout);
// ── 看门狗超时(分钟,0=禁用)──
const saveLoopWatchdog = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputLoopWatchdog') as HTMLInputElement).value.trim();
const minutes = val ? parseInt(val) : -1;
const ms = minutes > 0 ? minutes * 60_000 : (minutes === 0 ? 0 : 1_800_000); // 默认30分钟
state.set('loopWatchdogMs', ms);
if (db) await db.saveSetting('loopWatchdogMs', ms);
logSetting('看门狗超时', minutes >= 0 ? (minutes === 0 ? '已禁用' : `${minutes}分钟`) : '默认(30分钟)');
}, 500);
document.querySelector('#inputLoopWatchdog')!.addEventListener('input', saveLoopWatchdog);
}
export function openSettingsModal(): void {
@@ -429,50 +474,6 @@ async function populateSubAgentModels(): Promise<void> {
}
}
// 子代理模型设置保存
document.querySelector('#selectSubAgentModel')?.addEventListener('change', async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return;
const val = (document.querySelector('#selectSubAgentModel') as HTMLSelectElement).value;
await db.saveSetting('subAgentModel', val);
logSetting('子代理默认模型', val || '跟随当前模型');
});
// 子代理最大轮次
const saveSubAgentMaxLoops = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputSubAgentMaxLoops') as HTMLInputElement).value.trim();
const maxLoops = val ? parseInt(val) : 10;
state.set('subAgentMaxLoops', maxLoops);
if (db) await db.saveSetting('subAgentMaxLoops', maxLoops);
logSetting('子代理最大轮次', `${maxLoops}`);
}, 500);
document.querySelector('#inputSubAgentMaxLoops')!.addEventListener('input', saveSubAgentMaxLoops);
// 子代理超时(秒)
const saveSubAgentTimeout = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputSubAgentTimeout') as HTMLInputElement).value.trim();
const sec = val ? parseInt(val) : -1; // -1=默认
const ms = sec >= 0 ? sec * 1000 : 300_000;
state.set('subAgentTimeout', ms);
if (db) await db.saveSetting('subAgentTimeout', sec);
logSetting('子代理超时', sec >= 0 ? (sec === 0 ? '已禁用' : `${sec}s`) : '默认(300s)');
}, 500);
document.querySelector('#inputSubAgentTimeout')!.addEventListener('input', saveSubAgentTimeout);
// ── 看门狗超时(分钟,0=禁用)──
const saveLoopWatchdog = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputLoopWatchdog') as HTMLInputElement).value.trim();
const minutes = val ? parseInt(val) : -1;
const ms = minutes > 0 ? minutes * 60_000 : (minutes === 0 ? 0 : 1_800_000); // 默认30分钟
state.set('loopWatchdogMs', ms);
if (db) await db.saveSetting('loopWatchdogMs', ms);
logSetting('看门狗超时', minutes >= 0 ? (minutes === 0 ? '已禁用' : `${minutes}分钟`) : '默认(30分钟)');
}, 500);
document.querySelector('#inputLoopWatchdog')!.addEventListener('input', saveLoopWatchdog);
async function importSessions(filePath: string): Promise<void> {
const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return;
+2 -13
View File
@@ -4,7 +4,7 @@
*/
import { state, KEYS } from '../state/state.js';
import { formatTime } from '../utils/utils.js';
import { formatTime, escapeHtml } from '../utils/utils.js';
import { logError } from '../services/log-service.js';
import { getEffectiveNumCtx, getModelContextLength } from './model-bar.js';
import { estimateTokens } from '../services/context-manager.js';
@@ -39,17 +39,6 @@ interface AllSessionsTokenStats {
totals: GlobalTokenTotals;
}
/** P1 #5: HTML 转义,防止 XSS */
function escapeHtml(s: unknown): string {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/** 打开仪表盘 */
export function openTokenDashboard(): void {
if (!dashboardModalEl) return;
@@ -198,7 +187,7 @@ async function fetchGlobalStats(): Promise<AllSessionsTokenStats | null> {
return result;
}
} catch (err) {
logError('Token Dashboard: 获取全局统计失败', String(err));
logError('Token Dashboard: 获取全局统计失败', (err as Error).message);
}
return null;
}
+105 -79
View File
@@ -1,5 +1,8 @@
/**
* ToolConfirmModal - 工具调用确认对话框
*
* 队列模式:当多个工具并行执行需要确认时,按 FIFO 队列逐个确认,
* 避免单例竞态导致前一个确认被自动取消。
*/
import type { ToolCall } from '../types.js';
@@ -8,9 +11,18 @@ import { logDebug, logInfo } from '../services/log-service.js';
import { escapeHtml } from '../utils/utils.js';
let modalEl: HTMLElement | null = null;
let resolveConfirm: ((confirmed: boolean) => void) | null = null;
let confirmBtn: HTMLButtonElement | null = null;
let cancelBtn: HTMLButtonElement | null = null;
let initialized = false;
/** 确认请求队列 */
interface ConfirmRequest {
call: ToolCall;
resolve: (confirmed: boolean) => void;
}
const confirmQueue: ConfirmRequest[] = [];
let isShowing = false;
export function initToolConfirmModal(): void {
modalEl = document.querySelector('#toolConfirmModal')!;
if (!modalEl || initialized) return;
@@ -18,7 +30,7 @@ export function initToolConfirmModal(): void {
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) {
cancelConfirm();
cancelCurrent();
}
});
}
@@ -31,91 +43,105 @@ export function showToolConfirm(call: ToolCall): Promise<boolean> {
if (!initialized) {
initialized = true;
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) cancelConfirm();
if (e.target === modalEl) cancelCurrent();
});
}
}
// 如果有未完成的确认,先取消
if (resolveConfirm) {
const oldResolve = resolveConfirm;
resolveConfirm = null;
oldResolve(false);
// 加入队列,如果当前没有显示的确认框,则立即显示
confirmQueue.push({ call, resolve });
if (!isShowing) {
showNext();
}
resolveConfirm = resolve;
const icon = getToolIcon(call.function.name);
const name = formatToolName(call.function.name);
logInfo(`等待确认: ${call.function.name}`, JSON.stringify(call.function.arguments));
const titleEl = modalEl.querySelector('#toolConfirmTitle')!;
const bodyEl = modalEl.querySelector('#toolConfirmBody')!;
const confirmBtn = modalEl.querySelector('#toolConfirmBtn')! as HTMLButtonElement;
const cancelBtn = modalEl.querySelector('#toolCancelBtn')! as HTMLButtonElement;
titleEl.textContent = `${icon} 确认操作:${name}`;
let bodyHtml = `<div class="tool-confirm-info">`;
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">操作:</span><span>${name}</span></div>`;
const args = call.function.arguments || {};
if (call.function.name === 'read_file' || call.function.name === 'write_file' ||
call.function.name === 'list_directory' || call.function.name === 'search_files' ||
call.function.name === 'create_directory' || call.function.name === 'delete_file') {
if (args.path) {
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">路径:</span><code>${escapeHtml(String(args.path))}</code></div>`;
}
}
if (call.function.name === 'run_command') {
if (args.command) {
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">命令:</span><code>${escapeHtml(String(args.command))}</code></div>`;
}
}
if (call.function.name === 'write_file' && args.content) {
const preview = String(args.content).slice(0, 500);
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">内容预览:</span></div>`;
bodyHtml += `<pre class="tool-confirm-preview">${escapeHtml(preview)}${String(args.content).length > 500 ? '\n...' : ''}</pre>`;
}
bodyHtml += `</div>`;
bodyEl.innerHTML = bodyHtml;
modalEl.style.display = '';
const onConfirm = (e: Event) => {
e.stopPropagation();
const r = resolveConfirm;
cleanup();
logInfo(`已确认: ${call.function.name}`);
r?.(true);
};
const onCancel = (e: Event) => {
e.stopPropagation();
const r = resolveConfirm;
cleanup();
logInfo(`已取消: ${call.function.name}`);
r?.(false);
};
const cleanup = () => {
confirmBtn.removeEventListener('click', onConfirm);
cancelBtn.removeEventListener('click', onCancel);
if (modalEl) modalEl.style.display = 'none';
resolveConfirm = null;
};
confirmBtn.addEventListener('click', onConfirm);
cancelBtn.addEventListener('click', onCancel);
});
}
function cancelConfirm(): void {
if (resolveConfirm) {
const r = resolveConfirm;
resolveConfirm = null;
if (modalEl) modalEl.style.display = 'none';
r(false);
/** 显示队列中的下一个确认 */
function showNext(): void {
if (confirmQueue.length === 0) {
isShowing = false;
return;
}
isShowing = true;
const { call, resolve } = confirmQueue.shift()!;
const icon = getToolIcon(call.function.name);
const name = formatToolName(call.function.name);
logInfo(`等待确认: ${call.function.name}`, JSON.stringify(call.function.arguments));
const titleEl = modalEl!.querySelector('#toolConfirmTitle')!;
const bodyEl = modalEl!.querySelector('#toolConfirmBody')!;
confirmBtn = modalEl!.querySelector('#toolConfirmBtn') as HTMLButtonElement;
cancelBtn = modalEl!.querySelector('#toolCancelBtn') as HTMLButtonElement;
titleEl.textContent = `${icon} 确认操作:${name}`;
let bodyHtml = `<div class="tool-confirm-info">`;
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">操作:</span><span>${name}</span></div>`;
const args = call.function.arguments || {};
if (call.function.name === 'read_file' || call.function.name === 'write_file' ||
call.function.name === 'list_directory' || call.function.name === 'search_files' ||
call.function.name === 'create_directory' || call.function.name === 'delete_file') {
// 支持 paths 数组(批量删除)和单个 path
if (args.paths && Array.isArray(args.paths) && args.paths.length > 0) {
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">路径(${args.paths.length} 项):</span></div>`;
bodyHtml += `<pre class="tool-confirm-preview">${escapeHtml(args.paths.map((p: string) => String(p)).join('\n'))}</pre>`;
} else if (args.path) {
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">路径:</span><code>${escapeHtml(String(args.path))}</code></div>`;
}
}
if (call.function.name === 'run_command') {
if (args.command) {
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">命令:</span><code>${escapeHtml(String(args.command))}</code></div>`;
}
}
if (call.function.name === 'write_file' && args.content) {
const preview = String(args.content).slice(0, 500);
bodyHtml += `<div class="tool-confirm-row"><span class="tool-confirm-label">内容预览:</span></div>`;
bodyHtml += `<pre class="tool-confirm-preview">${escapeHtml(preview)}${String(args.content).length > 500 ? '\n...' : ''}</pre>`;
}
bodyHtml += `</div>`;
bodyEl.innerHTML = bodyHtml;
// 队列计数提示
if (confirmQueue.length > 0) {
bodyEl.querySelector('.tool-confirm-info')!.insertAdjacentHTML('beforeend',
`<div class="tool-confirm-row" style="margin-top:8px;color:var(--text-muted);font-size:12px;">队列中还有 ${confirmQueue.length} 个待确认</div>`);
}
modalEl!.style.display = '';
const onConfirm = (e: Event) => {
e.stopPropagation();
cleanup();
logInfo(`已确认: ${call.function.name}`);
resolve(true);
showNext();
};
const onCancel = (e: Event) => {
e.stopPropagation();
cleanup();
logInfo(`已取消: ${call.function.name}`);
resolve(false);
showNext();
};
const cleanup = () => {
confirmBtn?.removeEventListener('click', onConfirm);
cancelBtn?.removeEventListener('click', onCancel);
if (modalEl) modalEl.style.display = 'none';
};
confirmBtn.addEventListener('click', onConfirm);
cancelBtn.addEventListener('click', onCancel);
}
/** 取消当前显示的确认(不影响队列中的后续确认) */
function cancelCurrent(): void {
if (!isShowing) return;
// 当前显示的确认已从队列 shift 出来,直接触发取消按钮
if (cancelBtn) {
cancelBtn.click();
}
}
+47 -45
View File
@@ -1,35 +1,43 @@
/**
* ToolsModal - 工具面板模态框
* 展示所有可用工具的详细信息。Tool Calling 始终开启,不可关闭。
* 支持三档开关(auto/confirm/disabled)的工具统一管理
* 全局执行模式统一管理所有工具的确认行为
*/
import { state, KEYS } from '../state/state.js';
import { setToolMode, getToolMode, type ToolMode } from '../services/tool-registry.js';
import { setToolMode, type ToolMode } from '../services/tool-registry.js';
import { showToast } from './toast.js';
import { logInfo } from '../services/log-service.js';
import type { ChatDB } from '../db/chat-db.js';
let modalEl: HTMLElement;
const MODE_NAMES: Record<ToolMode, string> = {
const MODE_NAMES: Record<string, string> = {
auto: '自动执行',
confirm: '需确认',
disabled: '已禁用',
};
const BADGE_MAP: Record<ToolMode, { text: string; cls: string }> = {
auto: { text: '自动', cls: 'auto' },
confirm: { text: '需确认', cls: 'confirm' },
disabled: { text: '禁用', cls: 'disabled' },
};
// 需要全局模式管理的工具列表(与 tool-registry.ts MODE_TOOLS 一致)
const MANAGED_TOOLS = [
'run_command',
'write_file', 'create_directory', 'delete_file',
'edit_file', 'replace_in_files', 'move_file', 'copy_file',
'download_file', 'compress',
];
function updateToolBadge(toolName: string, mode: ToolMode): void {
const badge = modalEl.querySelector(`#badge_${toolName}`) as HTMLElement;
if (!badge) return;
const info = BADGE_MAP[mode];
badge.textContent = info.text;
badge.className = `tool-card-badge ${info.cls}`;
function updateAllBadges(mode: string): void {
const badgeText = mode === 'auto' ? '自动' : '需确认';
const badgeCls = mode === 'auto' ? 'auto' : 'confirm';
// 更新所有被管理工具的 badge
const badges = modalEl.querySelectorAll<HTMLElement>('.tool-card-badge');
badges.forEach(badge => {
// 只更新被管理工具的 badge(通过 tool-card-name 文本匹配)
const nameEl = badge.closest('.tool-card')?.querySelector('.tool-card-name');
if (nameEl && MANAGED_TOOLS.includes(nameEl.textContent || '')) {
badge.textContent = badgeText;
badge.className = `tool-card-badge ${badgeCls}`;
}
});
}
function updatePlanBadge(autoConfirm: boolean): void {
@@ -47,36 +55,33 @@ function updatePlanBadge(autoConfirm: boolean): void {
export function initToolsModal(): void {
modalEl = document.querySelector('#toolsModal')!;
// Header 按钮
document.querySelector('#btnTools')!.addEventListener('click', openToolsModal);
document.querySelector('#btnCloseTools')!.addEventListener('click', closeToolsModal);
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl) closeToolsModal();
});
// 所有带 .tool-mode-select 的下拉框统一绑定 change 事件
const selects = modalEl.querySelectorAll<HTMLSelectElement>('.tool-mode-select');
selects.forEach(selectEl => {
selectEl.addEventListener('change', async () => {
const toolName = selectEl.dataset.tool;
if (!toolName) return;
const mode = selectEl.value as ToolMode;
// 全局工具执行模式
const globalSelect = modalEl.querySelector<HTMLSelectElement>('#select_global_tool_mode');
if (globalSelect) {
globalSelect.addEventListener('change', async () => {
const mode = globalSelect.value as ToolMode;
const db = state.get<ChatDB | null>(KEYS.DB);
// 更新运行时模式
setToolMode(toolName, mode);
updateToolBadge(toolName, mode);
// 更新所有被管理工具的运行时模式
for (const toolName of MANAGED_TOOLS) {
setToolMode(toolName, mode);
}
// 持久化到数据库(所有工具模式合并存储为 JSON)
const allModes = state.get<Record<string, ToolMode>>('toolModes', {});
allModes[toolName] = mode;
state.set('toolModes', allModes);
if (db) await db.saveSetting('toolModes', allModes);
// 持久化全局模式
state.set('globalToolMode', mode);
if (db) await db.saveSetting('globalToolMode', mode);
showToast(`${toolName}${MODE_NAMES[mode]}`, 'info');
logInfo(`工具模式切换: ${toolName}${mode}`);
updateAllBadges(mode);
showToast(`工具执行模式:${MODE_NAMES[mode]}`, 'info');
logInfo(`全局工具模式切换: → ${mode}`);
});
});
}
// Plan 确认模式下拉框
const planSelect = modalEl.querySelector<HTMLSelectElement>('#select_plan_confirm');
@@ -96,18 +101,15 @@ export function initToolsModal(): void {
}
export function openToolsModal(): void {
// 从 state 读取所有工具模式,同步 UI
const allModes = state.get<Record<string, ToolMode>>('toolModes', {});
const selects = modalEl.querySelectorAll<HTMLSelectElement>('.tool-mode-select');
selects.forEach(selectEl => {
const toolName = selectEl.dataset.tool;
if (!toolName) return;
const mode = allModes[toolName] || getToolMode(toolName);
selectEl.value = mode;
updateToolBadge(toolName, mode);
});
// 同步全局工具执行模式
const globalMode = state.get<string>('globalToolMode', 'confirm');
const globalSelect = modalEl.querySelector<HTMLSelectElement>('#select_global_tool_mode');
if (globalSelect) {
globalSelect.value = globalMode;
updateAllBadges(globalMode);
}
// 同步 Plan 确认模式下拉框
// 同步 Plan 确认模式
const planSelect = modalEl.querySelector<HTMLSelectElement>('#select_plan_confirm');
if (planSelect) {
const autoConfirm = state.get<boolean>('planAutoConfirm', false);
+16 -1
View File
@@ -555,6 +555,12 @@ function renderTerminal(): void {
_termRenderedCount = 0;
}
// 从 idle 状态切换到有内容时,清空 idle DOM
// idle 状态用 position:absolute 覆盖在内容上方,不清空会同时显示
if (_termRenderedCount === 0 && container.querySelector('.ws-idle-state')) {
container.innerHTML = '';
}
// 增量追加新行
const fragment = document.createDocumentFragment();
for (let i = _termRenderedCount; i < session.lines.length; i++) {
@@ -1068,7 +1074,16 @@ function renderToolCard(tc: ToolCallRecord): string {
} else if (tc.name === 'write_file') {
resultHtml = `<div class="tool-result-entry">✅ 已写入 ${escapeHtml(String(r.path || ''))}</div>`;
} else if (tc.name === 'delete_file') {
resultHtml = `<div class="tool-result-entry">✅ 已删除 ${escapeHtml(String(r.path || ''))}</div>`;
if (r.batch) {
resultHtml = `<div class="tool-result-entry">✅ 批量删除 ${r.successCount}/${r.totalPaths} 个路径</div>`;
if (r.results) {
for (const res of r.results) {
resultHtml += `<div class="tool-result-entry">${res.success ? '✅' : '❌'} ${escapeHtml(String(res.path || ''))}${res.success ? '' : ' — ' + escapeHtml(String(res.error || ''))}</div>`;
}
}
} else {
resultHtml = `<div class="tool-result-entry">✅ 已删除 ${escapeHtml(String(r.path || ''))}</div>`;
}
} else if (tc.name === 'create_directory') {
resultHtml = `<div class="tool-result-entry">✅ 已创建 ${escapeHtml(String(r.path || ''))}</div>`;
} else {