返回 DeepSeek-Reasonix
useWorkspaceFilePreview.ts
根目录 / desktop / frontend / src / components / useWorkspaceFilePreview.ts
1 import { useCallback, useEffect, useRef, useState } from "react";
2 import { app } from "../lib/bridge";
3 import {
4 beginKeyedResourceRequest,
5 emptyKeyedResource,
6 rejectKeyedResourceRequest,
7 resolveKeyedResourceRequest,
8 } from "../lib/keyedResource";
9 import type { FilePreview } from "../lib/types";
10 import type { FileResourceSource } from "../lib/fileResource";
11
12 export type WorkspaceFilePreviewInput = Readonly<{
13 /** The dock is on screen; a hidden dock reads nothing. */
14 open: boolean;
15 /** The path the preview shows. */
16 selectedPath: string | null;
17 /** Presented tool call of the selected entry; a workspace entry passes none. */
18 presentedToolCallId?: string;
19 /** Selects the host reader that revalidates this entry's access. */
20 accessSource: FileResourceSource;
21 /** The selected entry renders as source. */
22 source: boolean;
23 /** Dock lifecycle generation; a new one re-reads the selection. */
24 generation: number;
25 workspaceScopeKey: string;
26 workspaceTabId: string;
27 /** Bumped by the workspace refresh owner when the content behind it changed. */
28 contentRevision: number;
29 }>;
30
31 /**
32 * The dock's file preview: which read its selection implies, the keyed resource
33 * that holds the result, and the one command that re-reads it. The read inputs
34 * come from the selected entry, never from a path-keyed cache, so a file
35 * reopened from another entry point reads under that command's credentials and
36 * an unchanged set of inputs never restarts a read that is still valid.
37 */
38 export function useWorkspaceFilePreview(input: WorkspaceFilePreviewInput) {
39 const { accessSource, contentRevision, generation, open, presentedToolCallId, selectedPath, source, workspaceScopeKey, workspaceTabId } = input;
40 const [resource, setResource] = useState(() => emptyKeyedResource<FilePreview>());
41 const [stale, setStale] = useState(false);
42 // A read that lands after this dock is gone must not be committed: nothing
43 // would render it. StrictMode's simulated unmount sets this back to mounted,
44 // so a replayed mount keeps the read it already started.
45 const mountedRef = useRef(true);
46 useEffect(() => {
47 mountedRef.current = true;
48 return () => { mountedRef.current = false; };
49 }, []);
50 const requestIdRef = useRef(0);
51 const scopeRef = useRef(workspaceScopeKey);
52 const generationRef = useRef(generation);
53 scopeRef.current = workspaceScopeKey;
54 generationRef.current = generation;
55 const previewKey = selectedPath
56 ? `${workspaceScopeKey}\u0000preview\u0000${source ? "source" : "preview"}\u0000${accessSource}\u0000${presentedToolCallId ?? ""}\u0000${selectedPath}`
57 : null;
58 const preview = previewKey && resource.key === previewKey ? resource.data : null;
59 const loading = previewKey != null && resource.key === previewKey && resource.status === "refreshing";
60 const error = previewKey && resource.key === previewKey ? resource.error : "";
61
62 const refreshSelected = useCallback(() => {
63 if (!selectedPath) return;
64 setStale(false);
65 const requestId = ++requestIdRef.current;
66 const requestPath = selectedPath;
67 const requestGeneration = generation;
68 const requestKey = `${workspaceScopeKey}\u0000preview\u0000${source ? "source" : "preview"}\u0000${accessSource}\u0000${presentedToolCallId ?? ""}\u0000${requestPath}`;
69 // Dock instance, session scope, resource identity and operation revision
70 // must all still match before a result may be committed for this read.
71 const current = () =>
72 mountedRef.current
73 && requestIdRef.current === requestId
74 && scopeRef.current === workspaceScopeKey
75 && generationRef.current === requestGeneration;
76 setResource((state) => beginKeyedResourceRequest(state, requestKey, requestId, contentRevision));
77 const read = accessSource === "reference"
78 ? source
79 ? app.ReadReferenceFileSourceForTab(workspaceTabId, requestPath)
80 : app.ReadReferenceFileForTab(workspaceTabId, requestPath)
81 : presentedToolCallId
82 ? source
83 ? app.ReadPresentedFileSourceForTab(workspaceTabId, presentedToolCallId, requestPath)
84 : app.ReadPresentedFileForTab(workspaceTabId, presentedToolCallId, requestPath)
85 : app.ReadFileForTab(workspaceTabId, requestPath);
86 read
87 .then((next) => {
88 if (current()) setResource((state) => resolveKeyedResourceRequest(state, requestKey, requestId, next, contentRevision));
89 })
90 .catch((reason) => {
91 if (current()) {
92 setResource((state) => rejectKeyedResourceRequest(state, requestKey, requestId, String((reason as Error)?.message ?? reason)));
93 }
94 });
95 }, [accessSource, contentRevision, generation, presentedToolCallId, selectedPath, source, workspaceScopeKey, workspaceTabId]);
96
97 // The read starts once per set of read inputs. A StrictMode mount replay, an
98 // effect reconnect or a re-render reconnect calls the effect again with the
99 // very same callback, and must not issue a second read for it.
100 const startedReadRef = useRef<typeof refreshSelected | null>(null);
101 useEffect(() => {
102 if (!open || !selectedPath) {
103 startedReadRef.current = null;
104 return;
105 }
106 if (startedReadRef.current === refreshSelected) return;
107 startedReadRef.current = refreshSelected;
108 refreshSelected();
109 }, [open, refreshSelected, selectedPath]);
110
111 const reset = useCallback(() => setResource(emptyKeyedResource()), []);
112 return {
113 previewKey,
114 preview,
115 loadingPreview: Boolean(loading),
116 previewErr: error,
117 presentedFileStale: stale,
118 setPresentedFileStale: setStale,
119 refreshSelected,
120 resetPreview: reset,
121 };
122 }
123
123 lines TYPESCRIPT