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