farming-code 2.2.8 → 2.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -28
- package/README.zh_cn.md +42 -28
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1592 -741
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +54 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +35 -0
- package/backend/control-api.js +192 -17
- package/backend/executable-discovery.js +2 -0
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +354 -84
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +176 -17
- package/backend/workspace-directory.js +232 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-DWsa7DXG.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/farming-2/site.webmanifest +2 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1978 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -9,6 +9,25 @@ const {
|
|
|
9
9
|
const { terminalInputToPtyString } = require('./input-parts');
|
|
10
10
|
const { deriveTerminalStatus } = require('./terminal-status');
|
|
11
11
|
const { normalizeInteractiveTerminalEnv } = require('./agent-env');
|
|
12
|
+
const {
|
|
13
|
+
allocateNativePtyRuntimeGeneration,
|
|
14
|
+
formatNativePtyRuntimeEpoch,
|
|
15
|
+
} = require('./native-pty-controller-generation');
|
|
16
|
+
const storageLayout = require('./storage-layout');
|
|
17
|
+
const {
|
|
18
|
+
acknowledgeTerminalReducerData,
|
|
19
|
+
createTerminalReducerFlowControl,
|
|
20
|
+
ensureTerminalReducerFlowControl,
|
|
21
|
+
enqueueTerminalReducerData,
|
|
22
|
+
resetTerminalReducerFlowControl,
|
|
23
|
+
} = require('./terminal-reducer-flow-control');
|
|
24
|
+
const {
|
|
25
|
+
captureTerminalAttachCheckpoint,
|
|
26
|
+
} = require('./terminal-attach-checkpoint');
|
|
27
|
+
const {
|
|
28
|
+
acceptTerminalExitData,
|
|
29
|
+
waitForTerminalExitDataQuiescence,
|
|
30
|
+
} = require('./terminal-exit-quiescence');
|
|
12
31
|
|
|
13
32
|
const CONTROLLED_BASH_PROMPT = [
|
|
14
33
|
'\\[\\e[90m\\]│\\[\\e[0m\\] ',
|
|
@@ -145,8 +164,9 @@ function normalizeShellSessionOptions(options) {
|
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
class LocalSessionEngine extends SessionEngine {
|
|
148
|
-
constructor() {
|
|
167
|
+
constructor(options = {}) {
|
|
149
168
|
super();
|
|
169
|
+
this.configDir = options.configDir || storageLayout.farmingConfigDir();
|
|
150
170
|
this.sessions = new Map();
|
|
151
171
|
this.screenWorkerPool = new TerminalScreenWorkerPool({
|
|
152
172
|
size: 3,
|
|
@@ -162,9 +182,13 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
162
182
|
const normalized = normalizeShellSessionOptions(options);
|
|
163
183
|
const previewCols = normalized.cols || 80;
|
|
164
184
|
const previewRows = normalized.rows || 30;
|
|
185
|
+
const runtimeGeneration = await allocateNativePtyRuntimeGeneration(this.configDir);
|
|
186
|
+
const runtimeEpoch = formatNativePtyRuntimeEpoch(runtimeGeneration);
|
|
165
187
|
const screenWorker = await this.screenWorkerPool.acquire({
|
|
166
188
|
cols: previewCols,
|
|
167
189
|
rows: previewRows,
|
|
190
|
+
runtimeEpoch,
|
|
191
|
+
runtimeGeneration,
|
|
168
192
|
});
|
|
169
193
|
|
|
170
194
|
let ptyProcess;
|
|
@@ -190,6 +214,11 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
190
214
|
process: ptyProcess,
|
|
191
215
|
output: '',
|
|
192
216
|
outputSeq: 0,
|
|
217
|
+
stateRevision: 0,
|
|
218
|
+
runtimeEpoch,
|
|
219
|
+
stateProofAvailable: true,
|
|
220
|
+
reducerFlowControl: createTerminalReducerFlowControl(),
|
|
221
|
+
reducerCommitQueue: Promise.resolve(),
|
|
193
222
|
renderOutput: '',
|
|
194
223
|
previewText: '',
|
|
195
224
|
previewSnapshot: null,
|
|
@@ -216,7 +245,7 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
216
245
|
|
|
217
246
|
session.screenWorker.on('preview', ({ previewText, title, cols, rows, previewSnapshot }) => {
|
|
218
247
|
const current = this.sessions.get(session.id);
|
|
219
|
-
if (
|
|
248
|
+
if (current !== session) return;
|
|
220
249
|
|
|
221
250
|
current.previewText = previewText || '';
|
|
222
251
|
current.previewSnapshot = previewSnapshot || null;
|
|
@@ -227,7 +256,8 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
227
256
|
current.title = title;
|
|
228
257
|
this.emit('session-title', {
|
|
229
258
|
sessionId: current.id,
|
|
230
|
-
title: current.title
|
|
259
|
+
title: current.title,
|
|
260
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
231
261
|
});
|
|
232
262
|
}
|
|
233
263
|
|
|
@@ -237,184 +267,275 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
237
267
|
cols: current.previewCols,
|
|
238
268
|
rows: current.previewRows,
|
|
239
269
|
previewSnapshot: current.previewSnapshot,
|
|
270
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
240
271
|
});
|
|
241
272
|
});
|
|
242
273
|
|
|
243
274
|
session.screenWorker.on('error', (error) => {
|
|
244
|
-
this.
|
|
245
|
-
sessionId: session.id,
|
|
246
|
-
error: `Failed to update terminal screen state: ${error.message}`,
|
|
247
|
-
fatal: false
|
|
248
|
-
});
|
|
275
|
+
this.failSessionScreenState(session, error);
|
|
249
276
|
});
|
|
250
277
|
|
|
251
278
|
this.sessions.set(session.id, session);
|
|
252
279
|
this.emit('session-started', {
|
|
253
280
|
sessionId: session.id,
|
|
254
281
|
status: session.status,
|
|
255
|
-
startedAt: session.startedAt
|
|
282
|
+
startedAt: session.startedAt,
|
|
283
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
284
|
+
outputSeq: session.outputSeq,
|
|
285
|
+
stateRevision: session.stateRevision
|
|
256
286
|
});
|
|
257
287
|
|
|
258
|
-
ptyProcess.onData(
|
|
259
|
-
const current = this.sessions.get(session.id);
|
|
260
|
-
if (!current) return;
|
|
261
|
-
|
|
262
|
-
const busyState = parseShellBusyMarkers(data, current.terminalBusy, current.shellBusyMarkerPending);
|
|
263
|
-
current.shellBusyMarkerPending = busyState.pending;
|
|
264
|
-
if (busyState.markerSeen) {
|
|
265
|
-
const markerAt = Date.now();
|
|
266
|
-
current.terminalBusy = busyState.terminalBusy;
|
|
267
|
-
if (busyState.cwd) {
|
|
268
|
-
current.shellCwd = busyState.cwd;
|
|
269
|
-
}
|
|
270
|
-
if (busyState.exitCodeSeen) {
|
|
271
|
-
current.shellLastExitCode = busyState.lastExitCode;
|
|
272
|
-
}
|
|
273
|
-
if (busyState.shellEvent) {
|
|
274
|
-
current.shellLastEvent = busyState.shellEvent;
|
|
275
|
-
}
|
|
276
|
-
if (busyState.shellEvent === 'start') {
|
|
277
|
-
current.shellCommandStartedAt = markerAt;
|
|
278
|
-
}
|
|
279
|
-
if (busyState.commandTextSeen) {
|
|
280
|
-
current.shellCommand = busyState.shellCommand || '';
|
|
281
|
-
}
|
|
282
|
-
if (busyState.shellEvent === 'finish') {
|
|
283
|
-
const commandStartedAt = current.shellCommandStartedAt;
|
|
284
|
-
if (current.shellCommand) {
|
|
285
|
-
current.shellLastCommand = current.shellCommand;
|
|
286
|
-
}
|
|
287
|
-
if (typeof commandStartedAt === 'number') {
|
|
288
|
-
current.shellLastCommandStartedAt = commandStartedAt;
|
|
289
|
-
current.shellLastCommandFinishedAt = markerAt;
|
|
290
|
-
current.shellLastCommandDurationMs = Math.max(0, markerAt - commandStartedAt);
|
|
291
|
-
}
|
|
292
|
-
current.shellCommand = '';
|
|
293
|
-
current.shellCommandStartedAt = null;
|
|
294
|
-
}
|
|
295
|
-
this.emit('session-busy-state', {
|
|
296
|
-
sessionId: current.id,
|
|
297
|
-
terminalBusy: current.terminalBusy,
|
|
298
|
-
cwd: current.shellCwd || current.cwd,
|
|
299
|
-
lastExitCode: current.shellLastExitCode,
|
|
300
|
-
shellEvent: current.shellLastEvent,
|
|
301
|
-
shellCommand: current.shellCommand,
|
|
302
|
-
shellLastCommand: current.shellLastCommand,
|
|
303
|
-
shellCommandStartedAt: current.shellCommandStartedAt,
|
|
304
|
-
shellLastCommandStartedAt: current.shellLastCommandStartedAt,
|
|
305
|
-
shellLastCommandFinishedAt: current.shellLastCommandFinishedAt,
|
|
306
|
-
shellLastCommandDurationMs: current.shellLastCommandDurationMs,
|
|
307
|
-
statusMarkerSeen: busyState.statusMarkerSeen,
|
|
308
|
-
busyMarkerSeen: busyState.busyMarkerSeen,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
288
|
+
ptyProcess.onData(data => this.handleSessionData(session.id, data, session));
|
|
311
289
|
|
|
312
|
-
|
|
313
|
-
|
|
290
|
+
ptyProcess.onExit(({ code }) => {
|
|
291
|
+
void this.handleSessionExit(session, code);
|
|
292
|
+
});
|
|
314
293
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
294
|
+
return {
|
|
295
|
+
sessionId: session.id,
|
|
296
|
+
status: session.status
|
|
297
|
+
};
|
|
298
|
+
}
|
|
318
299
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
300
|
+
handleSessionExit(session, code) {
|
|
301
|
+
if (this.sessions.get(session.id) !== session || session.status === 'exited') {
|
|
302
|
+
return Promise.resolve();
|
|
303
|
+
}
|
|
304
|
+
if (session.exitFinalizationPromise) return session.exitFinalizationPromise;
|
|
305
|
+
const finalization = this.finalizeSessionExit(session, code);
|
|
306
|
+
session.exitFinalizationPromise = finalization;
|
|
307
|
+
return finalization;
|
|
308
|
+
}
|
|
322
309
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
310
|
+
async finalizeSessionExit(session, code) {
|
|
311
|
+
const quiesced = await waitForTerminalExitDataQuiescence(session, {
|
|
312
|
+
flushMs: this.terminalExitDataFlushMs,
|
|
313
|
+
isCurrent: () => this.sessions.get(session.id) === session,
|
|
314
|
+
});
|
|
315
|
+
if (!quiesced) return;
|
|
316
|
+
await Promise.resolve(session.reducerCommitQueue).catch(() => {});
|
|
317
|
+
if (this.sessions.get(session.id) !== session) return;
|
|
318
|
+
const finalCheckpoint = await captureTerminalAttachCheckpoint(session, { requireCurrentCut: true });
|
|
319
|
+
if (this.sessions.get(session.id) !== session) return;
|
|
320
|
+
|
|
321
|
+
session.status = 'exited';
|
|
322
|
+
session.exitedAt = Date.now();
|
|
323
|
+
resetTerminalReducerFlowControl(
|
|
324
|
+
ensureTerminalReducerFlowControl(session),
|
|
325
|
+
session.process,
|
|
326
|
+
);
|
|
327
|
+
if (finalCheckpoint) {
|
|
328
|
+
session.finalCheckpoint = Object.freeze({ ...finalCheckpoint });
|
|
329
|
+
session.renderOutput = finalCheckpoint.renderOutput;
|
|
330
|
+
session.previewText = finalCheckpoint.previewText;
|
|
331
|
+
session.previewSnapshot = finalCheckpoint.previewSnapshot;
|
|
332
|
+
session.previewCols = finalCheckpoint.cols;
|
|
333
|
+
session.previewRows = finalCheckpoint.rows;
|
|
334
|
+
session.title = finalCheckpoint.title || session.title;
|
|
335
|
+
} else {
|
|
336
|
+
this.failSessionScreenState(session, new Error('Unable to capture the exact final Terminal checkpoint'));
|
|
337
|
+
}
|
|
338
|
+
if (session.screenWorker) {
|
|
339
|
+
await session.screenWorker.dispose().catch(() => {});
|
|
340
|
+
session.screenWorker = null;
|
|
341
|
+
}
|
|
342
|
+
cleanupShellBusyIntegration(session.shellBusyIntegration);
|
|
343
|
+
|
|
344
|
+
this.emit('session-exited', {
|
|
345
|
+
sessionId: session.id,
|
|
346
|
+
code,
|
|
347
|
+
exitedAt: session.exitedAt,
|
|
348
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
349
|
+
outputSeq: session.outputSeq,
|
|
350
|
+
stateRevision: session.stateRevision,
|
|
351
|
+
stateProofAvailable: session.stateProofAvailable !== false,
|
|
352
|
+
});
|
|
353
|
+
if (finalCheckpoint) {
|
|
354
|
+
this.emit('session-preview', {
|
|
355
|
+
sessionId: session.id,
|
|
356
|
+
previewText: session.previewText,
|
|
357
|
+
cols: session.previewCols,
|
|
358
|
+
rows: session.previewRows,
|
|
359
|
+
previewSnapshot: session.previewSnapshot,
|
|
360
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
handleSessionData(sessionId, rawData, expectedSession = null) {
|
|
366
|
+
const current = this.sessions.get(sessionId);
|
|
367
|
+
if (!current || (expectedSession && current !== expectedSession) || current.stateProofAvailable === false) return;
|
|
368
|
+
if (!acceptTerminalExitData(current)) {
|
|
369
|
+
current.finalCheckpoint = null;
|
|
370
|
+
this.failSessionScreenState(current, new Error('PTY emitted data after its final checkpoint cut'));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const busyState = parseShellBusyMarkers(
|
|
375
|
+
rawData,
|
|
376
|
+
current.terminalBusy,
|
|
377
|
+
current.shellBusyMarkerPending,
|
|
378
|
+
);
|
|
379
|
+
current.shellBusyMarkerPending = busyState.pending;
|
|
380
|
+
if (busyState.markerSeen) {
|
|
381
|
+
const markerAt = Date.now();
|
|
382
|
+
current.terminalBusy = busyState.terminalBusy;
|
|
383
|
+
if (busyState.cwd) current.shellCwd = busyState.cwd;
|
|
384
|
+
if (busyState.exitCodeSeen) current.shellLastExitCode = busyState.lastExitCode;
|
|
385
|
+
if (busyState.shellEvent) current.shellLastEvent = busyState.shellEvent;
|
|
386
|
+
if (busyState.shellEvent === 'start') current.shellCommandStartedAt = markerAt;
|
|
387
|
+
if (busyState.commandTextSeen) current.shellCommand = busyState.shellCommand || '';
|
|
388
|
+
if (busyState.shellEvent === 'finish') {
|
|
389
|
+
const commandStartedAt = current.shellCommandStartedAt;
|
|
390
|
+
if (current.shellCommand) current.shellLastCommand = current.shellCommand;
|
|
391
|
+
if (typeof commandStartedAt === 'number') {
|
|
392
|
+
current.shellLastCommandStartedAt = commandStartedAt;
|
|
393
|
+
current.shellLastCommandFinishedAt = markerAt;
|
|
394
|
+
current.shellLastCommandDurationMs = Math.max(0, markerAt - commandStartedAt);
|
|
395
|
+
}
|
|
396
|
+
current.shellCommand = '';
|
|
397
|
+
current.shellCommandStartedAt = null;
|
|
330
398
|
}
|
|
399
|
+
this.emit('session-busy-state', {
|
|
400
|
+
sessionId: current.id,
|
|
401
|
+
terminalBusy: current.terminalBusy,
|
|
402
|
+
cwd: current.shellCwd || current.cwd,
|
|
403
|
+
lastExitCode: current.shellLastExitCode,
|
|
404
|
+
shellEvent: current.shellLastEvent,
|
|
405
|
+
shellCommand: current.shellCommand,
|
|
406
|
+
shellLastCommand: current.shellLastCommand,
|
|
407
|
+
shellCommandStartedAt: current.shellCommandStartedAt,
|
|
408
|
+
shellLastCommandStartedAt: current.shellLastCommandStartedAt,
|
|
409
|
+
shellLastCommandFinishedAt: current.shellLastCommandFinishedAt,
|
|
410
|
+
shellLastCommandDurationMs: current.shellLastCommandDurationMs,
|
|
411
|
+
statusMarkerSeen: busyState.statusMarkerSeen,
|
|
412
|
+
busyMarkerSeen: busyState.busyMarkerSeen,
|
|
413
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
331
416
|
|
|
332
|
-
|
|
417
|
+
const data = busyState.data;
|
|
418
|
+
if (!data) return;
|
|
419
|
+
const reducerDelivery = enqueueTerminalReducerData(
|
|
420
|
+
ensureTerminalReducerFlowControl(current),
|
|
421
|
+
current.process,
|
|
422
|
+
data,
|
|
423
|
+
);
|
|
424
|
+
if (reducerDelivery.error) {
|
|
425
|
+
this.failSessionScreenState(current, reducerDelivery.error);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
current.outputSeq += 1;
|
|
429
|
+
current.stateRevision += 1;
|
|
430
|
+
const outputSeq = current.outputSeq;
|
|
431
|
+
const stateRevision = current.stateRevision;
|
|
432
|
+
current.output = `${current.output}${data}`.slice(-10000);
|
|
433
|
+
current.lastActivityAt = Date.now();
|
|
434
|
+
|
|
435
|
+
const fallbackTitle = extractLatestTerminalTitle(data);
|
|
436
|
+
if (fallbackTitle && fallbackTitle !== current.title) {
|
|
437
|
+
current.title = fallbackTitle;
|
|
438
|
+
this.emit('session-title', {
|
|
333
439
|
sessionId: current.id,
|
|
440
|
+
title: current.title,
|
|
441
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const commit = current.screenWorker.append(data, stateRevision, outputSeq);
|
|
446
|
+
current.reducerCommitQueue = current.reducerCommitQueue.then(() => commit).then(() => {
|
|
447
|
+
const latest = this.sessions.get(current.id);
|
|
448
|
+
if (latest !== current || latest.stateProofAvailable === false) return;
|
|
449
|
+
const flowError = acknowledgeTerminalReducerData(
|
|
450
|
+
ensureTerminalReducerFlowControl(latest),
|
|
451
|
+
latest.process,
|
|
452
|
+
reducerDelivery.bytes,
|
|
453
|
+
);
|
|
454
|
+
if (flowError) {
|
|
455
|
+
this.failSessionScreenState(latest, flowError);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.emit('session-output', {
|
|
459
|
+
sessionId: latest.id,
|
|
334
460
|
data,
|
|
335
|
-
|
|
461
|
+
runtimeEpoch: latest.runtimeEpoch,
|
|
462
|
+
outputSeq,
|
|
463
|
+
stateRevision,
|
|
336
464
|
});
|
|
337
465
|
this.emit('session-activity', {
|
|
338
|
-
sessionId:
|
|
339
|
-
lastActivityAt:
|
|
466
|
+
sessionId: latest.id,
|
|
467
|
+
lastActivityAt: latest.lastActivityAt,
|
|
468
|
+
runtimeEpoch: latest.runtimeEpoch,
|
|
340
469
|
});
|
|
470
|
+
}).catch(error => this.failSessionScreenState(current, error));
|
|
471
|
+
}
|
|
341
472
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
previewSnapshot: current.previewSnapshot,
|
|
353
|
-
title: current.title
|
|
354
|
-
})).then((screenState) => {
|
|
355
|
-
const latest = this.sessions.get(session.id);
|
|
356
|
-
if (!latest) return;
|
|
357
|
-
|
|
358
|
-
latest.status = 'exited';
|
|
359
|
-
latest.exitedAt = Date.now();
|
|
360
|
-
latest.renderOutput = screenState.renderOutput || latest.renderOutput;
|
|
361
|
-
latest.previewText = screenState.previewText || latest.previewText || latest.output.slice(-2000);
|
|
362
|
-
latest.previewSnapshot = screenState.previewSnapshot || latest.previewSnapshot;
|
|
363
|
-
latest.previewCols = screenState.cols || latest.previewCols;
|
|
364
|
-
latest.previewRows = screenState.rows || latest.previewRows;
|
|
365
|
-
latest.title = screenState.title || latest.title;
|
|
366
|
-
if (latest.screenWorker) {
|
|
367
|
-
latest.screenWorker.dispose().catch(() => {});
|
|
368
|
-
latest.screenWorker = null;
|
|
369
|
-
}
|
|
370
|
-
cleanupShellBusyIntegration(latest.shellBusyIntegration);
|
|
371
|
-
|
|
372
|
-
this.emit('session-exited', {
|
|
373
|
-
sessionId: latest.id,
|
|
374
|
-
code,
|
|
375
|
-
exitedAt: latest.exitedAt
|
|
376
|
-
});
|
|
377
|
-
this.emit('session-preview', {
|
|
378
|
-
sessionId: latest.id,
|
|
379
|
-
previewText: latest.previewText,
|
|
380
|
-
cols: latest.previewCols,
|
|
381
|
-
rows: latest.previewRows,
|
|
382
|
-
previewSnapshot: latest.previewSnapshot,
|
|
383
|
-
});
|
|
384
|
-
});
|
|
473
|
+
failSessionScreenState(session, error) {
|
|
474
|
+
const current = this.sessions.get(session.id);
|
|
475
|
+
if (current !== session || current.stateProofAvailable === false) return;
|
|
476
|
+
current.stateProofAvailable = false;
|
|
477
|
+
const message = error instanceof Error ? error.message : String(error || 'unknown reducer failure');
|
|
478
|
+
this.emit('session-error', {
|
|
479
|
+
sessionId: current.id,
|
|
480
|
+
error: `Terminal state reducer failed: ${message}`,
|
|
481
|
+
fatal: true,
|
|
482
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
385
483
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
484
|
+
try {
|
|
485
|
+
current.process?.kill();
|
|
486
|
+
} catch {
|
|
487
|
+
// The runtime cannot continue safely without its authoritative reducer.
|
|
488
|
+
}
|
|
391
489
|
}
|
|
392
490
|
|
|
393
|
-
async sendInput(sessionId, input) {
|
|
491
|
+
async sendInput(sessionId, input, options = {}) {
|
|
394
492
|
const session = this.sessions.get(sessionId);
|
|
395
|
-
if (!session || !session.process) {
|
|
493
|
+
if (!session || !session.process || session.status === 'exited' || session.exitFinalizing === true) {
|
|
396
494
|
throw new Error('Session not available');
|
|
397
495
|
}
|
|
398
496
|
|
|
497
|
+
if (options.expectedRuntimeEpoch && options.expectedRuntimeEpoch !== session.runtimeEpoch) {
|
|
498
|
+
return { status: 'input-rejected', reason: 'runtime-epoch-mismatch' };
|
|
499
|
+
}
|
|
399
500
|
session.process.write(terminalInputToPtyString(input));
|
|
400
501
|
session.lastActivityAt = Date.now();
|
|
401
502
|
this.emit('session-activity', {
|
|
402
503
|
sessionId,
|
|
403
|
-
lastActivityAt: session.lastActivityAt
|
|
504
|
+
lastActivityAt: session.lastActivityAt,
|
|
505
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
404
506
|
});
|
|
507
|
+
return { sent: true };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async interruptSession(sessionId, input = '\x03', options = {}) {
|
|
511
|
+
return this.sendInput(sessionId, input, options);
|
|
405
512
|
}
|
|
406
513
|
|
|
407
|
-
async
|
|
408
|
-
|
|
514
|
+
async getSessionAttachCheckpoint(sessionId) {
|
|
515
|
+
const session = this.sessions.get(sessionId);
|
|
516
|
+
if (!session) return null;
|
|
517
|
+
const checkpoint = await captureTerminalAttachCheckpoint(session);
|
|
518
|
+
return this.sessions.get(sessionId) === session ? checkpoint : null;
|
|
409
519
|
}
|
|
410
520
|
|
|
411
521
|
async resizeSession(sessionId, cols, rows) {
|
|
412
522
|
const session = this.sessions.get(sessionId);
|
|
413
523
|
if (!session || !session.process || !session.process.resize) {
|
|
414
|
-
return { resized: false };
|
|
524
|
+
return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
|
|
525
|
+
}
|
|
526
|
+
if (session.status === 'exited' || session.exitFinalizing === true) {
|
|
527
|
+
return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
|
|
415
528
|
}
|
|
416
|
-
|
|
417
|
-
|
|
529
|
+
|
|
530
|
+
if (cols === session.previewCols && rows === session.previewRows) {
|
|
531
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
532
|
+
return { status: 'resize-rejected', reason: 'session-replaced', resized: false };
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
status: 'resize-committed',
|
|
536
|
+
resized: true,
|
|
537
|
+
unchanged: true,
|
|
538
|
+
};
|
|
418
539
|
}
|
|
419
540
|
|
|
420
541
|
try {
|
|
@@ -423,96 +544,191 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
423
544
|
if (error && String(error.message || '').includes('EBADF')) {
|
|
424
545
|
session.status = 'exited';
|
|
425
546
|
session.exitedAt = session.exitedAt || Date.now();
|
|
426
|
-
return { resized: false };
|
|
547
|
+
return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
|
|
427
548
|
}
|
|
428
|
-
|
|
549
|
+
return {
|
|
550
|
+
status: 'resize-rejected',
|
|
551
|
+
reason: 'pty-resize-failed',
|
|
552
|
+
resized: false,
|
|
553
|
+
error: error instanceof Error ? error.message : String(error || 'unknown PTY resize failure'),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
557
|
+
return { status: 'resize-rejected', reason: 'session-replaced', resized: false };
|
|
429
558
|
}
|
|
559
|
+
session.stateRevision += 1;
|
|
560
|
+
const stateRevision = session.stateRevision;
|
|
561
|
+
const outputSeq = session.outputSeq;
|
|
562
|
+
const runtimeEpoch = session.runtimeEpoch;
|
|
430
563
|
session.previewCols = cols || session.previewCols;
|
|
431
564
|
session.previewRows = rows || session.previewRows;
|
|
432
565
|
if (!session.screenWorker) {
|
|
433
|
-
|
|
566
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
567
|
+
return { status: 'resize-rejected', reason: 'session-replaced', resized: false };
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
status: 'resize-committed',
|
|
571
|
+
resized: true,
|
|
572
|
+
unchanged: false,
|
|
573
|
+
};
|
|
434
574
|
}
|
|
435
575
|
|
|
436
|
-
|
|
437
|
-
|
|
576
|
+
const reducerCommit = session.screenWorker.resize(cols, rows, stateRevision);
|
|
577
|
+
const publishedCommit = session.reducerCommitQueue.then(() => reducerCommit).then((screenState) => {
|
|
578
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
579
|
+
throw new Error('Terminal session was replaced during resize');
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
screenState.runtimeEpoch !== runtimeEpoch ||
|
|
583
|
+
screenState.outputSeq !== outputSeq ||
|
|
584
|
+
screenState.stateRevision !== stateRevision
|
|
585
|
+
) {
|
|
586
|
+
throw new Error('Terminal screen resize returned a non-authoritative state cut');
|
|
587
|
+
}
|
|
438
588
|
session.previewText = screenState.previewText || '';
|
|
439
589
|
session.previewSnapshot = screenState.previewSnapshot || session.previewSnapshot;
|
|
440
|
-
session.renderOutput = screenState.renderOutput
|
|
590
|
+
session.renderOutput = typeof screenState.renderOutput === 'string'
|
|
591
|
+
? screenState.renderOutput
|
|
592
|
+
: session.renderOutput;
|
|
441
593
|
session.previewCols = screenState.cols || cols || session.previewCols;
|
|
442
594
|
session.previewRows = screenState.rows || rows || session.previewRows;
|
|
443
595
|
if (screenState.title && screenState.title !== session.title) {
|
|
444
596
|
session.title = screenState.title;
|
|
445
597
|
this.emit('session-title', {
|
|
446
598
|
sessionId,
|
|
447
|
-
title: session.title
|
|
599
|
+
title: session.title,
|
|
600
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
448
601
|
});
|
|
449
602
|
}
|
|
450
|
-
|
|
451
|
-
this.emit('session-error', {
|
|
603
|
+
this.emit('session-transition', {
|
|
452
604
|
sessionId,
|
|
453
|
-
|
|
454
|
-
|
|
605
|
+
kind: 'resize',
|
|
606
|
+
data: '',
|
|
607
|
+
runtimeEpoch: screenState.runtimeEpoch,
|
|
608
|
+
outputSeq: screenState.outputSeq,
|
|
609
|
+
stateRevision: screenState.stateRevision,
|
|
610
|
+
cols: screenState.cols,
|
|
611
|
+
rows: screenState.rows
|
|
455
612
|
});
|
|
613
|
+
return screenState;
|
|
614
|
+
});
|
|
615
|
+
session.reducerCommitQueue = publishedCommit.catch(error => {
|
|
616
|
+
this.failSessionScreenState(session, error);
|
|
617
|
+
});
|
|
618
|
+
try {
|
|
619
|
+
await publishedCommit;
|
|
620
|
+
} catch {
|
|
621
|
+
return { status: 'resize-rejected', reason: 'screen-reducer-failed', resized: false };
|
|
456
622
|
}
|
|
457
|
-
|
|
623
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
624
|
+
return { status: 'resize-rejected', reason: 'session-replaced', resized: false };
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
status: 'resize-committed',
|
|
628
|
+
resized: true,
|
|
629
|
+
unchanged: false,
|
|
630
|
+
runtimeEpoch,
|
|
631
|
+
outputSeq,
|
|
632
|
+
stateRevision,
|
|
633
|
+
cols,
|
|
634
|
+
rows,
|
|
635
|
+
};
|
|
458
636
|
}
|
|
459
637
|
|
|
460
|
-
async clearBuffer(sessionId) {
|
|
638
|
+
async clearBuffer(sessionId, options = {}) {
|
|
461
639
|
const session = this.sessions.get(sessionId);
|
|
462
|
-
if (!session) {
|
|
640
|
+
if (!session || session.status === 'exited' || session.exitFinalizing === true) {
|
|
463
641
|
return { cleared: false };
|
|
464
642
|
}
|
|
643
|
+
if (options.expectedRuntimeEpoch && options.expectedRuntimeEpoch !== session.runtimeEpoch) {
|
|
644
|
+
return { cleared: false, reason: 'runtime-epoch-mismatch' };
|
|
645
|
+
}
|
|
465
646
|
|
|
466
647
|
session.output = '';
|
|
467
|
-
session.
|
|
648
|
+
session.stateRevision += 1;
|
|
649
|
+
const stateRevision = session.stateRevision;
|
|
650
|
+
const outputSeq = session.outputSeq;
|
|
651
|
+
const runtimeEpoch = session.runtimeEpoch;
|
|
468
652
|
session.renderOutput = '';
|
|
469
653
|
session.previewText = '';
|
|
470
654
|
session.previewSnapshot = null;
|
|
471
655
|
session.lastActivityAt = Date.now();
|
|
472
656
|
|
|
657
|
+
let exactState = {
|
|
658
|
+
renderOutput: '',
|
|
659
|
+
runtimeEpoch,
|
|
660
|
+
outputSeq,
|
|
661
|
+
stateRevision,
|
|
662
|
+
cols: session.previewCols,
|
|
663
|
+
rows: session.previewRows,
|
|
664
|
+
};
|
|
473
665
|
if (session.screenWorker) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
666
|
+
const reducerCommit = session.screenWorker.clear(stateRevision, outputSeq);
|
|
667
|
+
const publishedCommit = session.reducerCommitQueue.then(() => reducerCommit).then((screenState) => {
|
|
668
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
669
|
+
throw new Error('Terminal session was replaced during clear');
|
|
670
|
+
}
|
|
671
|
+
if (
|
|
672
|
+
screenState.runtimeEpoch !== runtimeEpoch ||
|
|
673
|
+
screenState.outputSeq !== outputSeq ||
|
|
674
|
+
screenState.stateRevision !== stateRevision
|
|
675
|
+
) {
|
|
676
|
+
throw new Error('Terminal screen clear returned a non-authoritative state cut');
|
|
677
|
+
}
|
|
678
|
+
session.renderOutput = typeof screenState.renderOutput === 'string' ? screenState.renderOutput : '';
|
|
477
679
|
session.previewText = screenState.previewText || '';
|
|
478
680
|
session.previewSnapshot = screenState.previewSnapshot || null;
|
|
479
681
|
session.previewCols = screenState.cols || session.previewCols;
|
|
480
682
|
session.previewRows = screenState.rows || session.previewRows;
|
|
683
|
+
exactState = screenState;
|
|
481
684
|
if (screenState.title && screenState.title !== session.title) {
|
|
482
685
|
session.title = screenState.title;
|
|
483
|
-
this.emit('session-title', {
|
|
484
|
-
sessionId,
|
|
485
|
-
title: session.title
|
|
486
|
-
});
|
|
686
|
+
this.emit('session-title', { sessionId, title: session.title, runtimeEpoch: session.runtimeEpoch });
|
|
487
687
|
}
|
|
488
|
-
|
|
489
|
-
this.emit('session-error', {
|
|
688
|
+
this.emit('session-transition', {
|
|
490
689
|
sessionId,
|
|
491
|
-
|
|
492
|
-
|
|
690
|
+
kind: 'clear',
|
|
691
|
+
data: '\x1b[2J\x1b[3J\x1b[H',
|
|
692
|
+
runtimeEpoch: screenState.runtimeEpoch,
|
|
693
|
+
outputSeq: screenState.outputSeq,
|
|
694
|
+
stateRevision: screenState.stateRevision,
|
|
695
|
+
cols: screenState.cols,
|
|
696
|
+
rows: screenState.rows
|
|
493
697
|
});
|
|
698
|
+
return screenState;
|
|
699
|
+
});
|
|
700
|
+
session.reducerCommitQueue = publishedCommit.catch(error => {
|
|
701
|
+
this.failSessionScreenState(session, error);
|
|
702
|
+
});
|
|
703
|
+
try {
|
|
704
|
+
await publishedCommit;
|
|
705
|
+
} catch {
|
|
706
|
+
return { cleared: false };
|
|
494
707
|
}
|
|
495
708
|
}
|
|
496
|
-
|
|
497
|
-
this.emit('session-sync', {
|
|
498
|
-
sessionId,
|
|
499
|
-
output: session.renderOutput || session.output,
|
|
500
|
-
outputSeq: session.outputSeq,
|
|
501
|
-
replaceLive: true
|
|
502
|
-
});
|
|
709
|
+
if (this.sessions.get(sessionId) !== session) return { cleared: false, reason: 'session-replaced' };
|
|
503
710
|
this.emit('session-preview', {
|
|
504
711
|
sessionId,
|
|
505
712
|
previewText: session.previewText,
|
|
506
713
|
cols: session.previewCols,
|
|
507
714
|
rows: session.previewRows,
|
|
508
715
|
previewSnapshot: session.previewSnapshot,
|
|
509
|
-
title: session.title
|
|
716
|
+
title: session.title,
|
|
717
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
510
718
|
});
|
|
511
719
|
this.emit('session-activity', {
|
|
512
720
|
sessionId,
|
|
513
|
-
lastActivityAt: session.lastActivityAt
|
|
721
|
+
lastActivityAt: session.lastActivityAt,
|
|
722
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
514
723
|
});
|
|
515
|
-
return {
|
|
724
|
+
return {
|
|
725
|
+
cleared: true,
|
|
726
|
+
runtimeEpoch: exactState.runtimeEpoch,
|
|
727
|
+
outputSeq: exactState.outputSeq,
|
|
728
|
+
stateRevision: exactState.stateRevision,
|
|
729
|
+
cols: exactState.cols,
|
|
730
|
+
rows: exactState.rows,
|
|
731
|
+
};
|
|
516
732
|
}
|
|
517
733
|
|
|
518
734
|
async killSession(sessionId) {
|
|
@@ -527,7 +743,7 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
527
743
|
|
|
528
744
|
const killTimer = setTimeout(() => {
|
|
529
745
|
const latest = this.sessions.get(sessionId);
|
|
530
|
-
if (
|
|
746
|
+
if (latest !== session || latest.status === 'exited' || !latest.process) {
|
|
531
747
|
return;
|
|
532
748
|
}
|
|
533
749
|
latest.process.kill('SIGKILL');
|
|
@@ -538,23 +754,31 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
538
754
|
async getSessionState(sessionId) {
|
|
539
755
|
const session = this.sessions.get(sessionId);
|
|
540
756
|
if (!session) return null;
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
const
|
|
757
|
+
const snapshotOutput = session.output;
|
|
758
|
+
const fallbackPreviewText = session.previewText;
|
|
759
|
+
const fallbackPreviewSnapshot = session.previewSnapshot;
|
|
760
|
+
const fallbackPreviewCols = session.previewCols;
|
|
761
|
+
const fallbackPreviewRows = session.previewRows;
|
|
762
|
+
const fallbackTitle = session.title;
|
|
763
|
+
const stateProofAvailable = session.stateProofAvailable !== false;
|
|
764
|
+
const checkpoint = stateProofAvailable ? await captureTerminalAttachCheckpoint(session) : null;
|
|
765
|
+
if (this.sessions.get(sessionId) !== session) return null;
|
|
766
|
+
const title = checkpoint ? checkpoint.title : fallbackTitle;
|
|
767
|
+
const previewText = checkpoint ? checkpoint.previewText : (stateProofAvailable ? fallbackPreviewText : '');
|
|
547
768
|
|
|
548
769
|
return {
|
|
549
770
|
sessionId: session.id,
|
|
550
771
|
status: session.status,
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
772
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
773
|
+
output: snapshotOutput,
|
|
774
|
+
outputSeq: checkpoint?.outputSeq ?? null,
|
|
775
|
+
stateRevision: checkpoint?.stateRevision ?? null,
|
|
776
|
+
stateProofAvailable,
|
|
777
|
+
renderOutput: checkpoint ? checkpoint.renderOutput : (stateProofAvailable ? snapshotOutput : ''),
|
|
554
778
|
previewText,
|
|
555
|
-
previewSnapshot:
|
|
556
|
-
previewCols:
|
|
557
|
-
previewRows:
|
|
779
|
+
previewSnapshot: checkpoint ? checkpoint.previewSnapshot : (stateProofAvailable ? fallbackPreviewSnapshot : null),
|
|
780
|
+
previewCols: checkpoint ? checkpoint.cols : fallbackPreviewCols,
|
|
781
|
+
previewRows: checkpoint ? checkpoint.rows : fallbackPreviewRows,
|
|
558
782
|
title,
|
|
559
783
|
lastActivityAt: session.lastActivityAt,
|
|
560
784
|
startedAt: session.startedAt,
|
|
@@ -593,6 +817,7 @@ class LocalSessionEngine extends SessionEngine {
|
|
|
593
817
|
const screenState = session.screenWorker
|
|
594
818
|
? await session.screenWorker.getState({ includeRenderOutput: false }).catch(() => null)
|
|
595
819
|
: null;
|
|
820
|
+
if (this.sessions.get(sessionId) !== session) return '';
|
|
596
821
|
return (screenState && screenState.previewText) || session.previewText;
|
|
597
822
|
}
|
|
598
823
|
|