返回 DeepSeek-Reasonix
terminal.ts
根目录 / desktop / frontend / src / store / terminal.ts
1 import { create } from "zustand";
2
3 import { app } from "../lib/bridge";
4 import type { TerminalSessionView, TerminalWorkspaceView } from "../lib/types";
5 import { forgetTerminalSession, registerTerminalExitListener } from "../lib/terminalEvents";
6
7 type TerminalState = {
8 tabId: string;
9 generation: number;
10 workspace: TerminalWorkspaceView | null;
11 loading: boolean;
12 error: string | null;
13 activeSessionId: string | null;
14 syncWorkspace: (tabId: string, force?: boolean) => Promise<TerminalWorkspaceView | null>;
15 ensureReady: (tabId: string) => Promise<TerminalWorkspaceView | null>;
16 createSession: (tabId: string, relativePath?: string, shellId?: string) => Promise<TerminalSessionView | null>;
17 write: (tabId: string, sessionId: string, data: string) => Promise<void>;
18 resize: (tabId: string, sessionId: string, cols: number, rows: number) => Promise<void>;
19 closeSession: (tabId: string, sessionId: string) => Promise<void>;
20 renameSession: (tabId: string, sessionId: string, title: string) => Promise<void>;
21 clearError: () => void;
22 setActiveSession: (sessionId: string | null) => void;
23 };
24
25 let inFlight: { tabId: string; promise: Promise<TerminalWorkspaceView | null> } | null = null;
26
27 function errorMessage(error: unknown): string {
28 return error instanceof Error ? error.message : String(error);
29 }
30
31 function normalizedWorkspace(value: TerminalWorkspaceView): TerminalWorkspaceView {
32 return {
33 ...value,
34 sessions: Array.isArray(value.sessions) ? value.sessions : [],
35 shells: Array.isArray(value.shells) ? value.shells : [],
36 };
37 }
38
39 export const useTerminalStore = create<TerminalState>((set, get) => ({
40 tabId: "",
41 generation: 0,
42 workspace: null,
43 loading: false,
44 error: null,
45 activeSessionId: null,
46 async syncWorkspace(tabId, force = false) {
47 const normalizedTabId = tabId.trim();
48 if (!normalizedTabId) {
49 set({ tabId: "", workspace: null, activeSessionId: null, loading: false, error: null });
50 return null;
51 }
52 if (!force && inFlight?.tabId === normalizedTabId) return inFlight.promise;
53 const generation = get().generation + 1;
54 set({ tabId: normalizedTabId, generation, loading: true, workspace: null, activeSessionId: null, error: null });
55 const request = app.TerminalWorkspaceForTab(normalizedTabId)
56 .then((value) => {
57 const workspace = normalizedWorkspace(value);
58 const current = get();
59 if (current.tabId !== normalizedTabId || current.generation !== generation) return null;
60 const active = workspace.sessions.find((session) => session.running)?.id ?? workspace.sessions[0]?.id ?? null;
61 set({ workspace, loading: false, error: null, activeSessionId: active });
62 return workspace;
63 })
64 .catch((error) => {
65 const current = get();
66 if (current.tabId === normalizedTabId && current.generation === generation) {
67 set({ workspace: null, loading: false, error: errorMessage(error), activeSessionId: null });
68 }
69 throw error;
70 });
71 inFlight = { tabId: normalizedTabId, promise: request };
72 try {
73 return await request;
74 } finally {
75 if (inFlight?.promise === request) inFlight = null;
76 }
77 },
78 async ensureReady(tabId) {
79 const normalizedTabId = tabId.trim();
80 const current = get();
81 if (current.tabId === normalizedTabId && current.workspace && !current.loading) return current.workspace;
82 if (inFlight?.tabId === normalizedTabId) return inFlight.promise;
83 return get().syncWorkspace(normalizedTabId);
84 },
85 async createSession(tabId, relativePath = ".", shellId = "default") {
86 const normalizedTabId = tabId.trim();
87 set({ error: null });
88 try {
89 const workspace = await get().ensureReady(normalizedTabId);
90 if (!workspace?.available || workspace.readOnly) return null;
91 const session = await app.CreateTerminalForTab(normalizedTabId, relativePath, shellId);
92 set((state) => {
93 if (state.tabId !== normalizedTabId || !state.workspace) return {};
94 const sessions = state.workspace.sessions.some((item) => item.id === session.id)
95 ? state.workspace.sessions.map((item) => item.id === session.id ? session : item)
96 : [...state.workspace.sessions, session];
97 return { workspace: { ...state.workspace, sessions }, error: null, activeSessionId: session.id };
98 });
99 return session;
100 } catch (error) {
101 if (get().tabId === normalizedTabId) set({ error: errorMessage(error) });
102 throw error;
103 }
104 },
105 async write(tabId, sessionId, data) {
106 try {
107 await app.WriteTerminalForTab(tabId, sessionId, data);
108 } catch (error) {
109 const current = get();
110 if (current.tabId === tabId && current.workspace?.sessions.some((session) => session.id === sessionId)) {
111 set({ error: errorMessage(error) });
112 }
113 throw error;
114 }
115 },
116 async resize(tabId, sessionId, cols, rows) {
117 await app.ResizeTerminalForTab(tabId, sessionId, cols, rows);
118 },
119 async closeSession(tabId, sessionId) {
120 try {
121 await app.CloseTerminalForTab(tabId, sessionId);
122 forgetTerminalSession(sessionId);
123 set((state) => {
124 if (state.tabId !== tabId || !state.workspace) return {};
125 const sessions = state.workspace.sessions.filter((session) => session.id !== sessionId);
126 const activeSessionId = state.activeSessionId === sessionId
127 ? sessions.find((session) => session.running)?.id ?? sessions[0]?.id ?? null
128 : state.activeSessionId;
129 return { workspace: { ...state.workspace, sessions }, error: null, activeSessionId };
130 });
131 } catch (error) {
132 if (get().tabId === tabId) set({ error: errorMessage(error) });
133 throw error;
134 }
135 },
136 async renameSession(tabId, sessionId, title) {
137 try {
138 await app.RenameTerminalForTab(tabId, sessionId, title);
139 const current = get();
140 if (current.tabId !== tabId || !current.workspace) return;
141 const sessions = current.workspace.sessions.map((session) => session.id === sessionId ? { ...session, title } : session);
142 set({ workspace: { ...current.workspace, sessions }, error: null });
143 } catch (error) {
144 if (get().tabId === tabId) set({ error: errorMessage(error) });
145 throw error;
146 }
147 },
148 clearError: () => set({ error: null }),
149 setActiveSession: (activeSessionId) => set({ activeSessionId }),
150 }));
151
152 registerTerminalExitListener((event) => {
153 useTerminalStore.setState((state) => {
154 if (!state.workspace) return {};
155 if (event.removed) {
156 const sessions = state.workspace.sessions.filter((session) => session.id !== event.id);
157 const activeSessionId = state.activeSessionId === event.id
158 ? sessions.find((session) => session.running)?.id ?? sessions[0]?.id ?? null
159 : state.activeSessionId;
160 return { workspace: { ...state.workspace, sessions }, activeSessionId };
161 }
162 const sessions = state.workspace.sessions.map((session) => session.id === event.id
163 ? { ...session, running: false, exitCode: event.exitCode }
164 : session);
165 return { workspace: { ...state.workspace, sessions } };
166 });
167 });
168
169 export function resetTerminalStoreForTests(): void {
170 inFlight = null;
171 useTerminalStore.setState({ tabId: "", generation: 0, workspace: null, loading: false, error: null, activeSessionId: null });
172 }
173
173 lines TYPESCRIPT