pi-ui-extend 0.1.13 → 0.1.17
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 +1 -1
- package/dist/app/app.d.ts +7 -0
- package/dist/app/app.js +102 -17
- package/dist/app/commands/command-controller.js +2 -0
- package/dist/app/commands/command-host.d.ts +5 -0
- package/dist/app/commands/command-model-actions.d.ts +2 -0
- package/dist/app/commands/command-model-actions.js +40 -4
- package/dist/app/commands/command-navigation-actions.d.ts +9 -0
- package/dist/app/commands/command-navigation-actions.js +62 -0
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +16 -0
- package/dist/app/constants.d.ts +0 -1
- package/dist/app/constants.js +0 -1
- package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/logger.d.ts +25 -0
- package/dist/app/logger.js +90 -0
- package/dist/app/model/model-usage-status.js +30 -15
- package/dist/app/popup/menu-items-controller.d.ts +4 -0
- package/dist/app/popup/menu-items-controller.js +68 -6
- package/dist/app/popup/popup-action-controller.d.ts +2 -1
- package/dist/app/popup/popup-action-controller.js +7 -4
- package/dist/app/popup/popup-menu-controller.d.ts +36 -23
- package/dist/app/popup/popup-menu-controller.js +97 -326
- package/dist/app/rendering/conversation-entry-renderer.js +3 -3
- package/dist/app/rendering/conversation-viewport.d.ts +10 -2
- package/dist/app/rendering/conversation-viewport.js +157 -16
- package/dist/app/rendering/editor-panels.js +22 -9
- package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
- package/dist/app/rendering/popup-menu-renderer.js +405 -0
- package/dist/app/rendering/render-controller.js +30 -28
- package/dist/app/rendering/render-text.js +5 -2
- package/dist/app/rendering/status-line-renderer.d.ts +8 -1
- package/dist/app/rendering/status-line-renderer.js +217 -117
- package/dist/app/rendering/toast-controller.d.ts +12 -3
- package/dist/app/rendering/toast-controller.js +70 -12
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +20 -10
- package/dist/app/screen/mouse-controller.d.ts +2 -2
- package/dist/app/screen/mouse-controller.js +27 -48
- package/dist/app/screen/screen-styler.d.ts +1 -1
- package/dist/app/screen/screen-styler.js +9 -7
- package/dist/app/screen/scroll-controller.d.ts +12 -9
- package/dist/app/screen/scroll-controller.js +56 -45
- package/dist/app/screen/status-controller.js +2 -1
- package/dist/app/session/lazy-session-manager.d.ts +11 -0
- package/dist/app/session/lazy-session-manager.js +539 -0
- package/dist/app/session/pix-system-message.d.ts +16 -0
- package/dist/app/session/pix-system-message.js +64 -0
- package/dist/app/session/request-history.d.ts +4 -0
- package/dist/app/session/request-history.js +11 -0
- package/dist/app/session/session-event-controller.d.ts +11 -0
- package/dist/app/session/session-event-controller.js +58 -2
- package/dist/app/session/session-history.d.ts +18 -0
- package/dist/app/session/session-history.js +72 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
- package/dist/app/session/session-lifecycle-controller.js +7 -2
- package/dist/app/session/session-search.js +10 -0
- package/dist/app/session/tabs-controller.d.ts +17 -5
- package/dist/app/session/tabs-controller.js +308 -29
- package/dist/app/todo/todo-model.d.ts +4 -2
- package/dist/app/todo/todo-model.js +23 -13
- package/dist/app/types.d.ts +17 -6
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +12 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +82 -25
- package/dist/default-pix-config.js +4 -0
- package/dist/fuzzy.d.ts +2 -0
- package/dist/fuzzy.js +27 -7
- package/dist/input-editor.d.ts +9 -0
- package/dist/input-editor.js +52 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
- package/dist/schemas/pi-tools-suite-schema.js +1 -0
- package/dist/schemas/pix-schema.d.ts +3 -1
- package/dist/schemas/pix-schema.js +6 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/dist/theme.js +6 -6
- package/dist/ui.d.ts +8 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
- package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
- package/external/pi-tools-suite/src/config.ts +8 -0
- package/external/pi-tools-suite/src/dcp/index.ts +16 -1
- package/external/pi-tools-suite/src/dcp/state.ts +35 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
- package/external/pi-tools-suite/src/todo/index.ts +123 -14
- package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
- package/external/pi-tools-suite/src/todo/todo.ts +12 -23
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
- package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
- package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
- package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +11 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { getAgentDir,
|
|
3
|
+
import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import { getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { isRecord } from "../guards.js";
|
|
7
7
|
import { createId } from "../id.js";
|
|
8
8
|
import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-info.js";
|
|
@@ -10,6 +10,7 @@ import { tabPanelRows } from "../rendering/tab-line-renderer.js";
|
|
|
10
10
|
const TAB_STATE_VERSION = 3;
|
|
11
11
|
const MAX_RESTORED_TABS = 8;
|
|
12
12
|
const TAB_ATTENTION_BLINK_KEY = "tab-attention";
|
|
13
|
+
const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
|
|
13
14
|
export class AppTabsController {
|
|
14
15
|
host;
|
|
15
16
|
tabItems = [];
|
|
@@ -21,6 +22,8 @@ export class AppTabsController {
|
|
|
21
22
|
pendingActiveTabId;
|
|
22
23
|
historyLoadGeneration = 0;
|
|
23
24
|
restored = false;
|
|
25
|
+
retentionCleanupRunning = false;
|
|
26
|
+
retentionCleanupScheduled = false;
|
|
24
27
|
constructor(host) {
|
|
25
28
|
this.host = host;
|
|
26
29
|
}
|
|
@@ -67,9 +70,11 @@ export class AppTabsController {
|
|
|
67
70
|
return this.inputStatesByTabId.get(tabId);
|
|
68
71
|
}
|
|
69
72
|
async setInputStateForTab(tabId, state) {
|
|
73
|
+
const attachments = state.attachments?.map(clonePersistedAttachment) ?? [];
|
|
70
74
|
const nextState = {
|
|
71
75
|
text: state.text,
|
|
72
76
|
cursor: Math.max(0, Math.min(state.text.length, Math.trunc(state.cursor))),
|
|
77
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
73
78
|
};
|
|
74
79
|
const targetTabId = tabId ?? this.activeTabId;
|
|
75
80
|
if (targetTabId) {
|
|
@@ -153,17 +158,21 @@ export class AppTabsController {
|
|
|
153
158
|
if (!runtime)
|
|
154
159
|
return;
|
|
155
160
|
this.syncActiveTabFromRuntime({ save: false });
|
|
156
|
-
if (this.host.options.noSession)
|
|
161
|
+
if (this.host.options.noSession) {
|
|
162
|
+
this.settleStartupTabPlaceholders();
|
|
157
163
|
return;
|
|
164
|
+
}
|
|
158
165
|
const saved = await this.loadTabs();
|
|
159
166
|
if (!saved || saved.tabs.length === 0) {
|
|
167
|
+
this.settleStartupTabPlaceholders();
|
|
160
168
|
await this.saveTabs();
|
|
161
169
|
return;
|
|
162
170
|
}
|
|
163
|
-
const
|
|
164
|
-
const restoredTabs = this.restoredTabs(saved, sessionTitles);
|
|
171
|
+
const restoredTabs = this.restoredTabs(saved);
|
|
165
172
|
if (restoredTabs.length === 0) {
|
|
173
|
+
this.settleStartupTabPlaceholders();
|
|
166
174
|
await this.saveTabs();
|
|
175
|
+
this.scheduleProjectSessionRetention();
|
|
167
176
|
return;
|
|
168
177
|
}
|
|
169
178
|
const currentPath = runtime.session.sessionFile ? resolve(runtime.session.sessionFile) : undefined;
|
|
@@ -180,35 +189,41 @@ export class AppTabsController {
|
|
|
180
189
|
if (explicitSessionPath && currentPath)
|
|
181
190
|
this.ensureCurrentSessionTab(runtime.session);
|
|
182
191
|
if (!desiredPath) {
|
|
192
|
+
this.settleStartupTabPlaceholders();
|
|
183
193
|
await this.saveTabs();
|
|
194
|
+
this.scheduleProjectSessionRetention();
|
|
184
195
|
return;
|
|
185
196
|
}
|
|
197
|
+
let restoredRuntime = runtime;
|
|
186
198
|
if (currentPath !== desiredPath) {
|
|
187
199
|
this.host.setStatus("restoring tabs");
|
|
188
200
|
this.host.render();
|
|
189
201
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error("restore cancelled");
|
|
202
|
+
restoredRuntime = await this.host.createRuntimeForSession(desiredPath);
|
|
203
|
+
await this.host.activateRuntime(restoredRuntime);
|
|
193
204
|
}
|
|
194
205
|
catch {
|
|
195
206
|
this.host.showToast("Could not restore the previous active tab", "warning");
|
|
196
207
|
this.replaceTabs([this.tabFromSession(runtime.session), ...restoredTabs], currentPath);
|
|
197
208
|
this.storeActiveRuntime(runtime);
|
|
209
|
+
this.settleStartupTabPlaceholders();
|
|
198
210
|
await this.saveTabs();
|
|
211
|
+
this.scheduleProjectSessionRetention();
|
|
199
212
|
return;
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
this.syncActiveTabFromRuntime({ save: false });
|
|
216
|
+
this.settleStartupTabPlaceholders();
|
|
203
217
|
this.host.resetSessionView();
|
|
204
218
|
if (this.activeTabId)
|
|
205
219
|
this.restoreDeferredUserMessages(this.activeTabId);
|
|
206
220
|
this.host.loadSessionHistory();
|
|
207
|
-
this.host.setSessionStatus(
|
|
208
|
-
this.host.setSessionActivity(this.sessionActivity(
|
|
221
|
+
this.host.setSessionStatus(restoredRuntime.session);
|
|
222
|
+
this.host.setSessionActivity(this.sessionActivity(restoredRuntime.session));
|
|
209
223
|
if (this.activeTabId)
|
|
210
224
|
this.restoreInputState(this.activeTabId);
|
|
211
225
|
await this.saveTabs();
|
|
226
|
+
this.scheduleProjectSessionRetention();
|
|
212
227
|
}
|
|
213
228
|
async openNewTab() {
|
|
214
229
|
if (this.pendingActiveTabId) {
|
|
@@ -237,6 +252,7 @@ export class AppTabsController {
|
|
|
237
252
|
this.updateTabFromSession(tab, newRuntime.session);
|
|
238
253
|
this.setRuntimeForTab(tab.id, newRuntime);
|
|
239
254
|
this.restoreInputState(tab.id);
|
|
255
|
+
this.host.closeMenusForTabSwitch?.();
|
|
240
256
|
try {
|
|
241
257
|
await this.host.activateRuntime(newRuntime);
|
|
242
258
|
}
|
|
@@ -245,6 +261,7 @@ export class AppTabsController {
|
|
|
245
261
|
this.pendingActiveTabId = undefined;
|
|
246
262
|
}
|
|
247
263
|
void this.saveTabs();
|
|
264
|
+
this.scheduleProjectSessionRetention();
|
|
248
265
|
this.host.resetSessionView();
|
|
249
266
|
this.restoreDeferredUserMessages(tab.id);
|
|
250
267
|
if (isEmptyStartupSession(newRuntime)) {
|
|
@@ -307,6 +324,7 @@ export class AppTabsController {
|
|
|
307
324
|
this.updateTabFromSession(tab, newRuntime.session);
|
|
308
325
|
this.setRuntimeForTab(tab.id, newRuntime);
|
|
309
326
|
this.restoreInputState(tab.id);
|
|
327
|
+
this.host.closeMenusForTabSwitch?.();
|
|
310
328
|
try {
|
|
311
329
|
await this.host.activateRuntime(newRuntime);
|
|
312
330
|
}
|
|
@@ -316,6 +334,7 @@ export class AppTabsController {
|
|
|
316
334
|
this.activeTabId = previousTabId;
|
|
317
335
|
if (previousTabId)
|
|
318
336
|
this.restoreInputState(previousTabId);
|
|
337
|
+
this.host.closeMenusForTabSwitch?.();
|
|
319
338
|
if (this.host.runtime() !== previousRuntime) {
|
|
320
339
|
try {
|
|
321
340
|
await this.host.activateRuntime(previousRuntime);
|
|
@@ -346,6 +365,120 @@ export class AppTabsController {
|
|
|
346
365
|
await this.loadActiveSessionHistory(newRuntime);
|
|
347
366
|
return true;
|
|
348
367
|
}
|
|
368
|
+
async forkSessionEntryInNewTab(entryId) {
|
|
369
|
+
if (this.pendingActiveTabId) {
|
|
370
|
+
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const runtime = this.idleRuntime("fork");
|
|
374
|
+
if (!runtime)
|
|
375
|
+
return false;
|
|
376
|
+
if (this.host.options.noSession) {
|
|
377
|
+
this.host.showToast("Fork in new tab is unavailable with --no-session", "warning");
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
const currentSessionPath = runtime.session.sessionFile ? resolve(runtime.session.sessionFile) : undefined;
|
|
381
|
+
if (!currentSessionPath) {
|
|
382
|
+
this.host.showToast("Fork in new tab requires a persisted session", "warning");
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
this.cancelHistoryLoad();
|
|
386
|
+
this.syncActiveTabFromRuntime({ save: false });
|
|
387
|
+
this.storeActiveInputState();
|
|
388
|
+
this.storeActiveDeferredUserMessages();
|
|
389
|
+
const previousTabId = this.activeTabId;
|
|
390
|
+
const previousRuntime = runtime;
|
|
391
|
+
this.host.setStatus("forking session tab");
|
|
392
|
+
this.host.render();
|
|
393
|
+
let forkRuntime;
|
|
394
|
+
try {
|
|
395
|
+
forkRuntime = await this.host.createRuntimeForSession(currentSessionPath);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
this.host.showToast("Could not fork in new tab", "warning");
|
|
399
|
+
this.host.setSessionStatus(previousRuntime.session);
|
|
400
|
+
this.host.render();
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
let result;
|
|
404
|
+
try {
|
|
405
|
+
result = await forkRuntime.fork(entryId);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
if (result.cancelled) {
|
|
412
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
413
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
|
|
414
|
+
this.host.setSessionStatus(previousRuntime.session);
|
|
415
|
+
this.host.render();
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
const existingTab = this.findTabForSession(forkRuntime.session);
|
|
419
|
+
if (existingTab) {
|
|
420
|
+
if (result.selectedText)
|
|
421
|
+
this.inputStatesByTabId.set(existingTab.id, this.inputStateFromText(result.selectedText));
|
|
422
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
423
|
+
await this.switchToTab(existingTab.id);
|
|
424
|
+
this.host.showToast("Fork opened in existing tab", "success");
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
const tab = this.tabFromSession(forkRuntime.session, { titlePlaceholder: "new" });
|
|
428
|
+
this.tabItems.push(tab);
|
|
429
|
+
this.activeTabId = tab.id;
|
|
430
|
+
this.pendingActiveTabId = tab.id;
|
|
431
|
+
this.clearTabAttention(tab);
|
|
432
|
+
this.updateTabFromSession(tab, forkRuntime.session);
|
|
433
|
+
this.setRuntimeForTab(tab.id, forkRuntime);
|
|
434
|
+
if (result.selectedText)
|
|
435
|
+
this.inputStatesByTabId.set(tab.id, this.inputStateFromText(result.selectedText));
|
|
436
|
+
this.restoreInputState(tab.id);
|
|
437
|
+
this.host.closeMenusForTabSwitch?.();
|
|
438
|
+
try {
|
|
439
|
+
await this.host.activateRuntime(forkRuntime);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
this.pendingActiveTabId = undefined;
|
|
443
|
+
this.removeTab(tab.id);
|
|
444
|
+
this.activeTabId = previousTabId;
|
|
445
|
+
if (previousTabId)
|
|
446
|
+
this.restoreInputState(previousTabId);
|
|
447
|
+
this.host.closeMenusForTabSwitch?.();
|
|
448
|
+
if (this.host.runtime() !== previousRuntime) {
|
|
449
|
+
try {
|
|
450
|
+
await this.host.activateRuntime(previousRuntime);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// Keep the best available runtime below and surface the switch failure.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
457
|
+
this.host.showToast("Could not open fork tab", "warning");
|
|
458
|
+
this.host.resetSessionView();
|
|
459
|
+
if (previousTabId)
|
|
460
|
+
this.restoreDeferredUserMessages(previousTabId);
|
|
461
|
+
this.host.loadSessionHistory();
|
|
462
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
463
|
+
this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
|
|
464
|
+
this.host.render();
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
this.pendingActiveTabId = undefined;
|
|
468
|
+
this.activeTabId = tab.id;
|
|
469
|
+
this.clearTabAttention(tab);
|
|
470
|
+
this.updateTabFromSession(tab, forkRuntime.session);
|
|
471
|
+
this.setRuntimeForTab(tab.id, forkRuntime);
|
|
472
|
+
this.restoreInputState(tab.id);
|
|
473
|
+
void this.saveTabs();
|
|
474
|
+
this.scheduleProjectSessionRetention();
|
|
475
|
+
await this.loadActiveSessionHistory(forkRuntime);
|
|
476
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: `Forked from entry ${entryId} in a new tab.` });
|
|
477
|
+
this.host.setSessionStatus(forkRuntime.session);
|
|
478
|
+
this.host.showToast("Fork opened in new tab", "success");
|
|
479
|
+
this.host.render();
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
349
482
|
async switchToTab(tabId) {
|
|
350
483
|
if (this.pendingActiveTabId) {
|
|
351
484
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
@@ -376,6 +509,7 @@ export class AppTabsController {
|
|
|
376
509
|
target.activity = "thinking";
|
|
377
510
|
this.clearTabAttention(target);
|
|
378
511
|
this.restoreInputState(target.id);
|
|
512
|
+
this.host.closeMenusForTabSwitch?.();
|
|
379
513
|
this.host.resetSessionView();
|
|
380
514
|
this.restoreDeferredUserMessages(target.id);
|
|
381
515
|
this.host.setStatus("switching tab");
|
|
@@ -397,6 +531,7 @@ export class AppTabsController {
|
|
|
397
531
|
this.activeTabId = previousTabId;
|
|
398
532
|
if (previousTabId)
|
|
399
533
|
this.restoreInputState(previousTabId);
|
|
534
|
+
this.host.closeMenusForTabSwitch?.();
|
|
400
535
|
if (this.host.runtime() !== previousRuntime) {
|
|
401
536
|
try {
|
|
402
537
|
await this.host.activateRuntime(previousRuntime);
|
|
@@ -481,6 +616,7 @@ export class AppTabsController {
|
|
|
481
616
|
this.updateTabFromSession(nextTab, nextRuntime.session);
|
|
482
617
|
this.setRuntimeForTab(nextTab.id, nextRuntime);
|
|
483
618
|
this.restoreInputState(nextTab.id);
|
|
619
|
+
this.host.closeMenusForTabSwitch?.();
|
|
484
620
|
void this.host.disposeRuntime(runtime);
|
|
485
621
|
void this.saveTabs();
|
|
486
622
|
await this.loadActiveSessionHistory(nextRuntime);
|
|
@@ -509,6 +645,7 @@ export class AppTabsController {
|
|
|
509
645
|
this.inputStatesByTabId.delete(tab.id);
|
|
510
646
|
this.deferredUserMessagesByTabId.delete(tab.id);
|
|
511
647
|
this.restoreInputState(tab.id);
|
|
648
|
+
this.host.closeMenusForTabSwitch?.();
|
|
512
649
|
this.stopAttentionBlinkIfIdle();
|
|
513
650
|
this.host.resetSessionView();
|
|
514
651
|
this.restoreDeferredUserMessages(tab.id);
|
|
@@ -522,6 +659,7 @@ export class AppTabsController {
|
|
|
522
659
|
this.host.setSessionStatus(runtime.session);
|
|
523
660
|
this.host.setSessionActivity(this.sessionActivity(runtime.session));
|
|
524
661
|
void this.saveTabs();
|
|
662
|
+
this.scheduleProjectSessionRetention();
|
|
525
663
|
this.host.render();
|
|
526
664
|
}
|
|
527
665
|
async loadActiveSessionHistory(runtime) {
|
|
@@ -539,6 +677,7 @@ export class AppTabsController {
|
|
|
539
677
|
if (!isCancelled())
|
|
540
678
|
this.host.render();
|
|
541
679
|
},
|
|
680
|
+
lazyOlderHistory: true,
|
|
542
681
|
});
|
|
543
682
|
if (!completed || isCancelled())
|
|
544
683
|
return;
|
|
@@ -583,6 +722,12 @@ export class AppTabsController {
|
|
|
583
722
|
activeTab() {
|
|
584
723
|
return this.activeTabId ? this.tabItems.find((tab) => tab.id === this.activeTabId) : undefined;
|
|
585
724
|
}
|
|
725
|
+
settleStartupTabPlaceholders() {
|
|
726
|
+
for (const tab of this.tabItems) {
|
|
727
|
+
if (tab.titlePlaceholder === "loading")
|
|
728
|
+
tab.titlePlaceholder = "new";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
586
731
|
storeActiveRuntime(runtime = this.host.runtime()) {
|
|
587
732
|
if (!this.activeTabId || !runtime)
|
|
588
733
|
return;
|
|
@@ -643,9 +788,11 @@ export class AppTabsController {
|
|
|
643
788
|
if (!this.activeTabId)
|
|
644
789
|
return;
|
|
645
790
|
const state = this.host.captureInputState();
|
|
791
|
+
const attachments = state.attachments?.map(clonePersistedAttachment) ?? [];
|
|
646
792
|
this.inputStatesByTabId.set(this.activeTabId, {
|
|
647
793
|
text: state.text,
|
|
648
794
|
cursor: state.cursor,
|
|
795
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
649
796
|
});
|
|
650
797
|
}
|
|
651
798
|
storeActiveDeferredUserMessages() {
|
|
@@ -662,6 +809,9 @@ export class AppTabsController {
|
|
|
662
809
|
restoreInputState(tabId) {
|
|
663
810
|
this.host.restoreInputState(this.inputStatesByTabId.get(tabId) ?? { text: "", cursor: 0 });
|
|
664
811
|
}
|
|
812
|
+
inputStateFromText(text) {
|
|
813
|
+
return { text, cursor: text.length };
|
|
814
|
+
}
|
|
665
815
|
restoreDeferredUserMessages(tabId) {
|
|
666
816
|
this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
|
|
667
817
|
}
|
|
@@ -712,6 +862,7 @@ export class AppTabsController {
|
|
|
712
862
|
this.tabItems.push({
|
|
713
863
|
id: tab.id,
|
|
714
864
|
title: tab.title,
|
|
865
|
+
...(tab.titlePlaceholder ? { titlePlaceholder: tab.titlePlaceholder } : {}),
|
|
715
866
|
status: "waiting",
|
|
716
867
|
activity: tab.activity ?? "idle",
|
|
717
868
|
...(sessionPath ? { sessionPath } : {}),
|
|
@@ -800,8 +951,11 @@ export class AppTabsController {
|
|
|
800
951
|
return session.sessionFile ? resolve(session.sessionFile) : undefined;
|
|
801
952
|
}
|
|
802
953
|
sessionTitle(session) {
|
|
803
|
-
|
|
804
|
-
|
|
954
|
+
return this.sessionTitleFromParts(session.sessionId, session.sessionName);
|
|
955
|
+
}
|
|
956
|
+
sessionTitleFromParts(sessionId, sessionName) {
|
|
957
|
+
const name = sessionName?.trim();
|
|
958
|
+
return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : `session ${sessionId.slice(0, 8)}`;
|
|
805
959
|
}
|
|
806
960
|
sessionActivity(session) {
|
|
807
961
|
return session?.isStreaming || session?.isCompacting ? "running" : "idle";
|
|
@@ -833,19 +987,7 @@ export class AppTabsController {
|
|
|
833
987
|
initialVisible: true,
|
|
834
988
|
});
|
|
835
989
|
}
|
|
836
|
-
|
|
837
|
-
try {
|
|
838
|
-
const sessions = await SessionManager.list(this.host.options.cwd);
|
|
839
|
-
return new Map(sessions.map((session) => [
|
|
840
|
-
resolve(session.path),
|
|
841
|
-
(session.name?.trim() || `session ${session.id.slice(0, 8)}`),
|
|
842
|
-
]));
|
|
843
|
-
}
|
|
844
|
-
catch {
|
|
845
|
-
return new Map();
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
restoredTabs(saved, titles) {
|
|
990
|
+
restoredTabs(saved) {
|
|
849
991
|
const tabs = [];
|
|
850
992
|
const seen = new Set();
|
|
851
993
|
for (const tab of saved.tabs) {
|
|
@@ -855,10 +997,13 @@ export class AppTabsController {
|
|
|
855
997
|
if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasDeferredQueue))
|
|
856
998
|
continue;
|
|
857
999
|
seen.add(sessionPath);
|
|
858
|
-
const
|
|
1000
|
+
const savedTitle = tab.title?.trim();
|
|
1001
|
+
const restoredLoadingTitle = savedTitle !== undefined && LOADING_TAB_TITLE_PATTERN.test(savedTitle);
|
|
1002
|
+
const title = restoredLoadingTitle ? this.defaultSessionTitleFromPath(sessionPath) : savedTitle;
|
|
859
1003
|
tabs.push({
|
|
860
1004
|
id: createId("tab"),
|
|
861
1005
|
title: title || "session",
|
|
1006
|
+
...(restoredLoadingTitle ? { titlePlaceholder: "new" } : {}),
|
|
862
1007
|
status: "waiting",
|
|
863
1008
|
sessionPath,
|
|
864
1009
|
});
|
|
@@ -867,6 +1012,12 @@ export class AppTabsController {
|
|
|
867
1012
|
}
|
|
868
1013
|
return tabs;
|
|
869
1014
|
}
|
|
1015
|
+
defaultSessionTitleFromPath(sessionPath) {
|
|
1016
|
+
const fileName = basename(sessionPath, extname(sessionPath));
|
|
1017
|
+
const sessionId = /^[0-9a-f]{8}/iu.exec(fileName)?.[0]?.toLowerCase()
|
|
1018
|
+
?? createHash("sha256").update(sessionPath).digest("hex").slice(0, 8);
|
|
1019
|
+
return `session ${sessionId}`;
|
|
1020
|
+
}
|
|
870
1021
|
async loadTabs() {
|
|
871
1022
|
try {
|
|
872
1023
|
const raw = await readFile(this.filePath(), "utf8");
|
|
@@ -905,7 +1056,10 @@ export class AppTabsController {
|
|
|
905
1056
|
const cursor = typeof value.cursor === "number" && Number.isFinite(value.cursor)
|
|
906
1057
|
? Math.max(0, Math.min(value.text.length, Math.trunc(value.cursor)))
|
|
907
1058
|
: value.text.length;
|
|
908
|
-
|
|
1059
|
+
const attachments = Array.isArray(value.attachments)
|
|
1060
|
+
? value.attachments.flatMap(parsePersistedAttachment)
|
|
1061
|
+
: [];
|
|
1062
|
+
return { text: value.text, cursor, ...(attachments.length > 0 ? { attachments } : {}) };
|
|
909
1063
|
}
|
|
910
1064
|
parsePersistedSubmittedUserMessages(value) {
|
|
911
1065
|
if (!Array.isArray(value))
|
|
@@ -943,10 +1097,11 @@ export class AppTabsController {
|
|
|
943
1097
|
seen.add(sessionPath);
|
|
944
1098
|
const persistedTab = { path: sessionPath, title: tab.title };
|
|
945
1099
|
const input = this.inputStatesByTabId.get(tab.id);
|
|
946
|
-
if (input
|
|
1100
|
+
if (input && (input.text.length > 0 || (input.attachments?.length ?? 0) > 0)) {
|
|
947
1101
|
persistedTab.input = {
|
|
948
1102
|
text: input.text,
|
|
949
1103
|
cursor: Math.max(0, Math.min(input.text.length, Math.trunc(input.cursor))),
|
|
1104
|
+
...(input.attachments && input.attachments.length > 0 ? { attachments: input.attachments.map(clonePersistedAttachment) } : {}),
|
|
950
1105
|
};
|
|
951
1106
|
}
|
|
952
1107
|
const deferredUserMessages = this.deferredUserMessagesByTabId.get(tab.id);
|
|
@@ -978,4 +1133,128 @@ export class AppTabsController {
|
|
|
978
1133
|
const key = createHash("sha256").update(resolve(this.host.options.cwd)).digest("hex").slice(0, 24);
|
|
979
1134
|
return join(getAgentDir(), "pix", "tabs", `${key}.json`);
|
|
980
1135
|
}
|
|
1136
|
+
sessionDir() {
|
|
1137
|
+
const safePath = `--${resolve(this.host.options.cwd).replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
1138
|
+
return join(getAgentDir(), "sessions", safePath);
|
|
1139
|
+
}
|
|
1140
|
+
scheduleProjectSessionRetention() {
|
|
1141
|
+
if (this.host.options.noSession || this.maxProjectSessions() <= 0 || this.retentionCleanupScheduled || this.retentionCleanupRunning)
|
|
1142
|
+
return;
|
|
1143
|
+
this.retentionCleanupScheduled = true;
|
|
1144
|
+
setTimeout(() => {
|
|
1145
|
+
this.retentionCleanupScheduled = false;
|
|
1146
|
+
void this.cleanupOldProjectSessions();
|
|
1147
|
+
}, 0);
|
|
1148
|
+
}
|
|
1149
|
+
async cleanupOldProjectSessions() {
|
|
1150
|
+
if (this.retentionCleanupRunning)
|
|
1151
|
+
return;
|
|
1152
|
+
this.retentionCleanupRunning = true;
|
|
1153
|
+
try {
|
|
1154
|
+
const maxProjectSessions = this.maxProjectSessions();
|
|
1155
|
+
if (maxProjectSessions <= 0)
|
|
1156
|
+
return;
|
|
1157
|
+
const sessionDir = this.sessionDir();
|
|
1158
|
+
const preserved = this.preservedSessionPaths();
|
|
1159
|
+
const entries = await readdir(sessionDir, { withFileTypes: true });
|
|
1160
|
+
const sessions = [];
|
|
1161
|
+
for (const entry of entries) {
|
|
1162
|
+
if (!entry.isFile() || extname(entry.name) !== ".jsonl")
|
|
1163
|
+
continue;
|
|
1164
|
+
const path = resolve(sessionDir, entry.name);
|
|
1165
|
+
try {
|
|
1166
|
+
const info = await stat(path);
|
|
1167
|
+
sessions.push({ path, modifiedMs: info.mtimeMs });
|
|
1168
|
+
}
|
|
1169
|
+
catch {
|
|
1170
|
+
// Ignore files that disappear while cleanup is scanning.
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (sessions.length <= maxProjectSessions)
|
|
1174
|
+
return;
|
|
1175
|
+
sessions.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
1176
|
+
const keep = new Set(preserved);
|
|
1177
|
+
for (const session of sessions) {
|
|
1178
|
+
if (keep.size >= maxProjectSessions)
|
|
1179
|
+
break;
|
|
1180
|
+
keep.add(session.path);
|
|
1181
|
+
}
|
|
1182
|
+
for (const session of sessions) {
|
|
1183
|
+
if (keep.has(session.path))
|
|
1184
|
+
continue;
|
|
1185
|
+
try {
|
|
1186
|
+
await unlink(session.path);
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
// Session retention must never interrupt the terminal UI.
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
// Session retention must never interrupt the terminal UI.
|
|
1195
|
+
}
|
|
1196
|
+
finally {
|
|
1197
|
+
this.retentionCleanupRunning = false;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
preservedSessionPaths() {
|
|
1201
|
+
const preserved = new Set();
|
|
1202
|
+
const add = (sessionPath) => {
|
|
1203
|
+
if (sessionPath)
|
|
1204
|
+
preserved.add(resolve(sessionPath));
|
|
1205
|
+
};
|
|
1206
|
+
add(this.host.options.sessionPath);
|
|
1207
|
+
add(this.host.runtime()?.session.sessionFile);
|
|
1208
|
+
for (const tab of this.tabItems)
|
|
1209
|
+
add(tab.sessionPath);
|
|
1210
|
+
return preserved;
|
|
1211
|
+
}
|
|
1212
|
+
maxProjectSessions() {
|
|
1213
|
+
const value = this.host.maxProjectSessions;
|
|
1214
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function parsePersistedAttachment(value) {
|
|
1218
|
+
if (!isRecord(value) || typeof value.kind !== "string" || typeof value.tag !== "string")
|
|
1219
|
+
return [];
|
|
1220
|
+
if (value.kind === "image") {
|
|
1221
|
+
const image = parsePersistedImage(value.image);
|
|
1222
|
+
return image ? [{ kind: "image", tag: value.tag, image }] : [];
|
|
1223
|
+
}
|
|
1224
|
+
if (value.kind === "pasted-text") {
|
|
1225
|
+
if (typeof value.text !== "string" || typeof value.lineCount !== "number" || !Number.isFinite(value.lineCount))
|
|
1226
|
+
return [];
|
|
1227
|
+
return [{ kind: "pasted-text", tag: value.tag, text: value.text, lineCount: Math.max(1, Math.trunc(value.lineCount)) }];
|
|
1228
|
+
}
|
|
1229
|
+
if (value.kind === "file") {
|
|
1230
|
+
if (typeof value.path !== "string")
|
|
1231
|
+
return [];
|
|
1232
|
+
const image = parsePersistedImage(value.image);
|
|
1233
|
+
return [{
|
|
1234
|
+
kind: "file",
|
|
1235
|
+
tag: value.tag,
|
|
1236
|
+
path: value.path,
|
|
1237
|
+
...(typeof value.content === "string" ? { content: value.content } : {}),
|
|
1238
|
+
...(image ? { image } : {}),
|
|
1239
|
+
}];
|
|
1240
|
+
}
|
|
1241
|
+
return [];
|
|
1242
|
+
}
|
|
1243
|
+
function parsePersistedImage(value) {
|
|
1244
|
+
return isRecord(value) && value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string"
|
|
1245
|
+
? { type: "image", data: value.data, mimeType: value.mimeType }
|
|
1246
|
+
: undefined;
|
|
1247
|
+
}
|
|
1248
|
+
function clonePersistedAttachment(attachment) {
|
|
1249
|
+
if (attachment.kind === "image")
|
|
1250
|
+
return { kind: "image", tag: attachment.tag, image: { ...attachment.image } };
|
|
1251
|
+
if (attachment.kind === "pasted-text")
|
|
1252
|
+
return { kind: "pasted-text", tag: attachment.tag, text: attachment.text, lineCount: attachment.lineCount };
|
|
1253
|
+
return {
|
|
1254
|
+
kind: "file",
|
|
1255
|
+
tag: attachment.tag,
|
|
1256
|
+
path: attachment.path,
|
|
1257
|
+
...(attachment.content === undefined ? {} : { content: attachment.content }),
|
|
1258
|
+
...(attachment.image === undefined ? {} : { image: { ...attachment.image } }),
|
|
1259
|
+
};
|
|
981
1260
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { StyledSegment, TodoAction, TodoDetails, TodoLiveStateEvent,
|
|
1
|
+
import type { StyledSegment, ThinkingLevel, TodoAction, TodoDetails, TodoLiveStateEvent, TodoStatus, TodoTask, TodoTaskLinePart, TodoTaskRow } from "../types.js";
|
|
2
2
|
export declare function isTodoAction(value: unknown): value is TodoAction;
|
|
3
3
|
export declare function isTodoStatus(value: unknown): value is TodoStatus;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function isTodoThinkingLevel(value: unknown): value is ThinkingLevel;
|
|
5
5
|
export declare function isTodoTask(value: unknown): value is TodoTask;
|
|
6
6
|
export declare function isTodoDetails(value: unknown): value is TodoDetails;
|
|
7
7
|
export declare function isTodoLiveStateEvent(value: unknown): value is TodoLiveStateEvent;
|
|
@@ -17,6 +17,8 @@ export declare function formatTodoTaskLine(task: TodoTask, options?: {
|
|
|
17
17
|
}): string;
|
|
18
18
|
export declare function todoTaskLineSegments(task: TodoTask, mutedColor: string, options?: {
|
|
19
19
|
depth?: number;
|
|
20
|
+
thinkingColor?: (level: ThinkingLevel) => string;
|
|
21
|
+
statusColor?: (status: TodoStatus) => string;
|
|
20
22
|
}): StyledSegment[];
|
|
21
23
|
export declare function shiftSegmentsToSlice(segments: readonly StyledSegment[], start: number, length: number): StyledSegment[];
|
|
22
24
|
export declare function formatTodoPanelStats(tasks: readonly TodoTask[]): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isNumberArray, isRecord
|
|
1
|
+
import { THINKING_LEVELS, TODO_ACTIONS, TODO_STATUSES } from "../constants.js";
|
|
2
|
+
import { isNumberArray, isRecord } from "../guards.js";
|
|
3
3
|
import { APP_ICONS } from "../icons.js";
|
|
4
4
|
export function isTodoAction(value) {
|
|
5
5
|
return typeof value === "string" && TODO_ACTIONS.includes(value);
|
|
@@ -7,8 +7,8 @@ export function isTodoAction(value) {
|
|
|
7
7
|
export function isTodoStatus(value) {
|
|
8
8
|
return typeof value === "string" && TODO_STATUSES.includes(value);
|
|
9
9
|
}
|
|
10
|
-
export function
|
|
11
|
-
return typeof value === "string" &&
|
|
10
|
+
export function isTodoThinkingLevel(value) {
|
|
11
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
12
12
|
}
|
|
13
13
|
export function isTodoTask(value) {
|
|
14
14
|
if (!isRecord(value))
|
|
@@ -23,14 +23,12 @@ export function isTodoTask(value) {
|
|
|
23
23
|
return false;
|
|
24
24
|
if (value.activeForm !== undefined && typeof value.activeForm !== "string")
|
|
25
25
|
return false;
|
|
26
|
-
if (value.
|
|
26
|
+
if (value.thinking !== undefined && !isTodoThinkingLevel(value.thinking))
|
|
27
27
|
return false;
|
|
28
28
|
if (value.parentId !== undefined && typeof value.parentId !== "number")
|
|
29
29
|
return false;
|
|
30
30
|
if (value.blockedBy !== undefined && !isNumberArray(value.blockedBy))
|
|
31
31
|
return false;
|
|
32
|
-
if (value.tags !== undefined && !isStringArray(value.tags))
|
|
33
|
-
return false;
|
|
34
32
|
if (value.owner !== undefined && typeof value.owner !== "string")
|
|
35
33
|
return false;
|
|
36
34
|
if (value.metadata !== undefined && !isRecord(value.metadata))
|
|
@@ -116,17 +114,19 @@ export function visibleTodoTaskRows(details, showDeleted = false) {
|
|
|
116
114
|
}
|
|
117
115
|
export function todoTaskLineParts(task, options = {}) {
|
|
118
116
|
const treePrefix = todoTaskTreePrefix(options.depth ?? 0);
|
|
119
|
-
const
|
|
117
|
+
const subjectPart = { text: `${task.id}.${task.subject}` };
|
|
118
|
+
if (task.thinking)
|
|
119
|
+
subjectPart.thinking = task.thinking;
|
|
120
|
+
const parts = [
|
|
121
|
+
{ text: `${treePrefix}${todoStatusIcon(task.status)}` },
|
|
122
|
+
subjectPart,
|
|
123
|
+
];
|
|
120
124
|
if (task.status === "in_progress" && task.activeForm)
|
|
121
125
|
parts.push({ text: `— ${task.activeForm}` });
|
|
122
|
-
if (task.priority)
|
|
123
|
-
parts.push({ text: `(${task.priority})`, muted: true });
|
|
124
126
|
if (task.parentId !== undefined)
|
|
125
127
|
parts.push({ text: `parent:#${task.parentId}` });
|
|
126
128
|
if (task.blockedBy && task.blockedBy.length > 0)
|
|
127
129
|
parts.push({ text: `blocked:${task.blockedBy.map((id) => `#${id}`).join(",")}` });
|
|
128
|
-
if (task.tags && task.tags.length > 0)
|
|
129
|
-
parts.push({ text: task.tags.map((tag) => `#${tag}`).join(" "), muted: true });
|
|
130
130
|
return parts;
|
|
131
131
|
}
|
|
132
132
|
export function formatTodoTaskLine(task, options = {}) {
|
|
@@ -141,7 +141,17 @@ export function todoTaskLineSegments(task, mutedColor, options = {}) {
|
|
|
141
141
|
const start = offset;
|
|
142
142
|
const end = start + part.text.length;
|
|
143
143
|
const segment = { start, end };
|
|
144
|
-
if (
|
|
144
|
+
if (index === 0 && options.statusColor) {
|
|
145
|
+
const foreground = options.statusColor(task.status);
|
|
146
|
+
if (foreground)
|
|
147
|
+
segment.foreground = foreground;
|
|
148
|
+
}
|
|
149
|
+
else if (part.thinking) {
|
|
150
|
+
const foreground = options.thinkingColor?.(part.thinking);
|
|
151
|
+
if (foreground)
|
|
152
|
+
segment.foreground = foreground;
|
|
153
|
+
}
|
|
154
|
+
else if (part.muted)
|
|
145
155
|
segment.foreground = mutedColor;
|
|
146
156
|
if (task.status === "completed" && index > 0)
|
|
147
157
|
segment.strikethrough = true;
|