pi-ui-extend 0.1.70 → 0.1.72
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/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +39 -1
- package/dist/app/commands/command-host.d.ts +7 -0
- package/dist/app/commands/command-host.js +10 -1
- package/dist/app/commands/command-model-actions.d.ts +1 -1
- package/dist/app/commands/command-model-actions.js +36 -2
- package/dist/app/commands/command-navigation-actions.d.ts +1 -1
- package/dist/app/commands/command-navigation-actions.js +40 -6
- package/dist/app/commands/command-session-actions.d.ts +1 -1
- package/dist/app/commands/command-session-actions.js +56 -0
- package/dist/app/commands/shell-controller.d.ts +9 -2
- package/dist/app/commands/shell-controller.js +32 -21
- package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
- package/dist/app/extensions/extension-actions-controller.js +25 -7
- package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
- package/dist/app/extensions/extension-ui-controller.js +104 -64
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-action-controller.d.ts +4 -1
- package/dist/app/input/input-action-controller.js +68 -27
- package/dist/app/input/input-controller.d.ts +3 -0
- package/dist/app/input/input-controller.js +45 -1
- package/dist/app/input/input-paste-handler.d.ts +3 -0
- package/dist/app/input/input-paste-handler.js +21 -12
- package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
- package/dist/app/input/prompt-enhancer-controller.js +11 -5
- package/dist/app/input/voice-controller.d.ts +4 -0
- package/dist/app/input/voice-controller.js +76 -35
- package/dist/app/popup/popup-action-controller.d.ts +3 -0
- package/dist/app/popup/popup-action-controller.js +43 -8
- package/dist/app/rendering/render-controller.js +1 -0
- package/dist/app/rendering/status-line-renderer.d.ts +3 -1
- package/dist/app/rendering/status-line-renderer.js +11 -0
- package/dist/app/screen/mouse-controller.d.ts +5 -1
- package/dist/app/screen/mouse-controller.js +14 -2
- package/dist/app/session/queued-message-controller.d.ts +5 -4
- package/dist/app/session/queued-message-controller.js +32 -27
- package/dist/app/session/request-history.d.ts +2 -0
- package/dist/app/session/request-history.js +22 -15
- package/dist/app/session/session-event-controller.js +28 -8
- package/dist/app/session/session-history.js +8 -2
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
- package/dist/app/session/session-lifecycle-controller.js +91 -19
- package/dist/app/session/tabs-controller.d.ts +26 -0
- package/dist/app/session/tabs-controller.js +469 -96
- package/dist/app/terminal/terminal-controller.d.ts +4 -0
- package/dist/app/terminal/terminal-controller.js +38 -11
- package/dist/app/types.d.ts +10 -0
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +37 -9
- package/dist/input-editor.d.ts +2 -0
- package/dist/input-editor.js +17 -0
- package/docs/concurrency.md +76 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
- package/external/pi-tools-suite/src/todo/index.ts +2 -2
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir, open as openFile, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
@@ -13,6 +13,7 @@ const BACKGROUND_PREWARM_TAB_LIMIT = 2;
|
|
|
13
13
|
const TAB_ATTENTION_BLINK_KEY = "tab-attention";
|
|
14
14
|
const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
|
|
15
15
|
const DEFAULT_SESSION_TITLE_PATTERN = /^(?:session )?[0-9a-f]{8}$/iu;
|
|
16
|
+
const SESSION_HEADER_SCAN_MAX_BYTES = 64 * 1024;
|
|
16
17
|
const SESSION_TITLE_HEAD_SCAN_MAX_BYTES = 256 * 1024;
|
|
17
18
|
const SESSION_TITLE_SCAN_MAX_BYTES = 2 * 1024 * 1024;
|
|
18
19
|
export class AppTabsController {
|
|
@@ -29,9 +30,18 @@ export class AppTabsController {
|
|
|
29
30
|
sessionViewsByTabId = new Map();
|
|
30
31
|
scrollStatesByTabId = new Map();
|
|
31
32
|
tabIdsNeedingHistoryReload = new Set();
|
|
33
|
+
historyInvalidationGenerationByTabId = new Map();
|
|
34
|
+
runtimeOwnershipGenerationByTabId = new Map();
|
|
35
|
+
orphanRuntimeDisposals = new WeakSet();
|
|
36
|
+
lifecycleMutationQueue = [];
|
|
32
37
|
activeTabId;
|
|
33
38
|
pendingActiveTabId;
|
|
34
39
|
historyLoadGeneration = 0;
|
|
40
|
+
historyInvalidationGeneration = 0;
|
|
41
|
+
runtimeOwnershipGeneration = 0;
|
|
42
|
+
lifecycleGeneration = 0;
|
|
43
|
+
lifecycleMutationRunning = false;
|
|
44
|
+
saveTabsWriteTail = Promise.resolve();
|
|
35
45
|
restored = false;
|
|
36
46
|
retentionCleanupRunning = false;
|
|
37
47
|
retentionCleanupScheduled = false;
|
|
@@ -40,6 +50,48 @@ export class AppTabsController {
|
|
|
40
50
|
constructor(host) {
|
|
41
51
|
this.host = host;
|
|
42
52
|
}
|
|
53
|
+
runLifecycleMutation(operation) {
|
|
54
|
+
return new Promise((resolveOperation, rejectOperation) => {
|
|
55
|
+
const run = () => {
|
|
56
|
+
if (!this.host.isRunning()) {
|
|
57
|
+
resolveOperation(undefined);
|
|
58
|
+
const next = this.lifecycleMutationQueue.shift();
|
|
59
|
+
if (next)
|
|
60
|
+
queueMicrotask(next);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.lifecycleMutationRunning = true;
|
|
64
|
+
const generation = ++this.lifecycleGeneration;
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
result = operation(generation);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
result = Promise.reject(error);
|
|
71
|
+
}
|
|
72
|
+
void result.then(resolveOperation, rejectOperation).finally(() => {
|
|
73
|
+
this.lifecycleMutationRunning = false;
|
|
74
|
+
this.lifecycleMutationQueue.shift()?.();
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
if (this.lifecycleMutationRunning)
|
|
78
|
+
this.lifecycleMutationQueue.push(run);
|
|
79
|
+
else
|
|
80
|
+
run();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
isLifecycleOwner(generation, runtime, session) {
|
|
84
|
+
return this.host.isRunning()
|
|
85
|
+
&& generation === this.lifecycleGeneration
|
|
86
|
+
&& this.host.runtime() === runtime
|
|
87
|
+
&& runtime.session === session;
|
|
88
|
+
}
|
|
89
|
+
cancelPendingLifecycleWork() {
|
|
90
|
+
this.lifecycleGeneration += 1;
|
|
91
|
+
this.cancelHistoryLoad();
|
|
92
|
+
this.pendingActiveTabId = undefined;
|
|
93
|
+
this.clearRuntimeSubscriptions();
|
|
94
|
+
}
|
|
43
95
|
tabs() {
|
|
44
96
|
if (!this.pendingActiveTabId)
|
|
45
97
|
this.syncActiveTabFromRuntime({ save: false });
|
|
@@ -122,6 +174,24 @@ export class AppTabsController {
|
|
|
122
174
|
this.storeActiveDeferredUserMessages();
|
|
123
175
|
void this.saveTabs();
|
|
124
176
|
}
|
|
177
|
+
requeueAutoUserMessageForSession(session, message) {
|
|
178
|
+
if (this.host.runtime()?.session === session)
|
|
179
|
+
return;
|
|
180
|
+
const tab = this.findTabForSession(session);
|
|
181
|
+
if (!tab)
|
|
182
|
+
return;
|
|
183
|
+
const messages = this.autoUserMessagesByTabId.get(tab.id) ?? [];
|
|
184
|
+
messages.unshift(cloneSubmittedUserMessage(message));
|
|
185
|
+
this.autoUserMessagesByTabId.set(tab.id, messages);
|
|
186
|
+
void this.saveTabs();
|
|
187
|
+
}
|
|
188
|
+
inputTabOwnedByRuntime(tabId, runtime, session) {
|
|
189
|
+
if (runtime.session !== session)
|
|
190
|
+
return false;
|
|
191
|
+
if (tabId === undefined)
|
|
192
|
+
return this.host.runtime() === runtime;
|
|
193
|
+
return this.runtimesByTabId.get(tabId) === runtime;
|
|
194
|
+
}
|
|
125
195
|
syncActiveTabFromRuntime(options = {}) {
|
|
126
196
|
if (this.pendingActiveTabId && options.force !== true)
|
|
127
197
|
return;
|
|
@@ -163,19 +233,27 @@ export class AppTabsController {
|
|
|
163
233
|
if (options.save !== false)
|
|
164
234
|
void this.saveTabs();
|
|
165
235
|
}
|
|
166
|
-
|
|
236
|
+
restoreAfterStartup() {
|
|
237
|
+
return this.runLifecycleMutation((generation) => this.restoreAfterStartupMutation(generation));
|
|
238
|
+
}
|
|
239
|
+
async restoreAfterStartupMutation(generation) {
|
|
167
240
|
if (this.restored)
|
|
168
241
|
return;
|
|
169
242
|
this.restored = true;
|
|
170
243
|
const runtime = this.host.runtime();
|
|
171
244
|
if (!runtime)
|
|
172
245
|
return;
|
|
246
|
+
const startupSession = runtime.session;
|
|
173
247
|
this.syncActiveTabFromRuntime({ save: false });
|
|
174
248
|
if (this.host.options.noSession) {
|
|
175
249
|
this.clearStartupTabPlaceholders();
|
|
176
250
|
return;
|
|
177
251
|
}
|
|
178
252
|
const saved = await this.loadTabs();
|
|
253
|
+
if (!this.isLifecycleOwner(generation, runtime, startupSession)) {
|
|
254
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
179
257
|
if (!saved || saved.tabs.length === 0) {
|
|
180
258
|
this.clearStartupTabPlaceholders();
|
|
181
259
|
await this.saveTabs();
|
|
@@ -183,6 +261,10 @@ export class AppTabsController {
|
|
|
183
261
|
}
|
|
184
262
|
const restoredSessionPaths = saved.tabs.map((tab) => tab.path);
|
|
185
263
|
const forkedSessionPaths = await this.loadForkedSessionPaths(restoredSessionPaths);
|
|
264
|
+
if (!this.isLifecycleOwner(generation, runtime, startupSession)) {
|
|
265
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
186
268
|
const restoredTabs = this.restoredTabs(saved, new Map(), forkedSessionPaths);
|
|
187
269
|
if (restoredTabs.length === 0) {
|
|
188
270
|
this.clearStartupTabPlaceholders();
|
|
@@ -190,7 +272,7 @@ export class AppTabsController {
|
|
|
190
272
|
this.scheduleProjectSessionRetention();
|
|
191
273
|
return;
|
|
192
274
|
}
|
|
193
|
-
const currentPath =
|
|
275
|
+
const currentPath = startupSession.sessionFile ? resolve(startupSession.sessionFile) : undefined;
|
|
194
276
|
const explicitSessionPath = this.host.options.sessionPath ? resolve(this.host.options.sessionPath) : undefined;
|
|
195
277
|
const savedActivePath = saved.activePath ? resolve(saved.activePath) : undefined;
|
|
196
278
|
const desiredPath = explicitSessionPath && currentPath
|
|
@@ -202,11 +284,11 @@ export class AppTabsController {
|
|
|
202
284
|
this.restorePersistedInputStates(saved);
|
|
203
285
|
this.restorePersistedQueuedUserMessages(saved);
|
|
204
286
|
if (explicitSessionPath && currentPath)
|
|
205
|
-
this.ensureCurrentSessionTab(
|
|
287
|
+
this.ensureCurrentSessionTab(startupSession);
|
|
206
288
|
if (!desiredPath) {
|
|
207
289
|
this.clearStartupTabPlaceholders();
|
|
208
290
|
await this.saveTabs();
|
|
209
|
-
this.scheduleRestoredTabTitleRefresh(restoredSessionPaths);
|
|
291
|
+
this.scheduleRestoredTabTitleRefresh(restoredSessionPaths, generation);
|
|
210
292
|
this.scheduleProjectSessionRetention();
|
|
211
293
|
this.scheduleTabPrewarm();
|
|
212
294
|
return;
|
|
@@ -215,22 +297,51 @@ export class AppTabsController {
|
|
|
215
297
|
if (currentPath !== desiredPath) {
|
|
216
298
|
this.host.setStatus("restoring tabs");
|
|
217
299
|
this.host.render();
|
|
300
|
+
let candidateRuntime;
|
|
218
301
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
302
|
+
candidateRuntime = await this.host.createRuntimeForSession(desiredPath);
|
|
303
|
+
if (!this.isLifecycleOwner(generation, runtime, startupSession)) {
|
|
304
|
+
await this.disposeRuntimeIfOrphan(candidateRuntime);
|
|
305
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const candidateSession = candidateRuntime.session;
|
|
309
|
+
await this.host.activateRuntime(candidateRuntime, { awaitExtensions: false });
|
|
310
|
+
if (this.host.runtime() !== candidateRuntime || candidateRuntime.session !== candidateSession) {
|
|
311
|
+
await this.disposeRuntimeIfOrphan(candidateRuntime);
|
|
312
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
restoredRuntime = candidateRuntime;
|
|
221
316
|
}
|
|
222
317
|
catch {
|
|
318
|
+
if (candidateRuntime && this.host.runtime() === candidateRuntime) {
|
|
319
|
+
try {
|
|
320
|
+
await this.host.activateRuntime(runtime, { awaitExtensions: false });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Keep the best available runtime below and avoid overwriting a newer owner.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (candidateRuntime)
|
|
327
|
+
await this.disposeRuntimeIfOrphan(candidateRuntime);
|
|
328
|
+
if (this.host.runtime() !== runtime || runtime.session !== startupSession) {
|
|
329
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
223
332
|
this.host.showToast("Could not restore the previous active tab", "warning");
|
|
224
|
-
this.replaceTabs([this.tabFromSession(
|
|
333
|
+
this.replaceTabs([this.tabFromSession(startupSession), ...restoredTabs], currentPath);
|
|
225
334
|
this.storeActiveRuntime(runtime);
|
|
226
335
|
this.clearStartupTabPlaceholders();
|
|
227
336
|
await this.saveTabs();
|
|
228
|
-
this.scheduleRestoredTabTitleRefresh(restoredSessionPaths);
|
|
337
|
+
this.scheduleRestoredTabTitleRefresh(restoredSessionPaths, generation);
|
|
229
338
|
this.scheduleProjectSessionRetention();
|
|
230
339
|
return;
|
|
231
340
|
}
|
|
232
341
|
}
|
|
233
342
|
this.syncActiveTabFromRuntime({ save: false });
|
|
343
|
+
if (restoredRuntime !== runtime)
|
|
344
|
+
await this.disposeRuntimeIfOrphan(runtime);
|
|
234
345
|
this.clearStartupTabPlaceholders();
|
|
235
346
|
if (this.activeTabId)
|
|
236
347
|
this.restoreInputState(this.activeTabId);
|
|
@@ -238,10 +349,13 @@ export class AppTabsController {
|
|
|
238
349
|
await this.saveTabs();
|
|
239
350
|
this.scheduleProjectSessionRetention();
|
|
240
351
|
this.scheduleTabPrewarm();
|
|
241
|
-
await this.loadActiveSessionHistory(restoredRuntime);
|
|
242
|
-
this.scheduleRestoredTabTitleRefresh(restoredSessionPaths);
|
|
352
|
+
await this.loadActiveSessionHistory(restoredRuntime, generation);
|
|
353
|
+
this.scheduleRestoredTabTitleRefresh(restoredSessionPaths, generation);
|
|
354
|
+
}
|
|
355
|
+
openNewTab() {
|
|
356
|
+
return this.runLifecycleMutation((generation) => this.openNewTabMutation(generation));
|
|
243
357
|
}
|
|
244
|
-
async
|
|
358
|
+
async openNewTabMutation(generation) {
|
|
245
359
|
if (this.pendingActiveTabId) {
|
|
246
360
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
247
361
|
return;
|
|
@@ -258,6 +372,7 @@ export class AppTabsController {
|
|
|
258
372
|
this.storeActiveDeferredUserMessages();
|
|
259
373
|
const previousTabId = this.activeTabId;
|
|
260
374
|
const previousRuntime = runtime;
|
|
375
|
+
const previousSession = runtime.session;
|
|
261
376
|
const tab = {
|
|
262
377
|
id: createId("tab"),
|
|
263
378
|
title: "new",
|
|
@@ -297,6 +412,15 @@ export class AppTabsController {
|
|
|
297
412
|
this.host.render();
|
|
298
413
|
throw error;
|
|
299
414
|
}
|
|
415
|
+
if (!this.isLifecycleOwner(generation, previousRuntime, previousSession)) {
|
|
416
|
+
if (this.pendingActiveTabId === tab.id)
|
|
417
|
+
this.pendingActiveTabId = undefined;
|
|
418
|
+
this.removeTab(tab.id);
|
|
419
|
+
this.activeTabId = previousTabId;
|
|
420
|
+
await this.disposeRuntimeIfOrphan(newRuntime);
|
|
421
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
300
424
|
const existingTab = this.findTabForSession(newRuntime.session);
|
|
301
425
|
const targetTab = existingTab && existingTab.id !== tab.id ? existingTab : tab;
|
|
302
426
|
if (targetTab !== tab)
|
|
@@ -305,7 +429,6 @@ export class AppTabsController {
|
|
|
305
429
|
this.pendingActiveTabId = targetTab.id;
|
|
306
430
|
this.clearTabAttention(targetTab);
|
|
307
431
|
this.updateTabFromSession(targetTab, newRuntime.session);
|
|
308
|
-
this.setRuntimeForTab(targetTab.id, newRuntime);
|
|
309
432
|
this.restoreInputState(targetTab.id);
|
|
310
433
|
this.host.resetSessionView();
|
|
311
434
|
this.restoreDeferredUserMessages(targetTab.id);
|
|
@@ -314,10 +437,42 @@ export class AppTabsController {
|
|
|
314
437
|
try {
|
|
315
438
|
await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
|
|
316
439
|
}
|
|
317
|
-
|
|
440
|
+
catch (error) {
|
|
318
441
|
if (this.pendingActiveTabId === targetTab.id)
|
|
319
442
|
this.pendingActiveTabId = undefined;
|
|
443
|
+
if (targetTab === tab)
|
|
444
|
+
this.removeTab(tab.id);
|
|
445
|
+
this.activeTabId = previousTabId;
|
|
446
|
+
if (this.host.runtime() !== previousRuntime) {
|
|
447
|
+
try {
|
|
448
|
+
await this.host.activateRuntime(previousRuntime, { awaitExtensions: false });
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Keep the best available runtime and preserve the original activation error.
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await this.disposeRuntimeIfOrphan(newRuntime);
|
|
455
|
+
if (this.adoptCurrentRuntimeAfterFailedRollback(targetTab, newRuntime)) {
|
|
456
|
+
this.host.setSessionStatus(newRuntime.session);
|
|
457
|
+
this.host.setSessionActivity(this.sessionActivity(newRuntime.session));
|
|
458
|
+
this.host.render();
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
if (previousTabId)
|
|
462
|
+
this.restoreInputState(previousTabId);
|
|
463
|
+
this.host.closeMenusForTabSwitch?.();
|
|
464
|
+
this.host.resetSessionView();
|
|
465
|
+
if (previousTabId)
|
|
466
|
+
this.restoreDeferredUserMessages(previousTabId);
|
|
467
|
+
this.host.loadSessionHistory();
|
|
468
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
469
|
+
this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
|
|
470
|
+
this.host.render();
|
|
471
|
+
throw error;
|
|
320
472
|
}
|
|
473
|
+
if (this.pendingActiveTabId === targetTab.id)
|
|
474
|
+
this.pendingActiveTabId = undefined;
|
|
475
|
+
this.setRuntimeForTab(targetTab.id, newRuntime);
|
|
321
476
|
void this.saveTabs();
|
|
322
477
|
this.scheduleProjectSessionRetention();
|
|
323
478
|
this.host.resetSessionView();
|
|
@@ -333,7 +488,10 @@ export class AppTabsController {
|
|
|
333
488
|
this.host.setSessionActivity(this.sessionActivity(newRuntime.session));
|
|
334
489
|
this.host.render();
|
|
335
490
|
}
|
|
336
|
-
|
|
491
|
+
openSessionInNewTab(sessionPath) {
|
|
492
|
+
return this.runLifecycleMutation((generation) => this.openSessionInNewTabMutation(sessionPath, generation));
|
|
493
|
+
}
|
|
494
|
+
async openSessionInNewTabMutation(sessionPath, generation) {
|
|
337
495
|
if (this.pendingActiveTabId) {
|
|
338
496
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
339
497
|
return false;
|
|
@@ -349,7 +507,7 @@ export class AppTabsController {
|
|
|
349
507
|
const resolvedSessionPath = resolve(runtime.cwd, sessionPath);
|
|
350
508
|
const existingTab = this.findTabBySessionPath(resolvedSessionPath);
|
|
351
509
|
if (existingTab) {
|
|
352
|
-
await this.
|
|
510
|
+
await this.switchToTabMutation(existingTab.id, generation);
|
|
353
511
|
return true;
|
|
354
512
|
}
|
|
355
513
|
this.cancelHistoryLoad();
|
|
@@ -358,6 +516,7 @@ export class AppTabsController {
|
|
|
358
516
|
this.storeActiveDeferredUserMessages();
|
|
359
517
|
const previousTabId = this.activeTabId;
|
|
360
518
|
const previousRuntime = runtime;
|
|
519
|
+
const previousSession = runtime.session;
|
|
361
520
|
this.host.setStatus("opening session tab");
|
|
362
521
|
this.host.render();
|
|
363
522
|
const tab = {
|
|
@@ -399,8 +558,16 @@ export class AppTabsController {
|
|
|
399
558
|
this.host.render();
|
|
400
559
|
return false;
|
|
401
560
|
}
|
|
561
|
+
if (!this.isLifecycleOwner(generation, previousRuntime, previousSession)) {
|
|
562
|
+
if (this.pendingActiveTabId === tab.id)
|
|
563
|
+
this.pendingActiveTabId = undefined;
|
|
564
|
+
this.removeTab(tab.id);
|
|
565
|
+
this.activeTabId = previousTabId;
|
|
566
|
+
await this.disposeRuntimeIfOrphan(newRuntime);
|
|
567
|
+
this.syncActiveTabFromRuntime({ save: false, force: true });
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
402
570
|
this.updateTabFromSession(tab, newRuntime.session);
|
|
403
|
-
this.setRuntimeForTab(tab.id, newRuntime);
|
|
404
571
|
this.host.render();
|
|
405
572
|
try {
|
|
406
573
|
await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
|
|
@@ -420,7 +587,14 @@ export class AppTabsController {
|
|
|
420
587
|
// Keep the best available runtime below and surface the switch failure.
|
|
421
588
|
}
|
|
422
589
|
}
|
|
423
|
-
|
|
590
|
+
await this.disposeRuntimeIfOrphan(newRuntime);
|
|
591
|
+
if (this.adoptCurrentRuntimeAfterFailedRollback(tab, newRuntime)) {
|
|
592
|
+
this.host.showToast("Could not initialize session tab", "warning");
|
|
593
|
+
this.host.setSessionStatus(newRuntime.session);
|
|
594
|
+
this.host.setSessionActivity(this.sessionActivity(newRuntime.session));
|
|
595
|
+
this.host.render();
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
424
598
|
this.host.showToast("Could not open session tab", "warning");
|
|
425
599
|
this.host.resetSessionView();
|
|
426
600
|
if (previousTabId)
|
|
@@ -440,10 +614,13 @@ export class AppTabsController {
|
|
|
440
614
|
this.restoreInputState(tab.id);
|
|
441
615
|
void this.saveTabs();
|
|
442
616
|
this.scheduleTabPrewarm();
|
|
443
|
-
await this.loadActiveSessionHistory(newRuntime);
|
|
617
|
+
await this.loadActiveSessionHistory(newRuntime, generation);
|
|
444
618
|
return true;
|
|
445
619
|
}
|
|
446
|
-
|
|
620
|
+
forkSessionEntryInNewTab(entryId) {
|
|
621
|
+
return this.runLifecycleMutation((generation) => this.forkSessionEntryInNewTabMutation(entryId, generation));
|
|
622
|
+
}
|
|
623
|
+
async forkSessionEntryInNewTabMutation(entryId, generation) {
|
|
447
624
|
if (this.pendingActiveTabId) {
|
|
448
625
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
449
626
|
return false;
|
|
@@ -467,6 +644,7 @@ export class AppTabsController {
|
|
|
467
644
|
this.storeActiveDeferredUserMessages();
|
|
468
645
|
const previousTabId = this.activeTabId;
|
|
469
646
|
const previousRuntime = runtime;
|
|
647
|
+
const previousSession = runtime.session;
|
|
470
648
|
this.host.setStatus("forking session tab");
|
|
471
649
|
this.host.render();
|
|
472
650
|
let forkRuntime;
|
|
@@ -484,22 +662,28 @@ export class AppTabsController {
|
|
|
484
662
|
result = await forkRuntime.fork(entryId);
|
|
485
663
|
}
|
|
486
664
|
catch (error) {
|
|
487
|
-
|
|
665
|
+
await this.disposeRuntimeIfOrphan(forkRuntime);
|
|
488
666
|
throw error;
|
|
489
667
|
}
|
|
490
668
|
if (result.cancelled) {
|
|
491
|
-
|
|
669
|
+
await this.disposeRuntimeIfOrphan(forkRuntime);
|
|
492
670
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
|
|
493
671
|
this.host.setSessionStatus(previousRuntime.session);
|
|
494
672
|
this.host.render();
|
|
495
673
|
return false;
|
|
496
674
|
}
|
|
675
|
+
if (generation !== this.lifecycleGeneration
|
|
676
|
+
|| this.host.runtime() !== previousRuntime
|
|
677
|
+
|| (forkRuntime !== previousRuntime && previousRuntime.session !== previousSession)) {
|
|
678
|
+
await this.disposeRuntimeIfOrphan(forkRuntime);
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
497
681
|
const existingTab = this.findTabForSession(forkRuntime.session);
|
|
498
682
|
if (existingTab) {
|
|
499
683
|
if (result.selectedText)
|
|
500
684
|
this.inputStatesByTabId.set(existingTab.id, this.inputStateFromText(result.selectedText));
|
|
501
|
-
|
|
502
|
-
await this.
|
|
685
|
+
await this.disposeRuntimeIfOrphan(forkRuntime);
|
|
686
|
+
await this.switchToTabMutation(existingTab.id, generation);
|
|
503
687
|
this.host.showToast("Fork opened in existing tab", "success");
|
|
504
688
|
return true;
|
|
505
689
|
}
|
|
@@ -509,7 +693,6 @@ export class AppTabsController {
|
|
|
509
693
|
this.pendingActiveTabId = tab.id;
|
|
510
694
|
this.clearTabAttention(tab);
|
|
511
695
|
this.updateTabFromSession(tab, forkRuntime.session);
|
|
512
|
-
this.setRuntimeForTab(tab.id, forkRuntime);
|
|
513
696
|
if (result.selectedText)
|
|
514
697
|
this.inputStatesByTabId.set(tab.id, this.inputStateFromText(result.selectedText));
|
|
515
698
|
this.restoreInputState(tab.id);
|
|
@@ -536,7 +719,14 @@ export class AppTabsController {
|
|
|
536
719
|
// Keep the best available runtime below and surface the switch failure.
|
|
537
720
|
}
|
|
538
721
|
}
|
|
539
|
-
|
|
722
|
+
await this.disposeRuntimeIfOrphan(forkRuntime);
|
|
723
|
+
if (this.adoptCurrentRuntimeAfterFailedRollback(tab, forkRuntime)) {
|
|
724
|
+
this.host.showToast("Could not initialize fork tab", "warning");
|
|
725
|
+
this.host.setSessionStatus(forkRuntime.session);
|
|
726
|
+
this.host.setSessionActivity(this.sessionActivity(forkRuntime.session));
|
|
727
|
+
this.host.render();
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
540
730
|
this.host.showToast("Could not open fork tab", "warning");
|
|
541
731
|
this.host.resetSessionView();
|
|
542
732
|
if (previousTabId)
|
|
@@ -556,14 +746,17 @@ export class AppTabsController {
|
|
|
556
746
|
void this.saveTabs();
|
|
557
747
|
this.scheduleProjectSessionRetention();
|
|
558
748
|
this.scheduleTabPrewarm();
|
|
559
|
-
await this.loadActiveSessionHistory(forkRuntime);
|
|
749
|
+
await this.loadActiveSessionHistory(forkRuntime, generation);
|
|
560
750
|
this.host.addEntry({ id: createId("system"), kind: "system", text: `Forked from entry ${entryId} in a new tab.` });
|
|
561
751
|
this.host.setSessionStatus(forkRuntime.session);
|
|
562
752
|
this.host.showToast("Fork opened in new tab", "success");
|
|
563
753
|
this.host.render();
|
|
564
754
|
return true;
|
|
565
755
|
}
|
|
566
|
-
|
|
756
|
+
switchToTab(tabId) {
|
|
757
|
+
return this.runLifecycleMutation((generation) => this.switchToTabMutation(tabId, generation));
|
|
758
|
+
}
|
|
759
|
+
async switchToTabMutation(tabId, generation) {
|
|
567
760
|
if (this.pendingActiveTabId) {
|
|
568
761
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
569
762
|
return;
|
|
@@ -584,6 +777,7 @@ export class AppTabsController {
|
|
|
584
777
|
this.cancelHistoryLoad();
|
|
585
778
|
const previousTabId = this.activeTabId;
|
|
586
779
|
const previousRuntime = runtime;
|
|
780
|
+
const previousSession = runtime.session;
|
|
587
781
|
const previousTargetActivity = target.activity;
|
|
588
782
|
this.storeActiveRuntime(runtime);
|
|
589
783
|
this.storeActiveSessionView();
|
|
@@ -603,6 +797,9 @@ export class AppTabsController {
|
|
|
603
797
|
targetRuntime = await this.runtimeForTab(target);
|
|
604
798
|
if (!targetRuntime)
|
|
605
799
|
throw new Error("Could not load tab runtime");
|
|
800
|
+
if (!this.isLifecycleOwner(generation, previousRuntime, previousSession)) {
|
|
801
|
+
throw new Error("Tab ownership changed while loading the runtime");
|
|
802
|
+
}
|
|
606
803
|
await this.host.activateRuntime(targetRuntime, { awaitExtensions: false });
|
|
607
804
|
}
|
|
608
805
|
catch {
|
|
@@ -652,12 +849,19 @@ export class AppTabsController {
|
|
|
652
849
|
this.host.render();
|
|
653
850
|
}
|
|
654
851
|
else {
|
|
655
|
-
|
|
656
|
-
this.
|
|
852
|
+
const invalidationGeneration = this.historyInvalidationGenerationByTabId.get(target.id);
|
|
853
|
+
const loaded = await this.loadActiveSessionHistory(targetRuntime, generation);
|
|
854
|
+
if (loaded && this.historyInvalidationGenerationByTabId.get(target.id) === invalidationGeneration) {
|
|
855
|
+
this.tabIdsNeedingHistoryReload.delete(target.id);
|
|
856
|
+
this.historyInvalidationGenerationByTabId.delete(target.id);
|
|
857
|
+
}
|
|
657
858
|
}
|
|
658
|
-
this.scheduleDelayedHistoryReload(target.id, targetRuntime);
|
|
859
|
+
this.scheduleDelayedHistoryReload(target.id, targetRuntime, generation);
|
|
860
|
+
}
|
|
861
|
+
closeTab(tabId) {
|
|
862
|
+
return this.runLifecycleMutation((generation) => this.closeTabMutation(tabId, generation));
|
|
659
863
|
}
|
|
660
|
-
async
|
|
864
|
+
async closeTabMutation(tabId, generation) {
|
|
661
865
|
if (this.pendingActiveTabId) {
|
|
662
866
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
663
867
|
return;
|
|
@@ -667,7 +871,7 @@ export class AppTabsController {
|
|
|
667
871
|
return;
|
|
668
872
|
this.cancelHistoryLoad();
|
|
669
873
|
if (this.tabItems.length <= 1) {
|
|
670
|
-
await this.replaceLastTabWithNewSession(tabId);
|
|
874
|
+
await this.replaceLastTabWithNewSession(tabId, generation);
|
|
671
875
|
return;
|
|
672
876
|
}
|
|
673
877
|
if (tabId !== this.activeTabId) {
|
|
@@ -685,7 +889,7 @@ export class AppTabsController {
|
|
|
685
889
|
this.storeActiveDeferredUserMessages();
|
|
686
890
|
this.stopAttentionBlinkIfIdle();
|
|
687
891
|
if (tabRuntime)
|
|
688
|
-
|
|
892
|
+
await this.disposeRuntimeIfOrphan(tabRuntime);
|
|
689
893
|
void this.saveTabs();
|
|
690
894
|
this.host.render();
|
|
691
895
|
return;
|
|
@@ -700,10 +904,41 @@ export class AppTabsController {
|
|
|
700
904
|
}
|
|
701
905
|
this.host.setStatus("closing tab");
|
|
702
906
|
this.host.render();
|
|
907
|
+
const session = runtime.session;
|
|
703
908
|
const nextRuntime = await this.runtimeForTab(nextTab);
|
|
704
909
|
if (!nextRuntime)
|
|
705
910
|
return;
|
|
706
|
-
|
|
911
|
+
if (!this.isLifecycleOwner(generation, runtime, session))
|
|
912
|
+
return;
|
|
913
|
+
this.pendingActiveTabId = nextTab.id;
|
|
914
|
+
try {
|
|
915
|
+
await this.host.activateRuntime(nextRuntime, { awaitExtensions: false });
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
if (this.pendingActiveTabId === nextTab.id)
|
|
919
|
+
this.pendingActiveTabId = undefined;
|
|
920
|
+
if (this.host.runtime() !== runtime) {
|
|
921
|
+
try {
|
|
922
|
+
await this.host.activateRuntime(runtime, { awaitExtensions: false });
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
// Keep the best available runtime below and surface the close failure.
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (this.host.runtime() === nextRuntime) {
|
|
929
|
+
this.activeTabId = nextTab.id;
|
|
930
|
+
this.clearTabAttention(nextTab);
|
|
931
|
+
this.setRuntimeForTab(nextTab.id, nextRuntime);
|
|
932
|
+
this.restoreInputState(nextTab.id);
|
|
933
|
+
}
|
|
934
|
+
this.host.showToast("Could not close tab", "warning");
|
|
935
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
936
|
+
this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
|
|
937
|
+
this.host.render();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (this.pendingActiveTabId === nextTab.id)
|
|
941
|
+
this.pendingActiveTabId = undefined;
|
|
707
942
|
this.tabItems.splice(index, 1);
|
|
708
943
|
this.deleteRuntimeForTab(tabId);
|
|
709
944
|
this.inputStatesByTabId.delete(tabId);
|
|
@@ -716,23 +951,34 @@ export class AppTabsController {
|
|
|
716
951
|
this.setRuntimeForTab(nextTab.id, nextRuntime);
|
|
717
952
|
this.restoreInputState(nextTab.id);
|
|
718
953
|
this.host.closeMenusForTabSwitch?.();
|
|
719
|
-
|
|
954
|
+
await this.disposeRuntimeIfOrphan(runtime);
|
|
720
955
|
void this.saveTabs();
|
|
721
956
|
this.scheduleTabPrewarm();
|
|
722
|
-
await this.loadActiveSessionHistory(nextRuntime);
|
|
957
|
+
await this.loadActiveSessionHistory(nextRuntime, generation);
|
|
723
958
|
}
|
|
724
|
-
async replaceLastTabWithNewSession(tabId) {
|
|
959
|
+
async replaceLastTabWithNewSession(tabId, generation) {
|
|
725
960
|
const tab = this.tabItems.find((item) => item.id === tabId);
|
|
726
961
|
if (!tab)
|
|
727
962
|
return;
|
|
728
963
|
const runtime = this.idleRuntime("new");
|
|
729
964
|
if (!runtime)
|
|
730
965
|
return;
|
|
966
|
+
const session = runtime.session;
|
|
731
967
|
this.activeTabId = tab.id;
|
|
968
|
+
this.pendingActiveTabId = tab.id;
|
|
732
969
|
this.host.setStatus("starting new session");
|
|
733
970
|
this.host.render();
|
|
734
|
-
|
|
735
|
-
|
|
971
|
+
let result;
|
|
972
|
+
try {
|
|
973
|
+
await this.host.awaitCurrentSessionExtensions?.(runtime);
|
|
974
|
+
if (!this.isLifecycleOwner(generation, runtime, session))
|
|
975
|
+
return;
|
|
976
|
+
result = await runtime.newSession();
|
|
977
|
+
}
|
|
978
|
+
finally {
|
|
979
|
+
if (this.pendingActiveTabId === tab.id)
|
|
980
|
+
this.pendingActiveTabId = undefined;
|
|
981
|
+
}
|
|
736
982
|
if (result.cancelled) {
|
|
737
983
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
|
|
738
984
|
this.host.setSessionStatus(runtime.session);
|
|
@@ -764,12 +1010,25 @@ export class AppTabsController {
|
|
|
764
1010
|
this.scheduleProjectSessionRetention();
|
|
765
1011
|
this.host.render();
|
|
766
1012
|
}
|
|
767
|
-
async loadActiveSessionHistory(runtime) {
|
|
768
|
-
const
|
|
769
|
-
|
|
1013
|
+
async loadActiveSessionHistory(runtime, lifecycleGeneration) {
|
|
1014
|
+
const tabId = this.activeTabId;
|
|
1015
|
+
if (!tabId)
|
|
1016
|
+
return false;
|
|
1017
|
+
const session = runtime.session;
|
|
1018
|
+
const ownershipGeneration = this.runtimeOwnershipGenerationByTabId.get(tabId);
|
|
1019
|
+
const historyGeneration = ++this.historyLoadGeneration;
|
|
1020
|
+
const isCancelled = () => ownershipGeneration === undefined
|
|
1021
|
+
|| !this.host.isRunning()
|
|
1022
|
+
|| historyGeneration !== this.historyLoadGeneration
|
|
1023
|
+
|| lifecycleGeneration !== this.lifecycleGeneration
|
|
1024
|
+
|| this.pendingActiveTabId !== undefined
|
|
1025
|
+
|| this.activeTabId !== tabId
|
|
1026
|
+
|| this.host.runtime() !== runtime
|
|
1027
|
+
|| !this.isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration);
|
|
1028
|
+
if (isCancelled())
|
|
1029
|
+
return false;
|
|
770
1030
|
this.host.resetSessionView();
|
|
771
|
-
|
|
772
|
-
this.restoreDeferredUserMessages(this.activeTabId);
|
|
1031
|
+
this.restoreDeferredUserMessages(tabId);
|
|
773
1032
|
this.host.setStatus("loading session history");
|
|
774
1033
|
this.host.setSessionActivity("thinking");
|
|
775
1034
|
this.host.render();
|
|
@@ -782,12 +1041,15 @@ export class AppTabsController {
|
|
|
782
1041
|
lazyOlderHistory: true,
|
|
783
1042
|
});
|
|
784
1043
|
if (!completed || isCancelled())
|
|
785
|
-
return;
|
|
786
|
-
this.restoreStoredScrollState(
|
|
787
|
-
this.host.setSessionStatus(
|
|
1044
|
+
return false;
|
|
1045
|
+
this.restoreStoredScrollState(tabId);
|
|
1046
|
+
this.host.setSessionStatus(session);
|
|
1047
|
+
if (isCancelled())
|
|
1048
|
+
return false;
|
|
788
1049
|
this.host.syncUserSessionEntryMetadata();
|
|
789
|
-
this.host.setSessionActivity(this.sessionActivity(
|
|
1050
|
+
this.host.setSessionActivity(this.sessionActivity(session));
|
|
790
1051
|
this.host.render();
|
|
1052
|
+
return true;
|
|
791
1053
|
}
|
|
792
1054
|
cancelHistoryLoad() {
|
|
793
1055
|
this.historyLoadGeneration += 1;
|
|
@@ -851,17 +1113,39 @@ export class AppTabsController {
|
|
|
851
1113
|
this.host.restoreScrollState(scrollState);
|
|
852
1114
|
}
|
|
853
1115
|
setRuntimeForTab(tabId, runtime) {
|
|
1116
|
+
const session = runtime.session;
|
|
1117
|
+
const existingRuntime = this.runtimesByTabId.get(tabId);
|
|
1118
|
+
const existingSubscription = this.runtimeSubscriptionsByTabId.get(tabId);
|
|
1119
|
+
if (existingRuntime === runtime && existingSubscription?.session === session)
|
|
1120
|
+
return;
|
|
1121
|
+
for (const [ownerTabId, ownedRuntime] of this.runtimesByTabId) {
|
|
1122
|
+
if (ownerTabId === tabId || ownedRuntime !== runtime)
|
|
1123
|
+
continue;
|
|
1124
|
+
this.releaseRuntimeOwnership(ownerTabId);
|
|
1125
|
+
}
|
|
1126
|
+
if (existingRuntime && existingRuntime !== runtime)
|
|
1127
|
+
this.queueOrphanRuntimeDisposal(existingRuntime);
|
|
1128
|
+
this.clearRuntimeRefreshTimers(tabId);
|
|
1129
|
+
this.clearHistoryReloadTimers(tabId);
|
|
1130
|
+
existingSubscription?.unsubscribe();
|
|
1131
|
+
this.runtimeSubscriptionsByTabId.delete(tabId);
|
|
854
1132
|
this.runtimesByTabId.set(tabId, runtime);
|
|
1133
|
+
this.runtimeOwnershipGenerationByTabId.set(tabId, ++this.runtimeOwnershipGeneration);
|
|
855
1134
|
this.observeRuntimeForTab(tabId, runtime);
|
|
856
1135
|
}
|
|
857
1136
|
deleteRuntimeForTab(tabId) {
|
|
858
|
-
this.
|
|
1137
|
+
this.releaseRuntimeOwnership(tabId);
|
|
859
1138
|
this.runtimeLoadsByTabId.delete(tabId);
|
|
860
1139
|
this.sessionViewsByTabId.delete(tabId);
|
|
861
1140
|
this.scrollStatesByTabId.delete(tabId);
|
|
1141
|
+
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
1142
|
+
this.historyInvalidationGenerationByTabId.delete(tabId);
|
|
1143
|
+
}
|
|
1144
|
+
releaseRuntimeOwnership(tabId) {
|
|
1145
|
+
this.runtimesByTabId.delete(tabId);
|
|
1146
|
+
this.runtimeOwnershipGenerationByTabId.delete(tabId);
|
|
862
1147
|
this.clearRuntimeRefreshTimers(tabId);
|
|
863
1148
|
this.clearHistoryReloadTimers(tabId);
|
|
864
|
-
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
865
1149
|
const subscription = this.runtimeSubscriptionsByTabId.get(tabId);
|
|
866
1150
|
subscription?.unsubscribe();
|
|
867
1151
|
this.runtimeSubscriptionsByTabId.delete(tabId);
|
|
@@ -877,24 +1161,64 @@ export class AppTabsController {
|
|
|
877
1161
|
subscription.unsubscribe();
|
|
878
1162
|
}
|
|
879
1163
|
this.runtimeSubscriptionsByTabId.clear();
|
|
1164
|
+
this.runtimeOwnershipGenerationByTabId.clear();
|
|
880
1165
|
}
|
|
881
1166
|
observeRuntimeForTab(tabId, runtime) {
|
|
882
1167
|
const existing = this.runtimeSubscriptionsByTabId.get(tabId);
|
|
883
1168
|
if (existing?.runtime === runtime && existing.session === runtime.session)
|
|
884
1169
|
return;
|
|
885
1170
|
existing?.unsubscribe();
|
|
886
|
-
const
|
|
1171
|
+
const session = runtime.session;
|
|
1172
|
+
const ownershipGeneration = this.runtimeOwnershipGenerationByTabId.get(tabId);
|
|
1173
|
+
if (ownershipGeneration === undefined)
|
|
1174
|
+
return;
|
|
1175
|
+
const unsubscribe = session.subscribe((event) => {
|
|
1176
|
+
if (!this.isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration))
|
|
1177
|
+
return;
|
|
887
1178
|
if (this.shouldInvalidateCachedViewForRuntimeEvent(event)) {
|
|
888
1179
|
this.tabIdsNeedingHistoryReload.add(tabId);
|
|
1180
|
+
this.historyInvalidationGenerationByTabId.set(tabId, ++this.historyInvalidationGeneration);
|
|
889
1181
|
}
|
|
890
1182
|
if (this.shouldScheduleDelayedSyncForRuntimeEvent(event)) {
|
|
891
|
-
this.scheduleDelayedRuntimeSync(tabId, runtime);
|
|
1183
|
+
this.scheduleDelayedRuntimeSync(tabId, runtime, session, ownershipGeneration);
|
|
892
1184
|
}
|
|
893
1185
|
if (!this.shouldSyncTabFromRuntimeEvent(event))
|
|
894
1186
|
return;
|
|
895
|
-
this.syncTabFromObservedRuntime(tabId, runtime);
|
|
1187
|
+
this.syncTabFromObservedRuntime(tabId, runtime, session, ownershipGeneration);
|
|
896
1188
|
});
|
|
897
|
-
this.runtimeSubscriptionsByTabId.set(tabId, { runtime, session
|
|
1189
|
+
this.runtimeSubscriptionsByTabId.set(tabId, { runtime, session, unsubscribe });
|
|
1190
|
+
}
|
|
1191
|
+
isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration) {
|
|
1192
|
+
return this.runtimesByTabId.get(tabId) === runtime
|
|
1193
|
+
&& runtime.session === session
|
|
1194
|
+
&& this.runtimeOwnershipGenerationByTabId.get(tabId) === ownershipGeneration;
|
|
1195
|
+
}
|
|
1196
|
+
queueOrphanRuntimeDisposal(runtime) {
|
|
1197
|
+
if (this.orphanRuntimeDisposals.has(runtime))
|
|
1198
|
+
return;
|
|
1199
|
+
this.orphanRuntimeDisposals.add(runtime);
|
|
1200
|
+
queueMicrotask(() => {
|
|
1201
|
+
void this.disposeRuntimeIfOrphan(runtime).finally(() => {
|
|
1202
|
+
this.orphanRuntimeDisposals.delete(runtime);
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
async disposeRuntimeIfOrphan(runtime) {
|
|
1207
|
+
if (this.host.runtime() === runtime || [...this.runtimesByTabId.values()].includes(runtime))
|
|
1208
|
+
return;
|
|
1209
|
+
await this.host.disposeRuntime(runtime).catch(() => undefined);
|
|
1210
|
+
}
|
|
1211
|
+
adoptCurrentRuntimeAfterFailedRollback(tab, runtime) {
|
|
1212
|
+
if (this.host.runtime() !== runtime)
|
|
1213
|
+
return false;
|
|
1214
|
+
if (!this.tabItems.some((item) => item.id === tab.id))
|
|
1215
|
+
this.tabItems.push(tab);
|
|
1216
|
+
this.activeTabId = tab.id;
|
|
1217
|
+
this.clearTabAttention(tab);
|
|
1218
|
+
this.updateTabFromSession(tab, runtime.session);
|
|
1219
|
+
this.setRuntimeForTab(tab.id, runtime);
|
|
1220
|
+
this.restoreInputState(tab.id);
|
|
1221
|
+
return true;
|
|
898
1222
|
}
|
|
899
1223
|
shouldSyncTabFromRuntimeEvent(event) {
|
|
900
1224
|
return event.type === "session_info_changed"
|
|
@@ -922,12 +1246,12 @@ export class AppTabsController {
|
|
|
922
1246
|
|| event.type === "turn_end"
|
|
923
1247
|
|| event.type === "compaction_end";
|
|
924
1248
|
}
|
|
925
|
-
scheduleDelayedRuntimeSync(tabId, runtime) {
|
|
1249
|
+
scheduleDelayedRuntimeSync(tabId, runtime, session, ownershipGeneration) {
|
|
926
1250
|
this.clearRuntimeRefreshTimers(tabId);
|
|
927
1251
|
for (const delayMs of [0, 100, 500, 1500, 3000]) {
|
|
928
1252
|
const timer = setTimeout(() => {
|
|
929
1253
|
this.runtimeRefreshTimersByTabId.get(tabId)?.delete(timer);
|
|
930
|
-
this.syncTabFromObservedRuntime(tabId, runtime);
|
|
1254
|
+
this.syncTabFromObservedRuntime(tabId, runtime, session, ownershipGeneration);
|
|
931
1255
|
}, delayMs);
|
|
932
1256
|
timer.unref?.();
|
|
933
1257
|
let timers = this.runtimeRefreshTimersByTabId.get(tabId);
|
|
@@ -954,12 +1278,16 @@ export class AppTabsController {
|
|
|
954
1278
|
clearTimeout(timer);
|
|
955
1279
|
this.historyReloadTimersByTabId.delete(tabId);
|
|
956
1280
|
}
|
|
957
|
-
scheduleDelayedHistoryReload(tabId, runtime) {
|
|
1281
|
+
scheduleDelayedHistoryReload(tabId, runtime, lifecycleGeneration) {
|
|
958
1282
|
if (!this.tabIdsNeedingHistoryReload.has(tabId))
|
|
959
1283
|
return;
|
|
960
1284
|
if (tabId !== this.activeTabId || this.pendingActiveTabId !== undefined)
|
|
961
1285
|
return;
|
|
962
|
-
|
|
1286
|
+
const session = runtime.session;
|
|
1287
|
+
const ownershipGeneration = this.runtimeOwnershipGenerationByTabId.get(tabId);
|
|
1288
|
+
if (ownershipGeneration === undefined || !this.isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration))
|
|
1289
|
+
return;
|
|
1290
|
+
if (this.sessionActivity(session) === "running") {
|
|
963
1291
|
this.clearHistoryReloadTimers(tabId);
|
|
964
1292
|
return;
|
|
965
1293
|
}
|
|
@@ -967,7 +1295,7 @@ export class AppTabsController {
|
|
|
967
1295
|
for (const delayMs of [150, 1000, 3000]) {
|
|
968
1296
|
const timer = setTimeout(() => {
|
|
969
1297
|
this.historyReloadTimersByTabId.get(tabId)?.delete(timer);
|
|
970
|
-
void this.reloadActiveTabHistoryIfNeeded(tabId, runtime);
|
|
1298
|
+
void this.reloadActiveTabHistoryIfNeeded(tabId, runtime, session, ownershipGeneration, lifecycleGeneration);
|
|
971
1299
|
}, delayMs);
|
|
972
1300
|
timer.unref?.();
|
|
973
1301
|
let timers = this.historyReloadTimersByTabId.get(tabId);
|
|
@@ -978,19 +1306,29 @@ export class AppTabsController {
|
|
|
978
1306
|
timers.add(timer);
|
|
979
1307
|
}
|
|
980
1308
|
}
|
|
981
|
-
async reloadActiveTabHistoryIfNeeded(tabId, runtime) {
|
|
1309
|
+
async reloadActiveTabHistoryIfNeeded(tabId, runtime, session, ownershipGeneration, lifecycleGeneration) {
|
|
1310
|
+
if (lifecycleGeneration !== this.lifecycleGeneration)
|
|
1311
|
+
return;
|
|
982
1312
|
if (tabId !== this.activeTabId || this.pendingActiveTabId !== undefined || this.host.runtime() !== runtime)
|
|
983
1313
|
return;
|
|
1314
|
+
if (!this.isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration))
|
|
1315
|
+
return;
|
|
984
1316
|
if (!this.tabIdsNeedingHistoryReload.has(tabId))
|
|
985
1317
|
return;
|
|
986
|
-
if (this.sessionActivity(
|
|
1318
|
+
if (this.sessionActivity(session) === "running")
|
|
987
1319
|
return;
|
|
988
|
-
|
|
989
|
-
|
|
1320
|
+
const invalidationGeneration = this.historyInvalidationGenerationByTabId.get(tabId);
|
|
1321
|
+
const loaded = await this.loadActiveSessionHistory(runtime, lifecycleGeneration);
|
|
1322
|
+
if (loaded
|
|
1323
|
+
&& this.isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration)
|
|
1324
|
+
&& this.historyInvalidationGenerationByTabId.get(tabId) === invalidationGeneration) {
|
|
990
1325
|
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
1326
|
+
this.historyInvalidationGenerationByTabId.delete(tabId);
|
|
991
1327
|
}
|
|
992
1328
|
}
|
|
993
|
-
syncTabFromObservedRuntime(tabId, runtime) {
|
|
1329
|
+
syncTabFromObservedRuntime(tabId, runtime, session, ownershipGeneration) {
|
|
1330
|
+
if (!this.isRuntimeOwnedByTab(tabId, runtime, session, ownershipGeneration))
|
|
1331
|
+
return;
|
|
994
1332
|
const tab = this.tabItems.find((item) => item.id === tabId);
|
|
995
1333
|
if (!tab) {
|
|
996
1334
|
this.deleteRuntimeForTab(tabId);
|
|
@@ -999,7 +1337,7 @@ export class AppTabsController {
|
|
|
999
1337
|
const previousTitle = tab.title;
|
|
1000
1338
|
const previousActivity = tab.activity;
|
|
1001
1339
|
const previousSessionPath = tab.sessionPath;
|
|
1002
|
-
this.updateTabFromSession(tab,
|
|
1340
|
+
this.updateTabFromSession(tab, session);
|
|
1003
1341
|
if (tab.title === previousTitle && tab.activity === previousActivity && tab.sessionPath === previousSessionPath)
|
|
1004
1342
|
return;
|
|
1005
1343
|
if (tab.title !== previousTitle || tab.sessionPath !== previousSessionPath)
|
|
@@ -1046,9 +1384,15 @@ export class AppTabsController {
|
|
|
1046
1384
|
this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
|
|
1047
1385
|
}
|
|
1048
1386
|
async runtimeForTab(tab) {
|
|
1387
|
+
if (!this.host.isRunning())
|
|
1388
|
+
return undefined;
|
|
1049
1389
|
const existing = this.runtimesByTabId.get(tab.id);
|
|
1050
|
-
if (existing)
|
|
1390
|
+
if (existing && tab.sessionPath && this.sessionPath(existing.session) === resolve(tab.sessionPath))
|
|
1051
1391
|
return existing;
|
|
1392
|
+
if (existing) {
|
|
1393
|
+
this.releaseRuntimeOwnership(tab.id);
|
|
1394
|
+
await this.disposeRuntimeIfOrphan(existing);
|
|
1395
|
+
}
|
|
1052
1396
|
const loading = this.runtimeLoadsByTabId.get(tab.id);
|
|
1053
1397
|
if (loading)
|
|
1054
1398
|
return await loading;
|
|
@@ -1059,9 +1403,13 @@ export class AppTabsController {
|
|
|
1059
1403
|
const expectedPath = resolve(tab.sessionPath);
|
|
1060
1404
|
const pending = (async () => {
|
|
1061
1405
|
const runtime = await this.host.createRuntimeForSession(expectedPath);
|
|
1406
|
+
if (!this.host.isRunning()) {
|
|
1407
|
+
await this.disposeRuntimeIfOrphan(runtime);
|
|
1408
|
+
return undefined;
|
|
1409
|
+
}
|
|
1062
1410
|
const liveTab = this.tabItems.find((item) => item.id === tab.id);
|
|
1063
1411
|
if (!liveTab || !liveTab.sessionPath || resolve(liveTab.sessionPath) !== expectedPath) {
|
|
1064
|
-
|
|
1412
|
+
await this.disposeRuntimeIfOrphan(runtime);
|
|
1065
1413
|
return undefined;
|
|
1066
1414
|
}
|
|
1067
1415
|
this.setRuntimeForTab(tab.id, runtime);
|
|
@@ -1087,14 +1435,18 @@ export class AppTabsController {
|
|
|
1087
1435
|
&& resolve(tab.sessionPath) === normalizedPath));
|
|
1088
1436
|
}
|
|
1089
1437
|
replaceTabs(tabs, activeSessionPath) {
|
|
1438
|
+
const displacedRuntimes = [...new Set(this.runtimesByTabId.values())];
|
|
1090
1439
|
this.tabItems.length = 0;
|
|
1091
1440
|
this.runtimesByTabId.clear();
|
|
1441
|
+
this.runtimeLoadsByTabId.clear();
|
|
1092
1442
|
this.clearRuntimeSubscriptions();
|
|
1093
1443
|
this.inputStatesByTabId.clear();
|
|
1094
1444
|
this.autoUserMessagesByTabId.clear();
|
|
1095
1445
|
this.deferredUserMessagesByTabId.clear();
|
|
1096
1446
|
this.sessionViewsByTabId.clear();
|
|
1097
1447
|
this.scrollStatesByTabId.clear();
|
|
1448
|
+
this.tabIdsNeedingHistoryReload.clear();
|
|
1449
|
+
this.historyInvalidationGenerationByTabId.clear();
|
|
1098
1450
|
const seen = new Set();
|
|
1099
1451
|
for (const tab of tabs) {
|
|
1100
1452
|
const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
|
|
@@ -1117,6 +1469,8 @@ export class AppTabsController {
|
|
|
1117
1469
|
? this.tabItems.find((tab) => tab.sessionPath === activePath)?.id
|
|
1118
1470
|
: this.tabItems[0]?.id;
|
|
1119
1471
|
this.activeTabId ??= this.tabItems[0]?.id;
|
|
1472
|
+
for (const runtime of displacedRuntimes)
|
|
1473
|
+
this.queueOrphanRuntimeDisposal(runtime);
|
|
1120
1474
|
}
|
|
1121
1475
|
removeTab(tabId) {
|
|
1122
1476
|
const index = this.tabItems.findIndex((tab) => tab.id === tabId);
|
|
@@ -1322,18 +1676,20 @@ export class AppTabsController {
|
|
|
1322
1676
|
}));
|
|
1323
1677
|
return new Set(entries.filter((entry) => entry !== undefined));
|
|
1324
1678
|
}
|
|
1325
|
-
scheduleRestoredTabTitleRefresh(sessionPaths) {
|
|
1679
|
+
scheduleRestoredTabTitleRefresh(sessionPaths, lifecycleGeneration) {
|
|
1326
1680
|
if (sessionPaths.length === 0)
|
|
1327
1681
|
return;
|
|
1328
1682
|
setTimeout(() => {
|
|
1329
|
-
void this.refreshRestoredTabTitles(sessionPaths);
|
|
1683
|
+
void this.refreshRestoredTabTitles(sessionPaths, lifecycleGeneration);
|
|
1330
1684
|
}, 0).unref?.();
|
|
1331
1685
|
}
|
|
1332
|
-
async refreshRestoredTabTitles(sessionPaths) {
|
|
1686
|
+
async refreshRestoredTabTitles(sessionPaths, lifecycleGeneration) {
|
|
1333
1687
|
const [titles, forkedSessionPaths] = await Promise.all([
|
|
1334
1688
|
this.loadSessionTitles(sessionPaths),
|
|
1335
1689
|
this.loadForkedSessionPaths(sessionPaths),
|
|
1336
1690
|
]);
|
|
1691
|
+
if (!this.host.isRunning() || lifecycleGeneration !== this.lifecycleGeneration)
|
|
1692
|
+
return;
|
|
1337
1693
|
if (titles.size === 0 && forkedSessionPaths.size === 0)
|
|
1338
1694
|
return;
|
|
1339
1695
|
let changed = false;
|
|
@@ -1439,9 +1795,9 @@ export class AppTabsController {
|
|
|
1439
1795
|
}
|
|
1440
1796
|
return messages;
|
|
1441
1797
|
}
|
|
1442
|
-
|
|
1798
|
+
saveTabs() {
|
|
1443
1799
|
if (this.host.options.noSession)
|
|
1444
|
-
return;
|
|
1800
|
+
return Promise.resolve();
|
|
1445
1801
|
try {
|
|
1446
1802
|
const tabs = [];
|
|
1447
1803
|
const seen = new Set();
|
|
@@ -1477,7 +1833,7 @@ export class AppTabsController {
|
|
|
1477
1833
|
tabs.push(persistedTab);
|
|
1478
1834
|
}
|
|
1479
1835
|
if (tabs.length === 0)
|
|
1480
|
-
return;
|
|
1836
|
+
return Promise.resolve();
|
|
1481
1837
|
const activePath = this.activeTab()?.sessionPath;
|
|
1482
1838
|
const payload = JSON.stringify({
|
|
1483
1839
|
version: TAB_STATE_VERSION,
|
|
@@ -1486,13 +1842,28 @@ export class AppTabsController {
|
|
|
1486
1842
|
...(activePath ? { activePath: resolve(activePath) } : {}),
|
|
1487
1843
|
}, null, 2);
|
|
1488
1844
|
const filePath = this.filePath();
|
|
1845
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1846
|
+
const write = this.saveTabsWriteTail.then(async () => {
|
|
1847
|
+
await this.writeTabsSnapshot(filePath, tempPath, payload);
|
|
1848
|
+
}).catch(() => {
|
|
1849
|
+
// Tab state should never interrupt the terminal UI.
|
|
1850
|
+
});
|
|
1851
|
+
this.saveTabsWriteTail = write;
|
|
1852
|
+
return write;
|
|
1853
|
+
}
|
|
1854
|
+
catch {
|
|
1855
|
+
// Tab state should never interrupt the terminal UI.
|
|
1856
|
+
return Promise.resolve();
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async writeTabsSnapshot(filePath, tempPath, payload) {
|
|
1860
|
+
try {
|
|
1489
1861
|
await mkdir(dirname(filePath), { recursive: true });
|
|
1490
|
-
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1491
1862
|
await writeFile(tempPath, payload, "utf8");
|
|
1492
1863
|
await rename(tempPath, filePath);
|
|
1493
1864
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1865
|
+
finally {
|
|
1866
|
+
await unlink(tempPath).catch(() => undefined);
|
|
1496
1867
|
}
|
|
1497
1868
|
}
|
|
1498
1869
|
filePath() {
|
|
@@ -1504,7 +1875,7 @@ export class AppTabsController {
|
|
|
1504
1875
|
return join(getAgentDir(), "sessions", safePath);
|
|
1505
1876
|
}
|
|
1506
1877
|
scheduleProjectSessionRetention() {
|
|
1507
|
-
if (this.host.options.noSession || this.maxProjectSessions() <= 0 || this.retentionCleanupScheduled || this.retentionCleanupRunning)
|
|
1878
|
+
if (!this.host.isRunning() || this.host.options.noSession || this.maxProjectSessions() <= 0 || this.retentionCleanupScheduled || this.retentionCleanupRunning)
|
|
1508
1879
|
return;
|
|
1509
1880
|
this.retentionCleanupScheduled = true;
|
|
1510
1881
|
setTimeout(() => {
|
|
@@ -1513,7 +1884,7 @@ export class AppTabsController {
|
|
|
1513
1884
|
}, 0);
|
|
1514
1885
|
}
|
|
1515
1886
|
scheduleTabPrewarm() {
|
|
1516
|
-
if (this.host.options.noSession || this.prewarmScheduled || this.prewarmRunning)
|
|
1887
|
+
if (!this.host.isRunning() || this.host.options.noSession || this.prewarmScheduled || this.prewarmRunning)
|
|
1517
1888
|
return;
|
|
1518
1889
|
this.prewarmScheduled = true;
|
|
1519
1890
|
setTimeout(() => {
|
|
@@ -1528,6 +1899,8 @@ export class AppTabsController {
|
|
|
1528
1899
|
try {
|
|
1529
1900
|
let warmed = 0;
|
|
1530
1901
|
for (const tab of this.tabItems) {
|
|
1902
|
+
if (!this.host.isRunning())
|
|
1903
|
+
break;
|
|
1531
1904
|
if (warmed >= BACKGROUND_PREWARM_TAB_LIMIT)
|
|
1532
1905
|
break;
|
|
1533
1906
|
if (tab.id === this.activeTabId || !tab.sessionPath)
|
|
@@ -1599,17 +1972,9 @@ export class AppTabsController {
|
|
|
1599
1972
|
*/
|
|
1600
1973
|
async unlinkSessionAndDcpSidecar(sessionPath) {
|
|
1601
1974
|
let sidecarPath;
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
const parsed = JSON.parse(firstLine);
|
|
1606
|
-
if (parsed.type === "session" && typeof parsed.id === "string" && parsed.id) {
|
|
1607
|
-
sidecarPath = join(dirname(sessionPath), "dcp-state", parsed.id.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json");
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
catch {
|
|
1612
|
-
// Reading the session id is best-effort; proceed to unlink the file.
|
|
1975
|
+
const header = await readSessionHeader(sessionPath);
|
|
1976
|
+
if (header?.type === "session" && typeof header.id === "string" && header.id) {
|
|
1977
|
+
sidecarPath = join(dirname(sessionPath), "dcp-state", header.id.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json");
|
|
1613
1978
|
}
|
|
1614
1979
|
try {
|
|
1615
1980
|
await unlink(sessionPath);
|
|
@@ -1726,21 +2091,29 @@ async function readSessionTitleChunk(file, position, byteCount, options = {}) {
|
|
|
1726
2091
|
return undefined;
|
|
1727
2092
|
}
|
|
1728
2093
|
async function readSessionHasParent(sessionPath) {
|
|
2094
|
+
const header = await readSessionHeader(sessionPath);
|
|
2095
|
+
return header?.type === "session" && typeof header.parentSession === "string";
|
|
2096
|
+
}
|
|
2097
|
+
async function readSessionHeader(sessionPath) {
|
|
1729
2098
|
let file;
|
|
1730
2099
|
try {
|
|
1731
2100
|
file = await openFile(sessionPath, "r");
|
|
1732
|
-
const buffer = Buffer.alloc(
|
|
2101
|
+
const buffer = Buffer.alloc(SESSION_HEADER_SCAN_MAX_BYTES);
|
|
1733
2102
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
1734
2103
|
if (bytesRead <= 0)
|
|
1735
|
-
return
|
|
1736
|
-
const
|
|
2104
|
+
return undefined;
|
|
2105
|
+
const content = buffer.subarray(0, bytesRead);
|
|
2106
|
+
const newlineIndex = content.indexOf(0x0a);
|
|
2107
|
+
if (newlineIndex < 0 && bytesRead === buffer.length)
|
|
2108
|
+
return undefined;
|
|
2109
|
+
const firstLine = content.subarray(0, newlineIndex >= 0 ? newlineIndex : bytesRead).toString("utf8").trim();
|
|
1737
2110
|
if (!firstLine)
|
|
1738
|
-
return
|
|
2111
|
+
return undefined;
|
|
1739
2112
|
const parsed = JSON.parse(firstLine);
|
|
1740
|
-
return isRecord(parsed)
|
|
2113
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
1741
2114
|
}
|
|
1742
2115
|
catch {
|
|
1743
|
-
return
|
|
2116
|
+
return undefined;
|
|
1744
2117
|
}
|
|
1745
2118
|
finally {
|
|
1746
2119
|
await file?.close();
|