返回 DeepSeek-Reasonix
autoHTML.ts
根目录 / desktop / frontend / src / lib / autoHTML.ts
1 import type { PresentedFile, WireEvent } from "./types";
2 import type { State } from "./useController";
3 import { t } from "./i18n";
4 import { RECOVERABLE_ERROR_EVENT } from "./globalCrashHandlers";
5
6 export interface AutoPresentedHTMLClaim {
7 file: PresentedFile;
8 toolCallId: string;
9 }
10
11 /** Runtime-only gate for the first HTML present result in each live turn. */
12 export class AutoPresentedHTMLGate {
13 private readonly claimedTurnByTab = new Map<string, string>();
14
15 observeTurnStart(tabId: string): void {
16 this.claimedTurnByTab.delete(tabId);
17 }
18
19 claim(event: WireEvent, tabId: string, activeTabId: string | undefined, sessionGeneration: number, activeTurnId?: string, turnRevision = 0): AutoPresentedHTMLClaim | null {
20 if (event.kind !== "tool_result" || event.tool?.err || !event.tool?.id || tabId !== activeTabId) return null;
21 if (event.tool.name !== "present" && event.tool.resolvedName !== "present") return null;
22 const file = event.tool.presentedFiles?.find((candidate) => /\.html?$/i.test(candidate.path));
23 if (!file) return null;
24 const turnKey = `${turnRevision}:${sessionGeneration}:${activeTurnId ?? event.turnId ?? "active"}`;
25 if (this.claimedTurnByTab.get(tabId) === turnKey) return null;
26 this.claimedTurnByTab.set(tabId, turnKey);
27 return { file, toolCallId: event.tool.id };
28 }
29 }
30
31 export async function openAutomaticPresentedHTML(
32 gate: AutoPresentedHTMLGate,
33 event: WireEvent,
34 input: { tabId: string; activeTabId?: string; sessionGeneration: number; activeTurnId?: string; turnRevision: number },
35 ): Promise<string | null> {
36 const claim = gate.claim(event, input.tabId, input.activeTabId, input.sessionGeneration, input.activeTurnId, input.turnRevision);
37 if (!claim) return null;
38 const { openResource } = await import("./fileNavigationCommands");
39 const outcome = await openResource({
40 source: "presented", hostId: "local", tabId: input.tabId,
41 toolCallId: claim.toolCallId, path: claim.file.path,
42 sessionGeneration: input.sessionGeneration,
43 }, { view: "preview" });
44 return outcome.status === "failed" ? outcome.error.message : null;
45 }
46
47 const automaticGate = new AutoPresentedHTMLGate();
48
49 /** Keep the live-event controller small; all preview policy and failures stay in this lazy chunk. */
50 export default async function handleAutomaticPresentedHTML(
51 event: WireEvent,
52 tabId: string,
53 activeTab: { current?: string },
54 states: { current: Map<string, State> },
55 ): Promise<void> {
56 if (event.kind === "turn_done") {
57 automaticGate.observeTurnStart(tabId);
58 return;
59 }
60 const report = (error: unknown) => window.dispatchEvent(new CustomEvent(RECOVERABLE_ERROR_EVENT, { detail: {
61 message: t("chat.fileActionFailed", { error: error instanceof Error ? error.message : String(error) }),
62 } }));
63 try {
64 const activeTabId = activeTab.current;
65 const state = states.current.get(tabId);
66 if (!state?.meta || state.meta.remote) return;
67 const error = await openAutomaticPresentedHTML(automaticGate, event, {
68 tabId,
69 activeTabId,
70 sessionGeneration: state.meta.sessionGeneration ?? event.sessionGeneration ?? 0,
71 activeTurnId: state.activeTurnId,
72 turnRevision: 0,
73 });
74 if (error) report(error);
75 } catch (error) {
76 report(error);
77 }
78 }
79
79 lines TYPESCRIPT