返回 DeepSeek-Reasonix
useHistoryCatalog.ts
根目录 / desktop / frontend / src / lib / useHistoryCatalog.ts
1 import { desktopHost } from "./desktopHost";
2 import { useCallback, useEffect, useRef, useState } from "react";
3 import { app } from "./bridge";
4 import type { HistorySearchHit, SessionMeta } from "./types";
5
6 function hitKey(hit: HistorySearchHit): string {
7 return `${hit.sessionPath}:${hit.messageIndex}:${hit.kind}:${hit.toolName ?? ""}`;
8 }
9
10 export function useHistoryCatalog({
11 isTrash,
12 suppliedSessions,
13 scope,
14 status,
15 timeFilter,
16 query,
17 }: {
18 isTrash: boolean;
19 suppliedSessions: SessionMeta[];
20 scope: string;
21 status: string;
22 timeFilter: string;
23 query: string;
24 }) {
25 const [catalogSessions, setCatalogSessions] = useState<SessionMeta[]>(suppliedSessions);
26 const [nextSessionCursor, setNextSessionCursor] = useState("");
27 const [nextSearchCursor, setNextSearchCursor] = useState("");
28 const [partial, setPartial] = useState(false);
29 const [progress, setProgress] = useState({ indexed: 0, total: 0 });
30 const [searchHits, setSearchHits] = useState<HistorySearchHit[]>([]);
31 const [refreshNonce, setRefreshNonce] = useState(0);
32 const requestSeq = useRef(0);
33 const revision = useRef(0);
34 const refreshTimer = useRef<number | null>(null);
35 const workspaceRoot = suppliedSessions.find((session) => session.current)?.workspaceRoot ?? "";
36
37 useEffect(() => {
38 if (isTrash) setCatalogSessions(suppliedSessions);
39 }, [isTrash, suppliedSessions]);
40
41 const fetchPage = useCallback(async (
42 sessionCursor: string,
43 searchCursor: string,
44 append: boolean,
45 seq: number,
46 ) => {
47 const base = { scope, workspaceRoot, status, timeFilter, query: query.trim(), limit: 50 };
48 const sessionPromise = (!append || sessionCursor)
49 ? app.ListHistorySessions({ ...base, cursor: sessionCursor })
50 : Promise.resolve(null);
51 const searchPromise = query.trim() && (!append || searchCursor)
52 ? app.SearchHistoryContent({ ...base, cursor: searchCursor, kinds: [], toolName: "" })
53 : Promise.resolve(null);
54 const [page, bodyPage] = await Promise.all([sessionPromise, searchPromise]);
55 if (seq !== requestSeq.current) return;
56 if (page?.staleCursor && sessionCursor) {
57 void fetchPage("", "", false, seq);
58 return;
59 }
60 if (bodyPage?.staleCursor && searchCursor) {
61 void fetchPage("", "", false, seq);
62 return;
63 }
64 if (page) {
65 setCatalogSessions((current) => append
66 ? [...current, ...page.items.filter((item) => !current.some((existing) => existing.path === item.path))]
67 : page.items);
68 setNextSessionCursor(page.nextCursor || "");
69 } else if (!append) {
70 setCatalogSessions([]);
71 setNextSessionCursor("");
72 }
73 setPartial(Boolean(page?.partial) || Boolean(bodyPage?.partial));
74 if (bodyPage) {
75 setSearchHits((current) => {
76 if (!append) return bodyPage.items ?? [];
77 const seen = new Set(current.map(hitKey));
78 const merged = [...current];
79 for (const hit of bodyPage.items ?? []) {
80 const key = hitKey(hit);
81 if (seen.has(key)) continue;
82 seen.add(key);
83 merged.push(hit);
84 }
85 return merged;
86 });
87 setNextSearchCursor(bodyPage.nextCursor || "");
88 setProgress({ indexed: bodyPage.status.indexed, total: bodyPage.status.total });
89 } else if (!append) {
90 setSearchHits([]);
91 setNextSearchCursor("");
92 }
93 }, [query, scope, status, timeFilter, workspaceRoot]);
94
95 useEffect(() => {
96 if (isTrash || typeof window === "undefined" || desktopHost().kind === "none") return;
97 const seq = ++requestSeq.current;
98 const timer = window.setTimeout(() => {
99 void fetchPage("", "", false, seq).catch(() => {
100 if (seq !== requestSeq.current) return;
101 setCatalogSessions(suppliedSessions);
102 setNextSessionCursor("");
103 setNextSearchCursor("");
104 setSearchHits([]);
105 });
106 }, query.trim() ? 200 : 0);
107 return () => window.clearTimeout(timer);
108 }, [fetchPage, isTrash, query, refreshNonce, suppliedSessions]);
109
110 useEffect(() => {
111 const host = desktopHost();
112 if (isTrash || typeof window === "undefined" || host.kind === "none") return;
113 const unsubscribe = host.events.on("history-index:changed-v1", (payload?: unknown) => {
114 if (!payload || typeof payload !== "object") return;
115 const event = payload as { revision?: number; indexed?: number; total?: number; pending?: number };
116 const nextRevision = typeof event.revision === "number" ? event.revision : 0;
117 if (nextRevision <= revision.current) return;
118 revision.current = nextRevision;
119 setProgress({ indexed: Number(event.indexed) || 0, total: Number(event.total) || 0 });
120 setPartial((Number(event.pending) || 0) > 0 || (Number(event.indexed) || 0) < (Number(event.total) || 0));
121 if (refreshTimer.current === null) {
122 refreshTimer.current = window.setTimeout(() => {
123 refreshTimer.current = null;
124 setRefreshNonce((value) => value + 1);
125 }, 200);
126 }
127 });
128 return () => {
129 unsubscribe();
130 if (refreshTimer.current !== null) window.clearTimeout(refreshTimer.current);
131 refreshTimer.current = null;
132 };
133 }, [isTrash]);
134
135 const loadMore = useCallback(() => {
136 if (!nextSessionCursor && !nextSearchCursor) return;
137 void fetchPage(nextSessionCursor, nextSearchCursor, true, requestSeq.current);
138 }, [fetchPage, nextSearchCursor, nextSessionCursor]);
139
140 return {
141 sessions: isTrash ? suppliedSessions : catalogSessions,
142 nextCursor: nextSessionCursor || nextSearchCursor,
143 partial,
144 progress,
145 searchHits,
146 loadMore,
147 };
148 }
149
149 lines TYPESCRIPT