Skip to content

Commit 443a051

Browse files
authored
fix: 保持历史详情刷新后的会话内容 (#6)
Signed-off-by: Lulu <58587930+lulu-sk@users.noreply.github.com>
1 parent 7d255c0 commit 443a051

1 file changed

Lines changed: 74 additions & 39 deletions

File tree

web/src/App.tsx

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ function canonicalizePath(p: string): string {
123123

124124
function normDir(p?: string): string { return canonicalizePath(getDir(p)); }
125125

126+
function historyDirKey(p?: string): string { return normDir(p) || '__unknown__'; }
127+
126128
// ---------- Helpers ----------
127129

128130
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
@@ -869,13 +871,13 @@ export default function CodexFlowManagerUI() {
869871
// 若当前选择无效或为空,重置为缓存中的第一组(除非是点击项目触发的切换)
870872
if (!skipAuto) {
871873
const ids = new Set(cached.map((x) => x.id));
872-
const dirs = new Set(cached.map((x) => normDir(x.filePath) || '__unknown__'));
874+
const dirs = new Set(cached.map((x) => historyDirKey(x.filePath)));
873875
if (!selectedHistoryId || !ids.has(selectedHistoryId) || !selectedHistoryDir || !dirs.has(selectedHistoryDir)) {
874-
const firstKey = normDir(cached[0]?.filePath || '') || '__unknown__';
876+
const firstKey = historyDirKey(cached[0]?.filePath);
875877
if (firstKey) {
876878
setSelectedHistoryDir(firstKey);
877879
const firstInDir = cached
878-
.filter((x) => (normDir(x.filePath) || '__unknown__') === firstKey)
880+
.filter((x) => historyDirKey(x.filePath) === firstKey)
879881
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
880882
setSelectedHistoryId(firstInDir?.id || null);
881883
setCenterMode('history');
@@ -917,15 +919,15 @@ export default function CodexFlowManagerUI() {
917919
// 校验并修正当前选择,避免缓存残留导致的空白详情
918920
if (!skipAuto) {
919921
const ids = new Set(mapped.map((x) => x.id));
920-
const dirs = new Set(mapped.map((x) => normDir(x.filePath)).filter(Boolean));
922+
const dirs = new Set(mapped.map((x) => historyDirKey(x.filePath)));
921923
const needResetId = !selectedHistoryId || !ids.has(selectedHistoryId);
922924
const needResetDir = !selectedHistoryDir || !dirs.has(selectedHistoryDir);
923925
if (needResetId || needResetDir) {
924-
const firstKey = normDir(mapped[0]?.filePath || '') || '__unknown__';
926+
const firstKey = historyDirKey(mapped[0]?.filePath);
925927
if (firstKey) {
926928
setSelectedHistoryDir(firstKey);
927929
const firstInDir = mapped
928-
.filter((x) => (normDir(x.filePath) || '__unknown__') === firstKey)
930+
.filter((x) => historyDirKey(x.filePath) === firstKey)
929931
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
930932
setSelectedHistoryId(firstInDir?.id || null);
931933
setCenterMode('history');
@@ -1039,18 +1041,18 @@ export default function CodexFlowManagerUI() {
10391041
} catch {}
10401042
// 若当前选中项被移除,选择同组最新一条
10411043
if (selectedHistoryId && !next.some((x) => x.id === selectedHistoryId)) {
1042-
const key = normDir(filePath) || '__unknown__';
1044+
const key = historyDirKey(filePath);
10431045
const restInGroup = next
1044-
.filter((x) => (normDir(x.filePath) || '__unknown__') === key)
1046+
.filter((x) => historyDirKey(x.filePath) === key)
10451047
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
10461048
if (restInGroup.length > 0) setSelectedHistoryId(restInGroup[0].id);
10471049
else {
1048-
const groups = Array.from(new Set(next.map((x) => normDir(x.filePath) || '__unknown__')));
1050+
const groups = Array.from(new Set(next.map((x) => historyDirKey(x.filePath))));
10491051
const firstKey = groups[0] || null;
10501052
setSelectedHistoryDir(firstKey);
10511053
if (firstKey) {
10521054
const firstInDir = next
1053-
.filter((x) => (normDir(x.filePath) || '__unknown__') === firstKey)
1055+
.filter((x) => historyDirKey(x.filePath) === firstKey)
10541056
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
10551057
setSelectedHistoryId(firstInDir ? firstInDir.id : null);
10561058
} else {
@@ -2380,51 +2382,84 @@ function HistoryDetail({ sessions, selectedHistoryId, onBack, onResume, onResume
23802382
const [localSessions, setLocalSessions] = useState<HistorySession[]>(sessions);
23812383
const [typeFilter, setTypeFilter] = useState<Record<string, boolean>>({});
23822384
const reqSeq = useRef(0);
2383-
2384-
useEffect(() => setLocalSessions(sessions), [sessions]);
2385+
const lastLoadedFingerprintRef = useRef<string>("");
2386+
const selectedSession = useMemo(() => sessions.find((x) => x.id === selectedHistoryId) || null, [sessions, selectedHistoryId]);
2387+
const selectedLocalSession = useMemo(
2388+
() => localSessions.find((x) => x.id === selectedHistoryId) || null,
2389+
[localSessions, selectedHistoryId],
2390+
);
2391+
const selectedSessionFingerprint = useMemo(() => {
2392+
if (!selectedSession) return "none";
2393+
return [
2394+
selectedSession.id,
2395+
selectedSession.filePath || "",
2396+
selectedSession.date || "",
2397+
selectedSession.resumeMode || "",
2398+
selectedSession.resumeId || "",
2399+
selectedSession.preview || "",
2400+
selectedSession.rawDate || "",
2401+
].join("|");
2402+
}, [selectedSession]);
2403+
2404+
// 刷新列表时保留已加载的消息内容,避免详情面板闪烁
2405+
useEffect(() => {
2406+
setLocalSessions((cur) => {
2407+
const prevMap = new Map(cur.map((x) => [x.id, x]));
2408+
return sessions.map((s) => {
2409+
const prev = prevMap.get(s.id);
2410+
if (!prev) return s;
2411+
const prevMsgs = Array.isArray(prev.messages) ? prev.messages : [];
2412+
const nextMsgs = Array.isArray(s.messages) ? s.messages : [];
2413+
if (nextMsgs.length === 0 && prevMsgs.length > 0) return { ...s, messages: prevMsgs };
2414+
return s;
2415+
});
2416+
});
2417+
}, [sessions]);
23852418

23862419
useEffect(() => {
2420+
if (!selectedHistoryId || !selectedSession || !selectedSession.filePath) return;
2421+
const signature = selectedSessionFingerprint;
2422+
const hasMessages = !!(selectedLocalSession && Array.isArray(selectedLocalSession.messages) && selectedLocalSession.messages.length > 0);
2423+
if (hasMessages && lastLoadedFingerprintRef.current === signature) return;
2424+
setLoaded(false);
2425+
const seq = ++reqSeq.current;
23872426
(async () => {
2388-
if (!selectedHistoryId) return;
2389-
const s = sessions.find((x) => x.id === selectedHistoryId);
2390-
if (!s || !s.filePath) return;
2391-
setLoaded(false);
2392-
const seq = ++reqSeq.current;
23932427
try {
2394-
const res: any = await window.host.history.read({ filePath: s.filePath });
2428+
const res: any = await window.host.history.read({ filePath: selectedSession.filePath });
23952429
const msgs = (res.messages || []).map((m: any) => ({ role: m.role as any, content: m.content }));
23962430
if (seq === reqSeq.current) {
23972431
setLocalSessions((cur) => cur.map((x) => (x.id === selectedHistoryId ? { ...x, messages: msgs } : x)));
23982432
setSkipped(res.skippedLines || 0);
23992433
setLoaded(true);
2434+
lastLoadedFingerprintRef.current = signature;
24002435
}
2401-
try {
2402-
const allKeys = new Set<string>();
2403-
for (const mm of msgs) {
2404-
for (const it of (mm.content || [])) for (const k of keysOfItemCanonical(it)) allKeys.add(k);
2405-
}
2406-
// 去重策略:若同时存在 base 与 message.<base>,则仅保留 base(避免重复展示)。
2407-
const BASES = new Set(['input_text','output_text','text','code','json','instructions','environment_context','summary']);
2408-
const hasBase = (b: string) => allKeys.has(b);
2409-
const filtered: string[] = [];
2410-
for (const k of Array.from(allKeys)) {
2411-
if (k.startsWith('message.')) {
2412-
const tail = k.slice('message.'.length);
2413-
if (BASES.has(tail) && hasBase(tail)) continue; // drop redundant message.<base>
2436+
try {
2437+
const allKeys = new Set<string>();
2438+
for (const mm of msgs) {
2439+
for (const it of (mm.content || [])) for (const k of keysOfItemCanonical(it)) allKeys.add(k);
24142440
}
2415-
filtered.push(k);
2416-
}
2417-
filtered.sort();
2418-
const next: Record<string, boolean> = {};
2419-
for (const k of filtered) next[k] = (k === 'input_text' || k === 'output_text');
2420-
if (seq === reqSeq.current) setTypeFilter(next);
2421-
} catch {}
2441+
// 去重策略:若同时存在 base 与 message.<base>,则仅保留 base(避免重复展示)。
2442+
const BASES = new Set(['input_text','output_text','text','code','json','instructions','environment_context','summary']);
2443+
const hasBase = (b: string) => allKeys.has(b);
2444+
const filtered: string[] = [];
2445+
for (const k of Array.from(allKeys)) {
2446+
if (k.startsWith('message.')) {
2447+
const tail = k.slice('message.'.length);
2448+
if (BASES.has(tail) && hasBase(tail)) continue;
2449+
}
2450+
filtered.push(k);
2451+
}
2452+
filtered.sort();
2453+
const next: Record<string, boolean> = {};
2454+
for (const k of filtered) next[k] = (k === 'input_text' || k === 'output_text');
2455+
if (seq === reqSeq.current) setTypeFilter(next);
2456+
} catch {}
24222457
} catch (e) {
24232458
console.warn('history.read failed', e);
24242459
if (seq === reqSeq.current) setLoaded(true);
24252460
}
24262461
})();
2427-
}, [selectedHistoryId]);
2462+
}, [selectedHistoryId, selectedSession, selectedSessionFingerprint, selectedLocalSession]);
24282463

24292464
function buildFilteredText(): string {
24302465
if (!selectedHistoryId) return '';

0 commit comments

Comments
 (0)