Files
metona-ollama-desktop/src/renderer/state/state.ts
T
紫影233 bd3a06bfaf 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
2026-07-14 16:26:43 +08:00

149 lines
4.4 KiB
TypeScript

/**
* State - 轻量级响应式状态管理
*/
import type { StateKey, ChatSession } from '../types.js';
import type { ChatDB } from '../db/chat-db.js';
import type { OllamaAPI } from '../api/ollama.js';
function shallowEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== 'object' || typeof b !== 'object') return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a as object);
const keysB = Object.keys(b as object);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key) || (a as Record<string, unknown>)[key] !== (b as Record<string, unknown>)[key]) return false;
}
return true;
}
type Listener = (value: unknown, old: unknown) => void;
class AppState {
private _state: Record<string, unknown> = {};
private _listeners = new Map<string, Set<Listener>>();
private _batching = false;
private _batchQueue: Set<string> | null = null;
set(key: string, value: unknown): void {
const old = this._state[key];
this._state[key] = value;
if (!shallowEqual(old, value)) {
if (this._batching) {
this._batchQueue!.add(key);
} else {
this._notify(key, value, old);
}
}
}
update(key: string, updater: (old: any) => any): void {
const old = this._state[key];
const value = updater(old);
this._state[key] = value;
if (!shallowEqual(old, value)) {
if (this._batching) {
this._batchQueue!.add(key);
} else {
this._notify(key, value, old);
}
}
}
setIn(key: string, path: (string | number)[], value: unknown): void {
if (!path || path.length === 0) {
this.set(key, value);
return;
}
const old = this._state[key];
if (old == null || typeof old !== 'object') {
return;
}
const newRoot = Array.isArray(old) ? [...(old as unknown[])] : { ...(old as Record<string, unknown>) };
let current: Record<string, unknown> = newRoot as Record<string, unknown>;
for (let i = 0; i < path.length - 1; i++) {
const segment = path[i];
const next = current[segment as string];
if (next == null || typeof next !== 'object') {
const isIndex = typeof path[i + 1] === 'number';
current[segment as string] = isIndex ? [] : {};
} else {
current[segment as string] = Array.isArray(next) ? [...next] : { ...(next as Record<string, unknown>) };
}
current = current[segment as string] as Record<string, unknown>;
}
current[path[path.length - 1] as string] = value;
this._state[key] = newRoot;
if (!shallowEqual(old, newRoot)) {
if (this._batching) {
this._batchQueue!.add(key);
} else {
this._notify(key, newRoot, old);
}
}
}
batch(fn: () => void): void {
this._batching = true;
this._batchQueue = new Set();
try {
fn();
} finally {
const keys = this._batchQueue;
this._batching = false;
this._batchQueue = null;
for (const k of keys) {
this._notify(k, this._state[k], undefined);
}
}
}
private _notify(key: string, value: unknown, old: unknown): void {
if (!this._listeners.has(key)) return;
for (const cb of this._listeners.get(key)!) {
try { cb(value, old); } catch { /* 回调异常不影响其他监听器 */ }
}
}
get<T = unknown>(key: string, defaultValue?: T): T {
return (key in this._state ? this._state[key] : defaultValue) as T;
}
on(key: string, callback: Listener): () => void {
if (!this._listeners.has(key)) {
this._listeners.set(key, new Set());
}
this._listeners.get(key)!.add(callback);
return () => this._listeners.get(key)?.delete(callback);
}
init(initial: Record<string, unknown>): void {
Object.assign(this._state, initial);
}
}
export const state = new AppState();
export const KEYS = {
DB: 'db',
API: 'api',
CURRENT_SESSION: 'currentSession',
IS_STREAMING: 'isStreaming',
IS_HISTORY_VIEW: 'isHistoryView',
PENDING_IMAGES: 'pendingImages',
ABORT_CONTROLLER: 'abortController',
NUM_CTX: 'numCtx',
HISTORY_PAGE: 'historyPage',
HISTORY_SEARCH: 'historySearchQuery',
} as const;