Skip to content

Commit 8cbb966

Browse files
committed
perf: 减少索引器内存占用与持久化存储体积
1. 索引器优化(indexer.ts): - 引入 `stripDetailsForPersist` 方法,在持久化 `history.details` 时剔除高占用的 `messages` 字段,仅保留摘要信息,显著降低内存驻留与磁盘I/O开销。 - 优化 `DETAILS_CACHE_LIMIT` 缓存策略。 2. 资源清理(indexer.ts): - 增强 `stopHistoryIndexer` 清理逻辑,显式清除所有 pending 状态的重试定时器 (`clearTimeout`)。 - 退出时清空 `rescanCooldown` 映射,确保无残留对象引用。 3. 前端性能(App.tsx): - 优化 `buildFilteredText` 依赖项 (添加 `selectedSessionFingerprint` 等),减少无效计算与重渲染。 - 调整 Effect 依赖,确保视图更新更精准。 4. 国际化与文案: - 更新中英文 Locale (`history.json`, `terminal.json`),新增终端启动失败、复制行等文案。
1 parent 07502a8 commit 8cbb966

File tree

8 files changed

+484
-254
lines changed

8 files changed

+484
-254
lines changed

electron/history.ts

Lines changed: 129 additions & 93 deletions
Large diffs are not rendered by default.

electron/indexer.ts

Lines changed: 236 additions & 104 deletions
Large diffs are not rendered by default.

electron/main.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import opentype from 'opentype.js';
1313
import iconv from 'iconv-lite';
1414
import projects, { IMPLEMENTATION_NAME as PROJECTS_IMPL } from "./projects/index";
1515
import history, { purgeHistoryCacheIfOutdated } from "./history";
16-
import { startHistoryIndexer, getIndexedSummaries, getIndexedDetails, getLastIndexerRoots, stopHistoryIndexer } from "./indexer";
16+
import { startHistoryIndexer, getIndexedSummaries, getIndexedDetails, getLastIndexerRoots, stopHistoryIndexer, cacheDetails, getCachedDetails } from "./indexer";
1717
import { getSessionsRootsFastAsync } from "./wsl";
1818
import { perfLogger } from "./log";
1919
import settings, { ensureSettingsAutodetect, ensureFirstRunTerminalSelection, type ThemeSetting as SettingsThemeSetting, type AppSettings } from "./settings";
@@ -1042,11 +1042,20 @@ ipcMain.handle('history.list', async (_e, args: { projectWslPath?: string; proje
10421042
});
10431043

10441044
ipcMain.handle('history.read', async (_e, { filePath }: { filePath: string }) => {
1045+
try {
1046+
const cached = getCachedDetails(filePath);
1047+
if (cached) return cached;
1048+
} catch {}
10451049
try {
10461050
const det = getIndexedDetails(filePath);
1047-
if (det) return det;
1051+
if (det) {
1052+
try { cacheDetails(filePath, det); } catch {}
1053+
return det;
1054+
}
10481055
} catch {}
1049-
return history.readHistoryFile(filePath);
1056+
const parsed = await history.readHistoryFile(filePath);
1057+
try { cacheDetails(filePath, parsed as any); } catch {}
1058+
return parsed;
10501059
});
10511060

10521061
// 扫描所有索引的会话,找出"筛选后 input_text/output_text 皆为空"的文件
@@ -1063,6 +1072,7 @@ ipcMain.handle('history.findEmptySessions', async () => {
10631072
let date = Number((s as any).date || 0);
10641073
let id = String((s as any).id || '');
10651074
if (det && det.messages) {
1075+
try { cacheDetails(s.filePath, det); } catch {}
10661076
messages = det.messages as any[];
10671077
if (!title && (det as any).title) title = String((det as any).title);
10681078
if (!rawDate && (det as any).rawDate) rawDate = String((det as any).rawDate);
@@ -1072,6 +1082,7 @@ ipcMain.handle('history.findEmptySessions', async () => {
10721082
// 兜底读取(性能较差,仅在详情缓存缺失时触发)
10731083
try {
10741084
const r = await history.readHistoryFile(s.filePath);
1085+
try { cacheDetails(s.filePath, r as any); } catch {}
10751086
messages = r.messages as any[];
10761087
if (!title && (r as any).title) title = String((r as any).title);
10771088
if (!date && (r as any).date) date = Number((r as any).date);

web/src/App.tsx

Lines changed: 101 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,24 +2264,30 @@ export default function CodexFlowManagerUI() {
22642264
logs: [],
22652265
createdAt: Date.now(),
22662266
};
2267-
registerTabProject(tab.id, project.id);
2268-
setTabsByProject((m) => ({ ...m, [project.id]: [...(m[project.id] || []), tab] }));
2269-
setActiveTab(tab.id, { focusMode: 'immediate', allowDuringRename: true, delay: 0 });
2267+
let ptyId: string | undefined;
22702268
try {
22712269
const startupCmd = injectTraceEnv(codexCmd);
22722270
const { id } = await window.host.pty.openWSLConsole({ distro: wslDistro, wslPath: project.wslPath, winPath: project.winPath, cols: 80, rows: 24, startupCmd });
2273-
ptyByTabRef.current[tab.id] = id;
2274-
setPtyByTab((m) => ({ ...m, [tab.id]: id }));
2275-
ptyAliveRef.current[tab.id] = true;
2276-
setPtyAlive((m) => ({ ...m, [tab.id]: true }));
2277-
registerPtyForTab(tab.id, id);
2278-
try { tm.setPty(tab.id, id); } catch (err) { console.warn('tm.setPty failed', err); }
2279-
try { window.host.projects.touch(project.id); } catch {}
2280-
// 打开控制台后,立即在内存中更新最近使用时间,保证"最近使用优先"实时生效
2281-
markProjectUsed(project.id);
2271+
ptyId = id;
22822272
} catch (e) {
22832273
console.error('Failed to open PTY for project', e);
2274+
alert(String(t('terminal:openFailed', { error: String((e as any)?.message || e) })));
2275+
return;
2276+
}
2277+
registerTabProject(tab.id, project.id);
2278+
setTabsByProject((m) => ({ ...m, [project.id]: [...(m[project.id] || []), tab] }));
2279+
setActiveTab(tab.id, { focusMode: 'immediate', allowDuringRename: true, delay: 0 });
2280+
if (ptyId) {
2281+
ptyByTabRef.current[tab.id] = ptyId;
2282+
setPtyByTab((m) => ({ ...m, [tab.id]: ptyId }));
2283+
ptyAliveRef.current[tab.id] = true;
2284+
setPtyAlive((m) => ({ ...m, [tab.id]: true }));
2285+
registerPtyForTab(tab.id, ptyId);
2286+
try { tm.setPty(tab.id, ptyId); } catch (err) { console.warn('tm.setPty failed', err); }
22842287
}
2288+
try { window.host.projects.touch(project.id); } catch {}
2289+
// 打开控制台后,立即在内存中更新最近使用时间,保证"最近使用优先"实时生效
2290+
markProjectUsed(project.id);
22852291
// 确保视图停留在控制台
22862292
try { setCenterMode('console'); } catch {}
22872293
}
@@ -2335,10 +2341,7 @@ export default function CodexFlowManagerUI() {
23352341
logs: [],
23362342
createdAt: Date.now(),
23372343
};
2338-
registerTabProject(tab.id, selectedProject.id);
2339-
setTabsByProject((m) => ({ ...m, [selectedProject.id]: [...(m[selectedProject.id] || []), tab] }));
2340-
setActiveTab(tab.id, { focusMode: 'immediate', allowDuringRename: true, delay: 0 });
2341-
2344+
let ptyId: string | undefined;
23422345
// Open PTY in main (WSL)
23432346
try {
23442347
try { await (window as any).host?.utils?.perfLog?.(`[ui] openNewConsole start project=${selectedProject?.name}`); } catch {}
@@ -2352,21 +2355,30 @@ export default function CodexFlowManagerUI() {
23522355
startupCmd,
23532356
});
23542357
try { await (window as any).host?.utils?.perfLog?.(`[ui] openNewConsole pty=${id}`); } catch {}
2355-
ptyByTabRef.current[tab.id] = id;
2356-
setPtyByTab((m) => ({ ...m, [tab.id]: id }));
2357-
ptyAliveRef.current[tab.id] = true;
2358-
setPtyAlive((m) => ({ ...m, [tab.id]: true }));
2359-
registerPtyForTab(tab.id, id);
2360-
// inform manager about PTY so it can wire bridges
2361-
try { tm.setPty(tab.id, id); } catch (err) { console.warn('tm.setPty failed', err); }
2362-
// touch project lastOpenedAt
2363-
try { window.host.projects.touch(selectedProject.id); } catch {}
2364-
// 同步更新内存,触发排序刷新;并抑制历史面板自动切换
2365-
markProjectUsed(selectedProject.id);
2358+
ptyId = id;
23662359
} catch (e) {
23672360
console.error('Failed to open PTY', e);
23682361
try { await (window as any).host?.utils?.perfLog?.(`[ui] openNewConsole error ${String((e as any)?.stack || e)}`); } catch {}
2362+
alert(String(t('terminal:openFailed', { error: String((e as any)?.message || e) })));
2363+
return;
2364+
}
2365+
2366+
registerTabProject(tab.id, selectedProject.id);
2367+
setTabsByProject((m) => ({ ...m, [selectedProject.id]: [...(m[selectedProject.id] || []), tab] }));
2368+
setActiveTab(tab.id, { focusMode: 'immediate', allowDuringRename: true, delay: 0 });
2369+
if (ptyId) {
2370+
ptyByTabRef.current[tab.id] = ptyId;
2371+
setPtyByTab((m) => ({ ...m, [tab.id]: ptyId }));
2372+
ptyAliveRef.current[tab.id] = true;
2373+
setPtyAlive((m) => ({ ...m, [tab.id]: true }));
2374+
registerPtyForTab(tab.id, ptyId);
2375+
// inform manager about PTY so it can wire bridges
2376+
try { tm.setPty(tab.id, ptyId); } catch (err) { console.warn('tm.setPty failed', err); }
23692377
}
2378+
// touch project lastOpenedAt
2379+
try { window.host.projects.touch(selectedProject.id); } catch {}
2380+
// 同步更新内存,触发排序刷新;并抑制历史面板自动切换
2381+
markProjectUsed(selectedProject.id);
23702382
}
23712383

23722384
// 当项目变更时,加载历史(项目范围)
@@ -3332,38 +3344,48 @@ export default function CodexFlowManagerUI() {
33323344
logs: [],
33333345
createdAt: Date.now(),
33343346
};
3335-
registerTabProject(tab.id, selectedProject.id);
3336-
setTabsByProject((m) => ({ ...m, [selectedProject.id]: [...(m[selectedProject.id] || []), tab] }));
3337-
setActiveTab(tab.id, { focusMode: 'immediate', allowDuringRename: true, delay: 0 });
3338-
try {
3339-
setCenterMode('console');
3340-
requestAnimationFrame(() => {
3341-
try { scheduleFocusForTab(tab.id, { immediate: true, allowDuringRename: true }); } catch {}
3342-
});
3343-
} catch {}
3347+
let ptyId: string | undefined;
33443348
try {
33453349
await (window as any).host?.utils?.perfLog?.(`[ui] history.resume openWSLConsole start tab=${tab.id}`);
33463350
} catch {}
3347-
const { id } = await window.host.pty.openWSLConsole({
3351+
try {
3352+
const { id } = await window.host.pty.openWSLConsole({
33483353
distro: wslDistro,
33493354
wslPath: selectedProject.wslPath,
33503355
winPath: selectedProject.winPath,
33513356
cols: 80,
33523357
rows: 24,
33533358
startupCmd,
33543359
});
3360+
try {
3361+
await (window as any).host?.utils?.perfLog?.(`[ui] history.resume pty=${id} tab=${tab.id} - registering listener`);
3362+
} catch {}
3363+
ptyId = id;
3364+
} catch (err) {
3365+
console.warn('executeResume failed', err);
3366+
alert(String(t('history:resumeFailed', { error: String((err as any)?.message || err) })));
3367+
return false;
3368+
}
3369+
registerTabProject(tab.id, selectedProject.id);
3370+
setTabsByProject((m) => ({ ...m, [selectedProject.id]: [...(m[selectedProject.id] || []), tab] }));
3371+
setActiveTab(tab.id, { focusMode: 'immediate', allowDuringRename: true, delay: 0 });
33553372
try {
3356-
await (window as any).host?.utils?.perfLog?.(`[ui] history.resume pty=${id} tab=${tab.id} - registering listener`);
3357-
} catch {}
3358-
ptyByTabRef.current[tab.id] = id;
3359-
setPtyByTab((m) => ({ ...m, [tab.id]: id }));
3360-
ptyAliveRef.current[tab.id] = true;
3361-
setPtyAlive((m) => ({ ...m, [tab.id]: true }));
3362-
registerPtyForTab(tab.id, id);
3363-
try {
3364-
await (window as any).host?.utils?.perfLog?.(`[ui] history.resume pty=${id} tab=${tab.id} - listener registered`);
3373+
setCenterMode('console');
3374+
requestAnimationFrame(() => {
3375+
try { scheduleFocusForTab(tab.id, { immediate: true, allowDuringRename: true }); } catch {}
3376+
});
33653377
} catch {}
3366-
try { tm.setPty(tab.id, id); } catch (err) { console.warn('tm.setPty failed', err); }
3378+
if (ptyId) {
3379+
ptyByTabRef.current[tab.id] = ptyId;
3380+
setPtyByTab((m) => ({ ...m, [tab.id]: ptyId }));
3381+
ptyAliveRef.current[tab.id] = true;
3382+
setPtyAlive((m) => ({ ...m, [tab.id]: true }));
3383+
registerPtyForTab(tab.id, ptyId);
3384+
try {
3385+
await (window as any).host?.utils?.perfLog?.(`[ui] history.resume pty=${ptyId} tab=${tab.id} - listener registered`);
3386+
} catch {}
3387+
try { tm.setPty(tab.id, ptyId); } catch (err) { console.warn('tm.setPty failed', err); }
3388+
}
33673389
try { window.host.projects.touch(selectedProject.id); } catch {}
33683390
// 内存也更新最近使用时间,并抑制历史面板自动切换
33693391
markProjectUsed(selectedProject.id);
@@ -3382,6 +3404,7 @@ export default function CodexFlowManagerUI() {
33823404
try {
33833405
await (window as any).host?.utils?.perfLog?.(`[ui] history.resume ${mode} error ${String((err as any)?.stack || err)}`);
33843406
} catch {}
3407+
alert(String(t('history:resumeFailed', { error: String((err as any)?.message || err) })));
33853408
return false;
33863409
}
33873410
};
@@ -4566,9 +4589,24 @@ function filterHistoryMessages(session: HistorySession, typeFilter: Record<strin
45664589

45674590
function HistoryDetail({ sessions, selectedHistoryId, onBack, onResume, onResumeExternal, terminalMode }: { sessions: HistorySession[]; selectedHistoryId: string | null; onBack?: () => void; onResume?: (filePath?: string) => void; onResumeExternal?: (filePath?: string) => void; terminalMode: 'wsl' | 'windows' }) {
45684591
const { t } = useTranslation(['history', 'common']);
4592+
const MAX_HISTORY_MESSAGE_CACHE = 5;
45694593
const [loaded, setLoaded] = useState(false);
45704594
const [skipped, setSkipped] = useState(0);
4571-
const [localSessions, setLocalSessions] = useState<HistorySession[]>(sessions);
4595+
const [localSessions, setLocalSessions] = useState<HistorySession[]>(() => sessions.map((s) => ({ ...s, messages: [] })));
4596+
const messageCacheIdsRef = useRef<string[]>([]);
4597+
const pruneMessages = useCallback((list: HistorySession[], allowed: Set<string>) => {
4598+
if (!Array.isArray(list) || list.length === 0) return list;
4599+
if (allowed.size === 0) return list.map((s) => ({ ...s, messages: [] }));
4600+
return list.map((s) => (allowed.has(s.id) ? s : { ...s, messages: [] }));
4601+
}, []);
4602+
const touchMessageCache = useCallback((id?: string | null) => {
4603+
if (!id) return messageCacheIdsRef.current;
4604+
const next = messageCacheIdsRef.current.filter((x) => x !== id);
4605+
next.unshift(id);
4606+
if (next.length > MAX_HISTORY_MESSAGE_CACHE) next.length = MAX_HISTORY_MESSAGE_CACHE;
4607+
messageCacheIdsRef.current = next;
4608+
return messageCacheIdsRef.current;
4609+
}, [MAX_HISTORY_MESSAGE_CACHE]);
45724610
const [typeFilter, setTypeFilter] = useState<Record<string, boolean>>({});
45734611
const [detailSearch, setDetailSearch] = useState("");
45744612
const reqSeq = useRef(0);
@@ -4594,18 +4632,23 @@ function HistoryDetail({ sessions, selectedHistoryId, onBack, onResume, onResume
45944632

45954633
// 刷新列表时保留已加载的消息内容,避免详情面板闪烁
45964634
useEffect(() => {
4635+
const filteredIds = messageCacheIdsRef.current.filter((id) => sessions.some((s) => s.id === id));
4636+
messageCacheIdsRef.current = filteredIds;
4637+
const allowed = new Set(filteredIds);
45974638
setLocalSessions((cur) => {
45984639
const prevMap = new Map(cur.map((x) => [x.id, x]));
4599-
return sessions.map((s) => {
4640+
const merged = sessions.map((s) => {
46004641
const prev = prevMap.get(s.id);
4601-
if (!prev) return s;
4642+
if (!prev) return allowed.has(s.id) ? s : { ...s, messages: [] };
46024643
const prevMsgs = Array.isArray(prev.messages) ? prev.messages : [];
46034644
const nextMsgs = Array.isArray(s.messages) ? s.messages : [];
4604-
if (nextMsgs.length === 0 && prevMsgs.length > 0) return { ...s, messages: prevMsgs };
4645+
if (allowed.has(s.id) && nextMsgs.length === 0 && prevMsgs.length > 0) return { ...s, messages: prevMsgs };
4646+
if (!allowed.has(s.id)) return { ...s, messages: [] };
46054647
return s;
46064648
});
4649+
return pruneMessages(merged, allowed);
46074650
});
4608-
}, [sessions]);
4651+
}, [sessions, pruneMessages]);
46094652

46104653
useEffect(() => {
46114654
setDetailSearch("");
@@ -4725,7 +4768,11 @@ function HistoryDetail({ sessions, selectedHistoryId, onBack, onResume, onResume
47254768
const res: any = await window.host.history.read({ filePath: String(selectedSession.filePath || '') });
47264769
const msgs = (res.messages || []).map((m: any) => ({ role: m.role as any, content: m.content }));
47274770
if (seq === reqSeq.current) {
4728-
setLocalSessions((cur) => cur.map((x) => (x.id === selectedHistoryId ? { ...x, messages: msgs } : x)));
4771+
const allowedIds = new Set(touchMessageCache(selectedHistoryId));
4772+
setLocalSessions((cur) => {
4773+
const next = cur.map((x) => (x.id === selectedHistoryId ? { ...x, messages: msgs } : x));
4774+
return pruneMessages(next, allowedIds);
4775+
});
47294776
setSkipped(res.skippedLines || 0);
47304777
setLoaded(true);
47314778
lastLoadedFingerprintRef.current = signature;
@@ -4757,7 +4804,7 @@ function HistoryDetail({ sessions, selectedHistoryId, onBack, onResume, onResume
47574804
if (seq === reqSeq.current) setLoaded(true);
47584805
}
47594806
})();
4760-
}, [selectedHistoryId, selectedSession, selectedSessionFingerprint, selectedLocalSession]);
4807+
}, [selectedHistoryId, selectedSession, selectedSessionFingerprint, selectedLocalSession, pruneMessages, touchMessageCache]);
47614808

47624809
function buildFilteredText(): string {
47634810
if (!selectedHistoryId) return '';

web/src/locales/en/history.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"resumeShellMismatch": "This conversation was recorded in {expected} mode and cannot run while {current} is active. Switch to {expected} mode and try again.",
3232
"resumeShellMismatchExpectedLabel": "Session mode",
3333
"resumeShellMismatchCurrentLabel": "Active mode",
34+
"resumeFailed": "Failed to resume conversation: {error}",
3435
"panelTitle": "Project History",
3536
"searchPlaceholder": "Search history…",
3637
"searchPlaceholderHint": "Search history by title, path, or first prompt",

web/src/locales/en/terminal.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"expandInput": "Expand composer",
77
"collapseInput": "Exit expand",
88
"readyPlaceholder": "# Terminal ready. Embedded console placeholder (UI prototype; no real PTY yet).",
9+
"openFailed": "Failed to start console: {error}",
910
"ctx": {
1011
"copy": "Copy",
1112
"copyLine": "Copy Line",

web/src/locales/zh/history.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"resumeShellMismatch": "该会话来自 {expected} 模式,当前运行环境为 {current},暂不支持跨环境恢复。请切换到 {expected} 模式后再试。",
3232
"resumeShellMismatchExpectedLabel": "会话模式",
3333
"resumeShellMismatchCurrentLabel": "当前模式",
34+
"resumeFailed": "恢复会话失败:{error}",
3435
"panelTitle": "项目历史",
3536
"searchPlaceholder": "搜索历史记录…",
3637
"searchPlaceholderHint": "按标题、路径或输入首条历史内容搜索历史记录",

web/src/locales/zh/terminal.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"expandInput": "展开输入框",
77
"collapseInput": "退出大屏",
88
"readyPlaceholder": "# 终端就绪。这里是内嵌控制台的视觉占位(UI 原型,未接真 PTY)。",
9+
"openFailed": "启动终端失败:{error}",
910
"ctx": {
1011
"copy": "复制",
1112
"copyLine": "复制整行",

0 commit comments

Comments
 (0)