返回 DeepSeek-Reasonix
chatContentLoader.ts
根目录 / desktop / frontend / src / lib / chatContentLoader.ts
1 import { app } from "./bridge";
2 import { contentRequestScheduler } from "./contentRequestScheduler";
3 import { historyEntryIdForItemId } from "./transcriptHistoryEntry";
4 import { getTranscriptStore } from "./transcriptStore";
5 import type { Item } from "./useController";
6
7 type ContentField = "content" | "reasoning" | "tool" | "summary";
8
9 /** Two requests per session under a shared four-request application budget. */
10 export class ChatContentLoader {
11 private readonly requestOwner = contentRequestScheduler.owner();
12 private closed = false;
13 private generation = 0;
14 private pending = new Map<string, { item: Item; promise: Promise<string> }>();
15 constructor(private tabId?: string, private resolve?: (item: Item, field: ContentField) => Promise<string>) {}
16 activate() { this.closed = false; }
17 needsFullContent(item: Item, field: ContentField): boolean {
18 if (field === "tool" && item.kind === "tool" && (item.dataArchived || item.truncated || item.contentState === "unloaded")) return true;
19 const entry = item.kind === "compaction" ? item.historyEntryId : historyEntryIdForItemId(item.id);
20 return Boolean(entry && this.tabId && getTranscriptStore().hasContentReference(this.tabId, entry, field));
21 }
22 load = (item: Item, field: ContentField): Promise<string> => {
23 const key = `${item.id}:${field}`;
24 const previous = this.pending.get(key);
25 if (previous && sameContent(previous.item, item)) return previous.promise;
26 const generation = this.generation;
27 const request = new Promise<string>((resolve, reject) => {
28 const run = () => {
29 if (this.closed || generation !== this.generation) { reject(new Error("Content view closed")); return; }
30 void (this.resolve ? this.resolve(item, field) : this.fetch(item, field)).then(value => {
31 if (this.closed || generation !== this.generation) reject(new Error("Content view closed"));
32 else {
33 if (field === "tool" && item.kind === "tool" && this.tabId) getTranscriptStore().publishToolDetails(this.tabId, item, value);
34 resolve(value);
35 }
36 }, reject).finally(() => {
37 if (this.pending.get(key)?.promise === request) this.pending.delete(key);
38 contentRequestScheduler.release(this.requestOwner);
39 });
40 };
41 contentRequestScheduler.schedule(this.requestOwner, run, () => reject(new Error("Content view closed")));
42 });
43 this.pending.set(key, { item, promise: request });
44 return request;
45 };
46 private async fetch(item: Item, field: ContentField): Promise<string> {
47 if (field === "tool" && item.kind === "tool") {
48 if (this.tabId && item.contentState === "unloaded") {
49 const canonical = await getTranscriptStore().requestToolContent(this.tabId, item, { args: item.args, output: item.output, error: item.error, execution: item.execution });
50 if (canonical !== undefined) return canonical;
51 }
52
53 if (this.tabId && getTranscriptStore().hasContentResolver(this.tabId)) {
54 const full = await getTranscriptStore().requestFullContent(this.tabId, item.id, "tool");
55 if (full === undefined) throw new Error("Tool content unavailable");
56 return full;
57 }
58 let value: Record<string, unknown> = { args: item.args, output: item.output, error: item.error, diff: item.fileDiff, execution: item.execution };
59 if (item.dataArchived) {
60 if (!this.tabId) throw new Error("Tool content unavailable");
61 const archived = await app.ToolResultForTab(this.tabId, item.id);
62 if (!archived) throw new Error("Tool content unavailable");
63 value = { ...value, ...archived };
64 }
65 return (this.tabId && await getTranscriptStore().requestToolContent(this.tabId, item, value)) || JSON.stringify(value, null, 2);
66 }
67 const fallback = item.kind === "assistant" ? field === "reasoning" ? item.reasoning : item.text
68 : item.kind === "compaction" && field === "summary" ? item.summary
69 : item.kind === "user" || item.kind === "phase" || item.kind === "notice" ? item.text : "";
70 const entry = item.kind === "compaction" ? item.historyEntryId
71 : item.kind === "user" && item.messageId ? `m:${item.messageId}` : historyEntryIdForItemId(item.id);
72 if (!entry || !this.tabId) return fallback;
73 const store = getTranscriptStore();
74 const text = await store.requestFullContent(this.tabId, entry, field);
75 if (text === undefined && store.hasContentReference(this.tabId, entry, field)) throw new Error("Content reference unavailable; retry");
76 return text ?? fallback;
77 }
78 dispose() { this.generation++; this.closed = true; contentRequestScheduler.cancel(this.requestOwner); this.pending.clear(); }
79 }
80
81 function sameContent(a: Item, b: Item): boolean {
82 if (a === b) return true;
83 if (a.kind === "assistant" && b.kind === "assistant") return a.text === b.text && a.reasoning === b.reasoning && a.streaming === b.streaming;
84 if (a.kind === "tool" && b.kind === "tool") return a.args === b.args && a.output === b.output && a.error === b.error && a.fileDiff === b.fileDiff && a.dataArchived === b.dataArchived && a.execution === b.execution && a.status === b.status;
85 return "text" in a && "text" in b && a.kind === b.kind && a.text === b.text;
86 }
87
87 lines TYPESCRIPT