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.
Files changed (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. 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, SessionManager, } from "@earendil-works/pi-coding-agent";
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 sessionTitles = await this.loadSessionTitles();
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
- const result = await runtime.switchSession(desiredPath);
191
- if (result.cancelled)
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(runtime.session);
208
- this.host.setSessionActivity(this.sessionActivity(runtime.session));
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
- const name = session.sessionName?.trim();
804
- return name ? name : `session ${session.sessionId.slice(0, 8)}`;
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
- async loadSessionTitles() {
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 title = titles.get(sessionPath) ?? tab.title?.trim();
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
- return { text: value.text, cursor };
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?.text.length) {
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, TodoPriority, TodoStatus, TodoTask, TodoTaskLinePart, TodoTaskRow } from "../types.js";
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 isTodoPriority(value: unknown): value is TodoPriority;
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 { TODO_ACTIONS, TODO_PRIORITIES, TODO_STATUSES } from "../constants.js";
2
- import { isNumberArray, isRecord, isStringArray } from "../guards.js";
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 isTodoPriority(value) {
11
- return typeof value === "string" && TODO_PRIORITIES.includes(value);
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.priority !== undefined && !isTodoPriority(value.priority))
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 parts = [{ text: `${treePrefix}${todoStatusIcon(task.status)}` }, { text: `#${task.id}`, muted: true }, { text: task.subject }];
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 (part.muted)
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;