pi-ui-extend 0.1.13 → 0.1.15

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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +5 -0
  3. package/dist/app/app.js +82 -12
  4. package/dist/app/commands/command-controller.js +1 -0
  5. package/dist/app/commands/command-host.d.ts +3 -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.js +3 -0
  9. package/dist/app/commands/command-registry.d.ts +1 -0
  10. package/dist/app/commands/command-registry.js +8 -0
  11. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  12. package/dist/app/extensions/extension-ui-controller.js +99 -61
  13. package/dist/app/input/input-action-controller.d.ts +1 -0
  14. package/dist/app/input/input-action-controller.js +8 -2
  15. package/dist/app/logger.d.ts +25 -0
  16. package/dist/app/logger.js +90 -0
  17. package/dist/app/model/model-usage-status.js +30 -15
  18. package/dist/app/popup/menu-items-controller.d.ts +2 -0
  19. package/dist/app/popup/menu-items-controller.js +45 -6
  20. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  21. package/dist/app/popup/popup-action-controller.js +7 -4
  22. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  23. package/dist/app/popup/popup-menu-controller.js +68 -322
  24. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  25. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  26. package/dist/app/rendering/conversation-viewport.js +157 -16
  27. package/dist/app/rendering/editor-panels.js +4 -2
  28. package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
  29. package/dist/app/rendering/popup-menu-renderer.js +307 -0
  30. package/dist/app/rendering/render-controller.js +5 -13
  31. package/dist/app/rendering/status-line-renderer.d.ts +1 -1
  32. package/dist/app/rendering/status-line-renderer.js +27 -24
  33. package/dist/app/rendering/toast-controller.d.ts +11 -3
  34. package/dist/app/rendering/toast-controller.js +53 -12
  35. package/dist/app/runtime.d.ts +2 -1
  36. package/dist/app/runtime.js +20 -10
  37. package/dist/app/screen/mouse-controller.d.ts +2 -2
  38. package/dist/app/screen/mouse-controller.js +27 -48
  39. package/dist/app/screen/screen-styler.d.ts +1 -1
  40. package/dist/app/screen/screen-styler.js +9 -7
  41. package/dist/app/screen/scroll-controller.d.ts +11 -9
  42. package/dist/app/screen/scroll-controller.js +50 -45
  43. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  44. package/dist/app/session/lazy-session-manager.js +539 -0
  45. package/dist/app/session/pix-system-message.d.ts +16 -0
  46. package/dist/app/session/pix-system-message.js +64 -0
  47. package/dist/app/session/session-event-controller.d.ts +11 -0
  48. package/dist/app/session/session-event-controller.js +58 -2
  49. package/dist/app/session/session-history.d.ts +18 -0
  50. package/dist/app/session/session-history.js +72 -3
  51. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  52. package/dist/app/session/session-lifecycle-controller.js +7 -2
  53. package/dist/app/session/tabs-controller.d.ts +13 -1
  54. package/dist/app/session/tabs-controller.js +248 -27
  55. package/dist/app/todo/todo-model.d.ts +3 -1
  56. package/dist/app/todo/todo-model.js +14 -2
  57. package/dist/app/types.d.ts +5 -2
  58. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  59. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  60. package/dist/config.d.ts +5 -1
  61. package/dist/config.js +73 -25
  62. package/dist/default-pix-config.js +2 -0
  63. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  64. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  65. package/dist/schemas/pix-schema.d.ts +2 -1
  66. package/dist/schemas/pix-schema.js +5 -4
  67. package/dist/terminal-width.d.ts +2 -0
  68. package/dist/terminal-width.js +64 -3
  69. package/external/pi-tools-suite/README.md +1 -0
  70. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
  71. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
  72. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
  73. package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
  74. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
  75. package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
  76. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
  77. package/external/pi-tools-suite/src/config.ts +8 -0
  78. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  79. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  80. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  81. package/external/pi-tools-suite/src/todo/index.ts +181 -11
  82. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
  83. package/external/pi-tools-suite/src/todo/todo.ts +10 -5
  84. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
  85. package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
  86. package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
  87. package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
  88. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  89. package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
  90. package/package.json +1 -1
  91. package/schemas/pi-tools-suite.json +4 -0
  92. package/schemas/pix.json +6 -2
@@ -1,15 +1,17 @@
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";
9
9
  import { tabPanelRows } from "../rendering/tab-line-renderer.js";
10
10
  const TAB_STATE_VERSION = 3;
11
11
  const MAX_RESTORED_TABS = 8;
12
+ const MAX_PROJECT_SESSIONS = 20;
12
13
  const TAB_ATTENTION_BLINK_KEY = "tab-attention";
14
+ const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
13
15
  export class AppTabsController {
14
16
  host;
15
17
  tabItems = [];
@@ -21,6 +23,8 @@ export class AppTabsController {
21
23
  pendingActiveTabId;
22
24
  historyLoadGeneration = 0;
23
25
  restored = false;
26
+ retentionCleanupRunning = false;
27
+ retentionCleanupScheduled = false;
24
28
  constructor(host) {
25
29
  this.host = host;
26
30
  }
@@ -153,17 +157,21 @@ export class AppTabsController {
153
157
  if (!runtime)
154
158
  return;
155
159
  this.syncActiveTabFromRuntime({ save: false });
156
- if (this.host.options.noSession)
160
+ if (this.host.options.noSession) {
161
+ this.settleStartupTabPlaceholders();
157
162
  return;
163
+ }
158
164
  const saved = await this.loadTabs();
159
165
  if (!saved || saved.tabs.length === 0) {
166
+ this.settleStartupTabPlaceholders();
160
167
  await this.saveTabs();
161
168
  return;
162
169
  }
163
- const sessionTitles = await this.loadSessionTitles();
164
- const restoredTabs = this.restoredTabs(saved, sessionTitles);
170
+ const restoredTabs = this.restoredTabs(saved);
165
171
  if (restoredTabs.length === 0) {
172
+ this.settleStartupTabPlaceholders();
166
173
  await this.saveTabs();
174
+ this.scheduleProjectSessionRetention();
167
175
  return;
168
176
  }
169
177
  const currentPath = runtime.session.sessionFile ? resolve(runtime.session.sessionFile) : undefined;
@@ -180,35 +188,41 @@ export class AppTabsController {
180
188
  if (explicitSessionPath && currentPath)
181
189
  this.ensureCurrentSessionTab(runtime.session);
182
190
  if (!desiredPath) {
191
+ this.settleStartupTabPlaceholders();
183
192
  await this.saveTabs();
193
+ this.scheduleProjectSessionRetention();
184
194
  return;
185
195
  }
196
+ let restoredRuntime = runtime;
186
197
  if (currentPath !== desiredPath) {
187
198
  this.host.setStatus("restoring tabs");
188
199
  this.host.render();
189
200
  try {
190
- const result = await runtime.switchSession(desiredPath);
191
- if (result.cancelled)
192
- throw new Error("restore cancelled");
201
+ restoredRuntime = await this.host.createRuntimeForSession(desiredPath);
202
+ await this.host.activateRuntime(restoredRuntime);
193
203
  }
194
204
  catch {
195
205
  this.host.showToast("Could not restore the previous active tab", "warning");
196
206
  this.replaceTabs([this.tabFromSession(runtime.session), ...restoredTabs], currentPath);
197
207
  this.storeActiveRuntime(runtime);
208
+ this.settleStartupTabPlaceholders();
198
209
  await this.saveTabs();
210
+ this.scheduleProjectSessionRetention();
199
211
  return;
200
212
  }
201
213
  }
202
214
  this.syncActiveTabFromRuntime({ save: false });
215
+ this.settleStartupTabPlaceholders();
203
216
  this.host.resetSessionView();
204
217
  if (this.activeTabId)
205
218
  this.restoreDeferredUserMessages(this.activeTabId);
206
219
  this.host.loadSessionHistory();
207
- this.host.setSessionStatus(runtime.session);
208
- this.host.setSessionActivity(this.sessionActivity(runtime.session));
220
+ this.host.setSessionStatus(restoredRuntime.session);
221
+ this.host.setSessionActivity(this.sessionActivity(restoredRuntime.session));
209
222
  if (this.activeTabId)
210
223
  this.restoreInputState(this.activeTabId);
211
224
  await this.saveTabs();
225
+ this.scheduleProjectSessionRetention();
212
226
  }
213
227
  async openNewTab() {
214
228
  if (this.pendingActiveTabId) {
@@ -237,6 +251,7 @@ export class AppTabsController {
237
251
  this.updateTabFromSession(tab, newRuntime.session);
238
252
  this.setRuntimeForTab(tab.id, newRuntime);
239
253
  this.restoreInputState(tab.id);
254
+ this.host.closeMenusForTabSwitch?.();
240
255
  try {
241
256
  await this.host.activateRuntime(newRuntime);
242
257
  }
@@ -245,6 +260,7 @@ export class AppTabsController {
245
260
  this.pendingActiveTabId = undefined;
246
261
  }
247
262
  void this.saveTabs();
263
+ this.scheduleProjectSessionRetention();
248
264
  this.host.resetSessionView();
249
265
  this.restoreDeferredUserMessages(tab.id);
250
266
  if (isEmptyStartupSession(newRuntime)) {
@@ -307,6 +323,7 @@ export class AppTabsController {
307
323
  this.updateTabFromSession(tab, newRuntime.session);
308
324
  this.setRuntimeForTab(tab.id, newRuntime);
309
325
  this.restoreInputState(tab.id);
326
+ this.host.closeMenusForTabSwitch?.();
310
327
  try {
311
328
  await this.host.activateRuntime(newRuntime);
312
329
  }
@@ -316,6 +333,7 @@ export class AppTabsController {
316
333
  this.activeTabId = previousTabId;
317
334
  if (previousTabId)
318
335
  this.restoreInputState(previousTabId);
336
+ this.host.closeMenusForTabSwitch?.();
319
337
  if (this.host.runtime() !== previousRuntime) {
320
338
  try {
321
339
  await this.host.activateRuntime(previousRuntime);
@@ -346,6 +364,120 @@ export class AppTabsController {
346
364
  await this.loadActiveSessionHistory(newRuntime);
347
365
  return true;
348
366
  }
367
+ async forkSessionEntryInNewTab(entryId) {
368
+ if (this.pendingActiveTabId) {
369
+ this.host.showToast("Wait for the tab to finish loading", "info");
370
+ return false;
371
+ }
372
+ const runtime = this.idleRuntime("fork");
373
+ if (!runtime)
374
+ return false;
375
+ if (this.host.options.noSession) {
376
+ this.host.showToast("Fork in new tab is unavailable with --no-session", "warning");
377
+ return false;
378
+ }
379
+ const currentSessionPath = runtime.session.sessionFile ? resolve(runtime.session.sessionFile) : undefined;
380
+ if (!currentSessionPath) {
381
+ this.host.showToast("Fork in new tab requires a persisted session", "warning");
382
+ return false;
383
+ }
384
+ this.cancelHistoryLoad();
385
+ this.syncActiveTabFromRuntime({ save: false });
386
+ this.storeActiveInputState();
387
+ this.storeActiveDeferredUserMessages();
388
+ const previousTabId = this.activeTabId;
389
+ const previousRuntime = runtime;
390
+ this.host.setStatus("forking session tab");
391
+ this.host.render();
392
+ let forkRuntime;
393
+ try {
394
+ forkRuntime = await this.host.createRuntimeForSession(currentSessionPath);
395
+ }
396
+ catch {
397
+ this.host.showToast("Could not fork in new tab", "warning");
398
+ this.host.setSessionStatus(previousRuntime.session);
399
+ this.host.render();
400
+ return false;
401
+ }
402
+ let result;
403
+ try {
404
+ result = await forkRuntime.fork(entryId);
405
+ }
406
+ catch (error) {
407
+ void this.host.disposeRuntime(forkRuntime);
408
+ throw error;
409
+ }
410
+ if (result.cancelled) {
411
+ void this.host.disposeRuntime(forkRuntime);
412
+ this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
413
+ this.host.setSessionStatus(previousRuntime.session);
414
+ this.host.render();
415
+ return false;
416
+ }
417
+ const existingTab = this.findTabForSession(forkRuntime.session);
418
+ if (existingTab) {
419
+ if (result.selectedText)
420
+ this.inputStatesByTabId.set(existingTab.id, this.inputStateFromText(result.selectedText));
421
+ void this.host.disposeRuntime(forkRuntime);
422
+ await this.switchToTab(existingTab.id);
423
+ this.host.showToast("Fork opened in existing tab", "success");
424
+ return true;
425
+ }
426
+ const tab = this.tabFromSession(forkRuntime.session, { titlePlaceholder: "new" });
427
+ this.tabItems.push(tab);
428
+ this.activeTabId = tab.id;
429
+ this.pendingActiveTabId = tab.id;
430
+ this.clearTabAttention(tab);
431
+ this.updateTabFromSession(tab, forkRuntime.session);
432
+ this.setRuntimeForTab(tab.id, forkRuntime);
433
+ if (result.selectedText)
434
+ this.inputStatesByTabId.set(tab.id, this.inputStateFromText(result.selectedText));
435
+ this.restoreInputState(tab.id);
436
+ this.host.closeMenusForTabSwitch?.();
437
+ try {
438
+ await this.host.activateRuntime(forkRuntime);
439
+ }
440
+ catch {
441
+ this.pendingActiveTabId = undefined;
442
+ this.removeTab(tab.id);
443
+ this.activeTabId = previousTabId;
444
+ if (previousTabId)
445
+ this.restoreInputState(previousTabId);
446
+ this.host.closeMenusForTabSwitch?.();
447
+ if (this.host.runtime() !== previousRuntime) {
448
+ try {
449
+ await this.host.activateRuntime(previousRuntime);
450
+ }
451
+ catch {
452
+ // Keep the best available runtime below and surface the switch failure.
453
+ }
454
+ }
455
+ void this.host.disposeRuntime(forkRuntime);
456
+ this.host.showToast("Could not open fork tab", "warning");
457
+ this.host.resetSessionView();
458
+ if (previousTabId)
459
+ this.restoreDeferredUserMessages(previousTabId);
460
+ this.host.loadSessionHistory();
461
+ this.host.setSessionStatus(this.host.runtime()?.session);
462
+ this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
463
+ this.host.render();
464
+ return false;
465
+ }
466
+ this.pendingActiveTabId = undefined;
467
+ this.activeTabId = tab.id;
468
+ this.clearTabAttention(tab);
469
+ this.updateTabFromSession(tab, forkRuntime.session);
470
+ this.setRuntimeForTab(tab.id, forkRuntime);
471
+ this.restoreInputState(tab.id);
472
+ void this.saveTabs();
473
+ this.scheduleProjectSessionRetention();
474
+ await this.loadActiveSessionHistory(forkRuntime);
475
+ this.host.addEntry({ id: createId("system"), kind: "system", text: `Forked from entry ${entryId} in a new tab.` });
476
+ this.host.setSessionStatus(forkRuntime.session);
477
+ this.host.showToast("Fork opened in new tab", "success");
478
+ this.host.render();
479
+ return true;
480
+ }
349
481
  async switchToTab(tabId) {
350
482
  if (this.pendingActiveTabId) {
351
483
  this.host.showToast("Wait for the tab to finish loading", "info");
@@ -376,6 +508,7 @@ export class AppTabsController {
376
508
  target.activity = "thinking";
377
509
  this.clearTabAttention(target);
378
510
  this.restoreInputState(target.id);
511
+ this.host.closeMenusForTabSwitch?.();
379
512
  this.host.resetSessionView();
380
513
  this.restoreDeferredUserMessages(target.id);
381
514
  this.host.setStatus("switching tab");
@@ -397,6 +530,7 @@ export class AppTabsController {
397
530
  this.activeTabId = previousTabId;
398
531
  if (previousTabId)
399
532
  this.restoreInputState(previousTabId);
533
+ this.host.closeMenusForTabSwitch?.();
400
534
  if (this.host.runtime() !== previousRuntime) {
401
535
  try {
402
536
  await this.host.activateRuntime(previousRuntime);
@@ -481,6 +615,7 @@ export class AppTabsController {
481
615
  this.updateTabFromSession(nextTab, nextRuntime.session);
482
616
  this.setRuntimeForTab(nextTab.id, nextRuntime);
483
617
  this.restoreInputState(nextTab.id);
618
+ this.host.closeMenusForTabSwitch?.();
484
619
  void this.host.disposeRuntime(runtime);
485
620
  void this.saveTabs();
486
621
  await this.loadActiveSessionHistory(nextRuntime);
@@ -509,6 +644,7 @@ export class AppTabsController {
509
644
  this.inputStatesByTabId.delete(tab.id);
510
645
  this.deferredUserMessagesByTabId.delete(tab.id);
511
646
  this.restoreInputState(tab.id);
647
+ this.host.closeMenusForTabSwitch?.();
512
648
  this.stopAttentionBlinkIfIdle();
513
649
  this.host.resetSessionView();
514
650
  this.restoreDeferredUserMessages(tab.id);
@@ -522,6 +658,7 @@ export class AppTabsController {
522
658
  this.host.setSessionStatus(runtime.session);
523
659
  this.host.setSessionActivity(this.sessionActivity(runtime.session));
524
660
  void this.saveTabs();
661
+ this.scheduleProjectSessionRetention();
525
662
  this.host.render();
526
663
  }
527
664
  async loadActiveSessionHistory(runtime) {
@@ -539,6 +676,7 @@ export class AppTabsController {
539
676
  if (!isCancelled())
540
677
  this.host.render();
541
678
  },
679
+ lazyOlderHistory: true,
542
680
  });
543
681
  if (!completed || isCancelled())
544
682
  return;
@@ -583,6 +721,12 @@ export class AppTabsController {
583
721
  activeTab() {
584
722
  return this.activeTabId ? this.tabItems.find((tab) => tab.id === this.activeTabId) : undefined;
585
723
  }
724
+ settleStartupTabPlaceholders() {
725
+ for (const tab of this.tabItems) {
726
+ if (tab.titlePlaceholder === "loading")
727
+ tab.titlePlaceholder = "new";
728
+ }
729
+ }
586
730
  storeActiveRuntime(runtime = this.host.runtime()) {
587
731
  if (!this.activeTabId || !runtime)
588
732
  return;
@@ -662,6 +806,9 @@ export class AppTabsController {
662
806
  restoreInputState(tabId) {
663
807
  this.host.restoreInputState(this.inputStatesByTabId.get(tabId) ?? { text: "", cursor: 0 });
664
808
  }
809
+ inputStateFromText(text) {
810
+ return { text, cursor: text.length };
811
+ }
665
812
  restoreDeferredUserMessages(tabId) {
666
813
  this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
667
814
  }
@@ -712,6 +859,7 @@ export class AppTabsController {
712
859
  this.tabItems.push({
713
860
  id: tab.id,
714
861
  title: tab.title,
862
+ ...(tab.titlePlaceholder ? { titlePlaceholder: tab.titlePlaceholder } : {}),
715
863
  status: "waiting",
716
864
  activity: tab.activity ?? "idle",
717
865
  ...(sessionPath ? { sessionPath } : {}),
@@ -800,8 +948,11 @@ export class AppTabsController {
800
948
  return session.sessionFile ? resolve(session.sessionFile) : undefined;
801
949
  }
802
950
  sessionTitle(session) {
803
- const name = session.sessionName?.trim();
804
- return name ? name : `session ${session.sessionId.slice(0, 8)}`;
951
+ return this.sessionTitleFromParts(session.sessionId, session.sessionName);
952
+ }
953
+ sessionTitleFromParts(sessionId, sessionName) {
954
+ const name = sessionName?.trim();
955
+ return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : `session ${sessionId.slice(0, 8)}`;
805
956
  }
806
957
  sessionActivity(session) {
807
958
  return session?.isStreaming || session?.isCompacting ? "running" : "idle";
@@ -833,19 +984,7 @@ export class AppTabsController {
833
984
  initialVisible: true,
834
985
  });
835
986
  }
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) {
987
+ restoredTabs(saved) {
849
988
  const tabs = [];
850
989
  const seen = new Set();
851
990
  for (const tab of saved.tabs) {
@@ -855,10 +994,13 @@ export class AppTabsController {
855
994
  if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasDeferredQueue))
856
995
  continue;
857
996
  seen.add(sessionPath);
858
- const title = titles.get(sessionPath) ?? tab.title?.trim();
997
+ const savedTitle = tab.title?.trim();
998
+ const restoredLoadingTitle = savedTitle !== undefined && LOADING_TAB_TITLE_PATTERN.test(savedTitle);
999
+ const title = restoredLoadingTitle ? this.defaultSessionTitleFromPath(sessionPath) : savedTitle;
859
1000
  tabs.push({
860
1001
  id: createId("tab"),
861
1002
  title: title || "session",
1003
+ ...(restoredLoadingTitle ? { titlePlaceholder: "new" } : {}),
862
1004
  status: "waiting",
863
1005
  sessionPath,
864
1006
  });
@@ -867,6 +1009,12 @@ export class AppTabsController {
867
1009
  }
868
1010
  return tabs;
869
1011
  }
1012
+ defaultSessionTitleFromPath(sessionPath) {
1013
+ const fileName = basename(sessionPath, extname(sessionPath));
1014
+ const sessionId = /^[0-9a-f]{8}/iu.exec(fileName)?.[0]?.toLowerCase()
1015
+ ?? createHash("sha256").update(sessionPath).digest("hex").slice(0, 8);
1016
+ return `session ${sessionId}`;
1017
+ }
870
1018
  async loadTabs() {
871
1019
  try {
872
1020
  const raw = await readFile(this.filePath(), "utf8");
@@ -978,4 +1126,77 @@ export class AppTabsController {
978
1126
  const key = createHash("sha256").update(resolve(this.host.options.cwd)).digest("hex").slice(0, 24);
979
1127
  return join(getAgentDir(), "pix", "tabs", `${key}.json`);
980
1128
  }
1129
+ sessionDir() {
1130
+ const safePath = `--${resolve(this.host.options.cwd).replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
1131
+ return join(getAgentDir(), "sessions", safePath);
1132
+ }
1133
+ scheduleProjectSessionRetention() {
1134
+ if (this.host.options.noSession || this.retentionCleanupScheduled || this.retentionCleanupRunning)
1135
+ return;
1136
+ this.retentionCleanupScheduled = true;
1137
+ setTimeout(() => {
1138
+ this.retentionCleanupScheduled = false;
1139
+ void this.cleanupOldProjectSessions();
1140
+ }, 0);
1141
+ }
1142
+ async cleanupOldProjectSessions() {
1143
+ if (this.retentionCleanupRunning)
1144
+ return;
1145
+ this.retentionCleanupRunning = true;
1146
+ try {
1147
+ const sessionDir = this.sessionDir();
1148
+ const preserved = this.preservedSessionPaths();
1149
+ const entries = await readdir(sessionDir, { withFileTypes: true });
1150
+ const sessions = [];
1151
+ for (const entry of entries) {
1152
+ if (!entry.isFile() || extname(entry.name) !== ".jsonl")
1153
+ continue;
1154
+ const path = resolve(sessionDir, entry.name);
1155
+ try {
1156
+ const info = await stat(path);
1157
+ sessions.push({ path, modifiedMs: info.mtimeMs });
1158
+ }
1159
+ catch {
1160
+ // Ignore files that disappear while cleanup is scanning.
1161
+ }
1162
+ }
1163
+ if (sessions.length <= MAX_PROJECT_SESSIONS)
1164
+ return;
1165
+ sessions.sort((a, b) => b.modifiedMs - a.modifiedMs);
1166
+ const keep = new Set(preserved);
1167
+ for (const session of sessions) {
1168
+ if (keep.size >= MAX_PROJECT_SESSIONS)
1169
+ break;
1170
+ keep.add(session.path);
1171
+ }
1172
+ for (const session of sessions) {
1173
+ if (keep.has(session.path))
1174
+ continue;
1175
+ try {
1176
+ await unlink(session.path);
1177
+ }
1178
+ catch {
1179
+ // Session retention must never interrupt the terminal UI.
1180
+ }
1181
+ }
1182
+ }
1183
+ catch {
1184
+ // Session retention must never interrupt the terminal UI.
1185
+ }
1186
+ finally {
1187
+ this.retentionCleanupRunning = false;
1188
+ }
1189
+ }
1190
+ preservedSessionPaths() {
1191
+ const preserved = new Set();
1192
+ const add = (sessionPath) => {
1193
+ if (sessionPath)
1194
+ preserved.add(resolve(sessionPath));
1195
+ };
1196
+ add(this.host.options.sessionPath);
1197
+ add(this.host.runtime()?.session.sessionFile);
1198
+ for (const tab of this.tabItems)
1199
+ add(tab.sessionPath);
1200
+ return preserved;
1201
+ }
981
1202
  }
@@ -1,7 +1,8 @@
1
- import type { StyledSegment, TodoAction, TodoDetails, TodoLiveStateEvent, TodoPriority, TodoStatus, TodoTask, TodoTaskLinePart, TodoTaskRow } from "../types.js";
1
+ import type { StyledSegment, ThinkingLevel, TodoAction, TodoDetails, TodoLiveStateEvent, TodoPriority, 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
4
  export declare function isTodoPriority(value: unknown): value is TodoPriority;
5
+ export declare function isTodoThinkingLevel(value: unknown): value is ThinkingLevel;
5
6
  export declare function isTodoTask(value: unknown): value is TodoTask;
6
7
  export declare function isTodoDetails(value: unknown): value is TodoDetails;
7
8
  export declare function isTodoLiveStateEvent(value: unknown): value is TodoLiveStateEvent;
@@ -17,6 +18,7 @@ export declare function formatTodoTaskLine(task: TodoTask, options?: {
17
18
  }): string;
18
19
  export declare function todoTaskLineSegments(task: TodoTask, mutedColor: string, options?: {
19
20
  depth?: number;
21
+ thinkingColor?: (level: ThinkingLevel) => 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,4 +1,4 @@
1
- import { TODO_ACTIONS, TODO_PRIORITIES, TODO_STATUSES } from "../constants.js";
1
+ import { THINKING_LEVELS, TODO_ACTIONS, TODO_PRIORITIES, TODO_STATUSES } from "../constants.js";
2
2
  import { isNumberArray, isRecord, isStringArray } from "../guards.js";
3
3
  import { APP_ICONS } from "../icons.js";
4
4
  export function isTodoAction(value) {
@@ -10,6 +10,9 @@ export function isTodoStatus(value) {
10
10
  export function isTodoPriority(value) {
11
11
  return typeof value === "string" && TODO_PRIORITIES.includes(value);
12
12
  }
13
+ export function isTodoThinkingLevel(value) {
14
+ return typeof value === "string" && THINKING_LEVELS.includes(value);
15
+ }
13
16
  export function isTodoTask(value) {
14
17
  if (!isRecord(value))
15
18
  return false;
@@ -25,6 +28,8 @@ export function isTodoTask(value) {
25
28
  return false;
26
29
  if (value.priority !== undefined && !isTodoPriority(value.priority))
27
30
  return false;
31
+ if (value.thinking !== undefined && !isTodoThinkingLevel(value.thinking))
32
+ return false;
28
33
  if (value.parentId !== undefined && typeof value.parentId !== "number")
29
34
  return false;
30
35
  if (value.blockedBy !== undefined && !isNumberArray(value.blockedBy))
@@ -121,6 +126,8 @@ export function todoTaskLineParts(task, options = {}) {
121
126
  parts.push({ text: `— ${task.activeForm}` });
122
127
  if (task.priority)
123
128
  parts.push({ text: `(${task.priority})`, muted: true });
129
+ if (task.thinking)
130
+ parts.push({ text: `[${task.thinking}]`, thinking: task.thinking });
124
131
  if (task.parentId !== undefined)
125
132
  parts.push({ text: `parent:#${task.parentId}` });
126
133
  if (task.blockedBy && task.blockedBy.length > 0)
@@ -141,7 +148,12 @@ export function todoTaskLineSegments(task, mutedColor, options = {}) {
141
148
  const start = offset;
142
149
  const end = start + part.text.length;
143
150
  const segment = { start, end };
144
- if (part.muted)
151
+ if (part.thinking) {
152
+ const foreground = options.thinkingColor?.(part.thinking);
153
+ if (foreground)
154
+ segment.foreground = foreground;
155
+ }
156
+ else if (part.muted)
145
157
  segment.foreground = mutedColor;
146
158
  if (task.status === "completed" && index > 0)
147
159
  segment.strikethrough = true;
@@ -100,6 +100,7 @@ export type TodoTask = {
100
100
  description?: string;
101
101
  activeForm?: string;
102
102
  priority?: TodoPriority;
103
+ thinking?: ThinkingLevel;
103
104
  parentId?: number;
104
105
  blockedBy?: number[];
105
106
  tags?: string[];
@@ -109,6 +110,7 @@ export type TodoTask = {
109
110
  export type TodoTaskLinePart = {
110
111
  text: string;
111
112
  muted?: boolean;
113
+ thinking?: ThinkingLevel;
112
114
  };
113
115
  export type TodoTaskRow = {
114
116
  task: TodoTask;
@@ -498,9 +500,10 @@ export type ThinkingMenuValue = {
498
500
  level: ThinkingLevel;
499
501
  current: boolean;
500
502
  };
501
- export type UserMessageMenuValue = "copy" | "fork" | "undo";
503
+ export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
502
504
  export type UserMessageJumpMenuValue = {
503
- entryId: string;
505
+ entryId?: string;
506
+ sessionEntryId?: string;
504
507
  };
505
508
  export type QueueMessageMenuValue = "cancel" | "edit" | "send-now";
506
509
  export type ResumeMenuValue = {
@@ -18,6 +18,7 @@ export type AppWorkspaceActionsControllerHost = {
18
18
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
19
19
  render(): void;
20
20
  isRunning(): boolean;
21
+ forkSessionEntryInNewTab(sessionEntryId: string): Promise<boolean>;
21
22
  };
22
23
  export declare class AppWorkspaceActionsController {
23
24
  private readonly host;
@@ -30,6 +31,7 @@ export declare class AppWorkspaceActionsController {
30
31
  syncUserSessionEntryMetadata(): void;
31
32
  copyUserMessage(entryId: string): Promise<void>;
32
33
  forkFromUserMessage(entryId: string): Promise<void>;
34
+ forkFromUserMessageInNewTab(entryId: string): Promise<void>;
33
35
  undoChangesFromUserMessage(entryId: string): Promise<void>;
34
36
  private resolveUserSessionEntryId;
35
37
  private getIdleRuntimeForAction;
@@ -108,6 +108,18 @@ export class AppWorkspaceActionsController {
108
108
  this.host.setSessionStatus(runtime.session);
109
109
  this.host.showToast("Session forked", "success");
110
110
  }
111
+ async forkFromUserMessageInNewTab(entryId) {
112
+ const runtime = this.getIdleRuntimeForAction("fork in new tab");
113
+ if (!runtime)
114
+ return;
115
+ const entry = this.host.findUserEntry(entryId);
116
+ if (!entry)
117
+ throw new Error("User message is no longer available");
118
+ const sessionEntryId = this.resolveUserSessionEntryId(entry);
119
+ if (!sessionEntryId)
120
+ throw new Error("Session entry for this message is not available yet");
121
+ await this.host.forkSessionEntryInNewTab(sessionEntryId);
122
+ }
111
123
  async undoChangesFromUserMessage(entryId) {
112
124
  const runtime = this.getIdleRuntimeForAction("undo changes");
113
125
  if (!runtime)
package/dist/config.d.ts CHANGED
@@ -67,16 +67,20 @@ export type PixConfig = {
67
67
  modelColors: ModelColorsConfig;
68
68
  iconTheme: IconThemeConfig;
69
69
  dictation: DictationConfig;
70
+ ignoreContextFiles: boolean;
70
71
  };
71
72
  export declare function getPixConfigPath(homeDir?: string): string;
73
+ export declare function getProjectPixConfigPath(cwd: string): string;
72
74
  export declare function resolveDefaultModelRef(config: PixConfig): string | undefined;
73
75
  export declare function savePixDefaultModel(modelRef: string): DefaultModelConfig | undefined;
74
76
  export declare function savePixDefaultThinking(thinking: string, fallbackModelRef?: string): DefaultModelConfig | undefined;
75
77
  export declare function savePixAutocompleteModel(modelRef: string): AutocompleteConfig;
78
+ export declare function saveProjectPixIgnoreContextFiles(cwd: string, ignoreContextFiles: boolean): boolean;
79
+ export declare function upsertPixIgnoreContextFilesInJsonc(source: string, ignoreContextFiles: boolean): string;
76
80
  export declare function upsertPixDefaultModelInJsonc(source: string, modelRef: string): string;
77
81
  export declare function upsertPixDefaultThinkingInJsonc(source: string, thinking: string, fallbackModelRef?: string): string;
78
82
  export declare function upsertPixAutocompleteModelInJsonc(source: string, modelRef: string): string;
79
- export declare function loadPixConfig(): PixConfig;
83
+ export declare function loadPixConfig(cwd?: string): PixConfig;
80
84
  export declare function savePixDictationLanguage(language: string): void;
81
85
  export declare function upsertPixDictationLanguageInJsonc(source: string, language: string): string;
82
86
  export declare function resolveModelColor(modelRef: string, config: ModelColorsConfig): string | undefined;