返回 DeepSeek-Reasonix
terminalEvents.ts
根目录 / desktop / frontend / src / lib / terminalEvents.ts
1 import { onTerminalExit, onTerminalOutput, type TerminalExitEvent, type TerminalOutputEvent } from "./bridge";
2 import { createSubscriptionScope } from "./subscriptionScope";
3 import { desktopHost } from "./desktopHost";
4
5 const MAX_HISTORY_BYTES = 1024 * 1024;
6
7 type SequencedTerminalSink = (data: Uint8Array, sequence: number) => void;
8
9 const sinks = new Map<string, SequencedTerminalSink>();
10 const exitListeners = new Set<(event: TerminalExitEvent) => void>();
11 const gapListeners = new Set<(ids: string[]) => void>();
12 const history = new Map<string, Uint8Array[]>();
13 const historyBytes = new Map<string, number>();
14 const nextSequence = new Map<string, number>();
15 let bridge: { users: number; scope: ReturnType<typeof createSubscriptionScope> } | null = null;
16
17 function decodeBase64(value: string): Uint8Array {
18 if (typeof atob !== "function") return new Uint8Array();
19 const binary = atob(value);
20 const bytes = new Uint8Array(binary.length);
21 for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
22 return bytes;
23 }
24
25 function deliverOutput(event: TerminalOutputEvent): void {
26 const bytes = decodeBase64(event.data);
27 if (bytes.byteLength === 0) return;
28 const sequence = nextSequence.get(event.id) ?? 0;
29 nextSequence.set(event.id, sequence + 1);
30 const queue = history.get(event.id) ?? [];
31 queue.push(bytes);
32 let total = (historyBytes.get(event.id) ?? 0) + bytes.byteLength;
33 while (total > MAX_HISTORY_BYTES && queue.length > 0) {
34 total -= queue.shift()?.byteLength ?? 0;
35 }
36 history.set(event.id, queue);
37 historyBytes.set(event.id, total);
38 sinks.get(event.id)?.(bytes, sequence);
39 }
40
41 function deliverExit(event: TerminalExitEvent): void {
42 if (event.removed) forgetTerminalSession(event.id);
43 exitListeners.forEach((listener) => listener(event));
44 }
45
46 export function startTerminalEventBridge(): () => void {
47 if (!bridge) {
48 const scope = createSubscriptionScope();
49 scope.listen(onTerminalOutput, deliverOutput);
50 scope.listen(onTerminalExit, deliverExit);
51 scope.listen(callback => desktopHost().events.on("desktop:resync", callback), (event: unknown) => {
52 const reason = (event as { reason?: string } | undefined)?.reason;
53 if (history.size || reason === "gap" || reason === "subscription") {
54 for (const listener of gapListeners) listener(reason === "generation" ? [...history.keys()] : []);
55 }
56 });
57 bridge = { users: 0, scope };
58 }
59 const owned = bridge;
60 owned.users += 1;
61 let released = false;
62 return () => {
63 if (released) return;
64 released = true;
65 owned.users -= 1;
66 if (owned.users !== 0) return;
67 owned.scope.dispose();
68 if (bridge === owned) bridge = null;
69 };
70 }
71
72 export function registerTerminalOutputSink(id: string, sink: SequencedTerminalSink): readonly [
73 unregister: () => void,
74 history: () => readonly [chunks: readonly Uint8Array[], nextSequence: number],
75 ] {
76 sinks.set(id, sink);
77 return [
78 () => {
79 if (sinks.get(id) === sink) sinks.delete(id);
80 },
81 () => [history.get(id) ?? [], nextSequence.get(id) ?? 0],
82 ];
83 }
84
85 export function forgetTerminalSession(id: string): void {
86 history.delete(id);
87 historyBytes.delete(id);
88 nextSequence.delete(id);
89 }
90
91 export function registerTerminalExitListener(listener: (event: TerminalExitEvent) => void): () => void {
92 exitListeners.add(listener);
93 return () => exitListeners.delete(listener);
94 }
95
96 export function registerTerminalGapListener(listener: (ids: string[]) => void): () => void {
97 gapListeners.add(listener);
98 return () => gapListeners.delete(listener);
99 }
100
101 export function __resetTerminalEventBus(): void {
102 sinks.clear();
103 history.clear();
104 historyBytes.clear();
105 nextSequence.clear();
106 bridge?.scope.dispose();
107 bridge = null;
108 }
109
110 export const terminalEventBufferLimit = MAX_HISTORY_BYTES;
111
111 lines TYPESCRIPT