pi-ui-extend 0.1.49 → 0.1.51

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 (42) hide show
  1. package/dist/app/icons.d.ts +1 -0
  2. package/dist/app/icons.js +2 -0
  3. package/dist/app/input/autocomplete-controller.js +1 -1
  4. package/dist/app/input/input-controller.d.ts +1 -0
  5. package/dist/app/input/input-controller.js +7 -24
  6. package/dist/app/input/input-paste-handler.d.ts +1 -0
  7. package/dist/app/input/input-paste-handler.js +13 -16
  8. package/dist/app/rendering/conversation-tool-renderer.js +1 -0
  9. package/dist/app/rendering/popup-menu-renderer.js +2 -2
  10. package/dist/app/rendering/render-controller.js +10 -16
  11. package/dist/app/rendering/status-line-renderer.d.ts +0 -7
  12. package/dist/app/rendering/status-line-renderer.js +7 -79
  13. package/dist/app/rendering/tab-line-renderer.js +1 -1
  14. package/dist/app/rendering/toast-renderer.js +40 -15
  15. package/dist/app/rendering/tool-block-renderer.d.ts +2 -1
  16. package/dist/app/rendering/tool-block-renderer.js +44 -3
  17. package/dist/app/screen/mouse-controller.d.ts +1 -0
  18. package/dist/app/screen/mouse-controller.js +30 -76
  19. package/dist/app/session/lazy-session-manager.js +0 -7
  20. package/dist/app/session/queued-message-controller.d.ts +0 -1
  21. package/dist/app/session/queued-message-controller.js +5 -13
  22. package/dist/app/session/queued-message-entries.d.ts +1 -0
  23. package/dist/app/session/queued-message-entries.js +8 -0
  24. package/dist/app/session/tabs-controller.d.ts +2 -1
  25. package/dist/app/session/tabs-controller.js +67 -24
  26. package/dist/app/types.d.ts +1 -0
  27. package/dist/bundled-extensions/session-title/title-generation.js +1 -1
  28. package/dist/tool-renderers/compress.js +28 -7
  29. package/dist/tool-renderers/patch-normalize.js +4 -4
  30. package/dist/tool-renderers/types.d.ts +10 -0
  31. package/external/pi-tools-suite/package.json +3 -3
  32. package/external/pi-tools-suite/src/async-subagents/core/cleanup.ts +2 -2
  33. package/external/pi-tools-suite/src/async-subagents/core/paths.ts +12 -0
  34. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +3 -4
  35. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +1 -1
  36. package/external/pi-tools-suite/src/async-subagents/core/sessions.ts +2 -2
  37. package/external/pi-tools-suite/src/async-subagents/core/state.ts +3 -3
  38. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +1 -1
  39. package/external/pi-tools-suite/src/async-subagents/lib.ts +1 -1
  40. package/external/pi-tools-suite/src/coding-discipline/index.ts +1 -1
  41. package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
  42. package/package.json +4 -4
@@ -436,8 +436,8 @@ export class AppMouseController {
436
436
  const inputEndRow = inputStartRow + renderedInput.lines.length;
437
437
  if (localY < inputStartRow || localY >= inputEndRow)
438
438
  return undefined;
439
- const { contentWidth } = horizontalPaddingLayout(columns);
440
- return { renderedInput, inputStartRow, localY, contentWidth };
439
+ const { left, contentWidth } = horizontalPaddingLayout(columns);
440
+ return { renderedInput, inputStartRow, localY, left, contentWidth };
441
441
  }
442
442
  tabLineTargetAt(event) {
443
443
  return this.tabLineTargets.find((candidate) => (event.y === candidate.row
@@ -459,30 +459,21 @@ export class AppMouseController {
459
459
  return true;
460
460
  }
461
461
  handleStatusModelClick(event) {
462
- const target = this.statusModelTarget;
463
- if (!target)
464
- return false;
465
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
462
+ if (!this.statusTargetContains(this.statusModelTarget, event))
466
463
  return false;
467
464
  this.popupMenus.openDirectPopupMenu("model");
468
465
  this.host.render();
469
466
  return true;
470
467
  }
471
468
  handleStatusThinkingClick(event) {
472
- const target = this.statusThinkingTarget;
473
- if (!target)
474
- return false;
475
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
469
+ if (!this.statusTargetContains(this.statusThinkingTarget, event))
476
470
  return false;
477
471
  this.popupMenus.openDirectPopupMenu("thinking");
478
472
  this.host.render();
479
473
  return true;
480
474
  }
481
475
  handleStatusContextClick(event) {
482
- const target = this.statusContextTarget;
483
- if (!target)
484
- return false;
485
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
476
+ if (!this.statusTargetContains(this.statusContextTarget, event))
486
477
  return false;
487
478
  const session = this.host.runtimeSession();
488
479
  if (!session)
@@ -492,19 +483,13 @@ export class AppMouseController {
492
483
  return true;
493
484
  }
494
485
  handleStatusModelUsageClick(event) {
495
- const target = this.statusModelUsageTarget;
496
- if (!target)
497
- return false;
498
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
486
+ if (!this.statusTargetContains(this.statusModelUsageTarget, event))
499
487
  return false;
500
488
  void this.host.refreshModelUsageStatus();
501
489
  return true;
502
490
  }
503
491
  handleStatusUserJumpClick(event) {
504
- const target = this.statusUserJumpTarget;
505
- if (!target)
506
- return false;
507
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
492
+ if (!this.statusTargetContains(this.statusUserJumpTarget, event))
508
493
  return false;
509
494
  void this.openStatusUserJumpMenu();
510
495
  return true;
@@ -535,102 +520,75 @@ export class AppMouseController {
535
520
  }
536
521
  }
537
522
  handleStatusDraftQueueClick(event) {
538
- const target = this.statusDraftQueueTarget;
539
- if (!target)
540
- return false;
541
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
523
+ if (!this.statusTargetContains(this.statusDraftQueueTarget, event))
542
524
  return false;
543
525
  void this.host.queueInputFromStatus?.();
544
526
  return true;
545
527
  }
546
528
  handleStatusThinkingExpandClick(event) {
547
- const target = this.statusThinkingExpandTarget;
548
- if (!target)
549
- return false;
550
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
529
+ if (!this.statusTargetContains(this.statusThinkingExpandTarget, event))
551
530
  return false;
552
531
  this.host.toggleAllThinkingExpanded?.();
553
532
  return true;
554
533
  }
555
534
  handleStatusCompactToolsClick(event) {
556
- const target = this.statusCompactToolsTarget;
557
- if (!target)
558
- return false;
559
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
535
+ if (!this.statusTargetContains(this.statusCompactToolsTarget, event))
560
536
  return false;
561
537
  this.host.toggleSuperCompactTools?.();
562
538
  return true;
563
539
  }
564
540
  handleStatusTerminalBellSoundClick(event) {
565
- const target = this.statusTerminalBellSoundTarget;
566
- if (!target)
567
- return false;
568
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
541
+ if (!this.statusTargetContains(this.statusTerminalBellSoundTarget, event))
569
542
  return false;
570
543
  this.host.toggleTerminalBellSound?.();
571
544
  return true;
572
545
  }
573
546
  handleStatusSessionClick(event) {
574
- const target = this.statusSessionTarget;
575
- if (!target)
576
- return false;
577
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
547
+ if (!this.statusTargetContains(this.statusSessionTarget, event))
578
548
  return false;
579
549
  void this.commandController.runResumeCommand({ preserveStatus: true });
580
550
  return true;
581
551
  }
582
552
  handleStatusPromptEnhancerClick(event) {
583
- const target = this.statusPromptEnhancerTarget;
584
- if (!target)
585
- return false;
586
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
553
+ if (!this.statusTargetContains(this.statusPromptEnhancerTarget, event))
587
554
  return false;
588
555
  void this.host.enhancePrompt();
589
556
  return true;
590
557
  }
591
558
  handleStatusVoiceMicClick(event) {
592
- const target = this.statusVoiceMicTarget;
593
- if (!target)
594
- return false;
595
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
559
+ if (!this.statusTargetContains(this.statusVoiceMicTarget, event))
596
560
  return false;
597
561
  this.host.toggleVoiceRecording();
598
562
  return true;
599
563
  }
600
564
  handleStatusVoiceLanguageClick(event) {
601
- const target = this.statusVoiceLanguageTarget;
602
- if (!target)
603
- return false;
604
- if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
565
+ if (!this.statusTargetContains(this.statusVoiceLanguageTarget, event))
605
566
  return false;
606
567
  this.host.toggleVoiceLanguage();
607
568
  return true;
608
569
  }
609
570
  handleStatusQuickScrollClick(event) {
610
- const target = [this.statusQuickScrollUpTarget, this.statusQuickScrollDownTarget].find((candidate) => !!candidate
611
- && event.y === candidate.row
612
- && event.x >= candidate.startColumn
613
- && event.x < candidate.endColumn);
571
+ const target = [this.statusQuickScrollUpTarget, this.statusQuickScrollDownTarget].find((candidate) => this.statusTargetContains(candidate, event));
614
572
  if (!target)
615
573
  return false;
616
574
  this.host.scrollConversationQuick(target.direction);
617
575
  return true;
618
576
  }
577
+ statusTargetContains(target, event) {
578
+ return !!target
579
+ && event.y === target.row
580
+ && event.x >= target.startColumn
581
+ && event.x < target.endColumn;
582
+ }
619
583
  handleInputClick(event) {
620
- const columns = this.host.terminalColumns();
621
- const terminalRows = this.host.terminalRows();
622
- const tabPanelRows = this.host.tabPanelRows(terminalRows);
623
- const rows = editorLayoutRows(terminalRows, tabPanelRows);
624
- const localY = event.y - editorLayoutTopOffset(tabPanelRows);
625
- const { renderedInput, inputStartRow } = this.host.editorLayoutRenderer().computeLayout(columns, rows);
626
- const inputEndRow = inputStartRow + renderedInput.lines.length;
627
- if (localY < inputStartRow || localY >= inputEndRow)
584
+ const geometry = this.inputGeometry(event);
585
+ if (!geometry)
628
586
  return false;
587
+ const { renderedInput, inputStartRow, localY, left, contentWidth } = geometry;
629
588
  if (localY < inputStartRow + renderedInput.editorStartRowOffset)
630
589
  return false;
631
590
  const visibleRowOffset = localY - inputStartRow - renderedInput.editorStartRowOffset;
632
591
  const visualRow = renderedInput.scrollOffset + visibleRowOffset;
633
- const { left, contentWidth } = horizontalPaddingLayout(columns);
634
592
  const cursor = this.host.inputEditor().offsetAtVisualPosition(visualRow, event.x - left, contentWidth, "", "");
635
593
  this.host.resetRequestHistoryNavigation();
636
594
  this.host.inputEditor().setCursor(cursor, { preserveScroll: true });
@@ -638,16 +596,12 @@ export class AppMouseController {
638
596
  return true;
639
597
  }
640
598
  handleExtensionInputClick(event) {
641
- const columns = this.host.terminalColumns();
642
- const terminalRows = this.host.terminalRows();
643
- const tabPanelRows = this.host.tabPanelRows(terminalRows);
644
- const rows = editorLayoutRows(terminalRows, tabPanelRows);
645
- const localY = event.y - editorLayoutTopOffset(tabPanelRows);
646
- const { renderedInput, inputStartRow } = this.host.editorLayoutRenderer().computeLayout(columns, rows);
647
- const inputEndRow = inputStartRow + renderedInput.editorStartRowOffset;
648
- if (localY < inputStartRow || localY >= inputEndRow)
599
+ const geometry = this.inputGeometry(event);
600
+ if (!geometry)
601
+ return false;
602
+ const { renderedInput, inputStartRow, localY, left, contentWidth } = geometry;
603
+ if (localY >= inputStartRow + renderedInput.editorStartRowOffset)
649
604
  return false;
650
- const { left, contentWidth } = horizontalPaddingLayout(columns);
651
605
  return this.host.handleExtensionInputMouse({
652
606
  ...event,
653
607
  localRow: localY - inputStartRow,
@@ -20,7 +20,6 @@ class LazySessionManager {
20
20
  entries = [];
21
21
  byId = new Map();
22
22
  labelsById = new Map();
23
- labelTimestampsById = new Map();
24
23
  leafId = null;
25
24
  hydrated;
26
25
  tailEntryCount;
@@ -69,7 +68,6 @@ class LazySessionManager {
69
68
  this.entries = [];
70
69
  this.byId.clear();
71
70
  this.labelsById.clear();
72
- this.labelTimestampsById.clear();
73
71
  this.leafId = null;
74
72
  mkdirSync(this.sessionDirPath, { recursive: true });
75
73
  this.sessionFilePath = join(this.sessionDirPath, `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`);
@@ -212,11 +210,9 @@ class LazySessionManager {
212
210
  this.appendEntry(entry);
213
211
  if (label) {
214
212
  this.labelsById.set(targetId, label);
215
- this.labelTimestampsById.set(targetId, entry.timestamp);
216
213
  }
217
214
  else {
218
215
  this.labelsById.delete(targetId);
219
- this.labelTimestampsById.delete(targetId);
220
216
  }
221
217
  return entry.id;
222
218
  }
@@ -290,7 +286,6 @@ class LazySessionManager {
290
286
  rebuildIndexes() {
291
287
  this.byId.clear();
292
288
  this.labelsById.clear();
293
- this.labelTimestampsById.clear();
294
289
  this.leafId = null;
295
290
  for (const entry of this.entries) {
296
291
  this.byId.set(entry.id, entry);
@@ -298,11 +293,9 @@ class LazySessionManager {
298
293
  if (entry.type === "label") {
299
294
  if (entry.label) {
300
295
  this.labelsById.set(entry.targetId, entry.label);
301
- this.labelTimestampsById.set(entry.targetId, entry.timestamp);
302
296
  }
303
297
  else {
304
298
  this.labelsById.delete(entry.targetId);
305
- this.labelTimestampsById.delete(entry.targetId);
306
299
  }
307
300
  }
308
301
  }
@@ -75,7 +75,6 @@ export declare class AppQueuedMessageController {
75
75
  private requeueRemovedEntry;
76
76
  private restoreSubmittedMessageToEditor;
77
77
  private restorableSubmittedMessageText;
78
- private cloneSubmittedUserMessage;
79
78
  private notifyDeferredUserMessagesChanged;
80
79
  private notifyAutoUserMessagesChanged;
81
80
  }
@@ -1,6 +1,6 @@
1
1
  import { createId } from "../id.js";
2
2
  import { stringifyUnknown, submittedUserDisplayText } from "../rendering/message-content.js";
3
- import { autoQueuedMessageEntries, deferredQueuedMessageEntries, queuedMessageEntries } from "./queued-message-entries.js";
3
+ import { autoQueuedMessageEntries, cloneSubmittedUserMessage, deferredQueuedMessageEntries, queuedMessageEntries } from "./queued-message-entries.js";
4
4
  export class AppQueuedMessageController {
5
5
  host;
6
6
  autoUserMessages = [];
@@ -16,20 +16,20 @@ export class AppQueuedMessageController {
16
16
  this.promptSubmissionInFlightSession = undefined;
17
17
  }
18
18
  captureAutoUserMessages() {
19
- return this.autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
19
+ return this.autoUserMessages.map(cloneSubmittedUserMessage);
20
20
  }
21
21
  restoreAutoUserMessages(messages) {
22
22
  this.autoUserMessages.length = 0;
23
- this.autoUserMessages.push(...messages.map((message) => this.cloneSubmittedUserMessage(message)));
23
+ this.autoUserMessages.push(...messages.map(cloneSubmittedUserMessage));
24
24
  this.updateQueuedMessageStatus();
25
25
  void this.flushAutoUserMessages();
26
26
  }
27
27
  captureDeferredUserMessages() {
28
- return this.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
28
+ return this.deferredUserMessages.map(cloneSubmittedUserMessage);
29
29
  }
30
30
  restoreDeferredUserMessages(messages) {
31
31
  this.deferredUserMessages.length = 0;
32
- this.deferredUserMessages.push(...messages.map((message) => this.cloneSubmittedUserMessage(message)));
32
+ this.deferredUserMessages.push(...messages.map(cloneSubmittedUserMessage));
33
33
  this.updateQueuedMessageStatus();
34
34
  }
35
35
  createSubmittedUserMessage(promptText, displayText, images) {
@@ -398,14 +398,6 @@ export class AppQueuedMessageController {
398
398
  ? message.promptText.replace(/\[Image \d+(?:: [^\]]+)?\]\s*/g, "").trimEnd()
399
399
  : message.promptText.trimEnd();
400
400
  }
401
- cloneSubmittedUserMessage(message) {
402
- return {
403
- id: message.id,
404
- promptText: message.promptText,
405
- displayText: message.displayText,
406
- images: message.images.map((image) => ({ ...image })),
407
- };
408
- }
409
401
  notifyDeferredUserMessagesChanged() {
410
402
  this.host.onDeferredUserMessagesChanged?.();
411
403
  }
@@ -7,3 +7,4 @@ export declare function sdkQueuedMessageEntries(session: AgentSession | undefine
7
7
  export declare function deferredQueuedMessageEntries(messages: readonly SubmittedUserMessage[]): QueuedEntry[];
8
8
  export declare function autoQueuedMessageEntries(messages: readonly SubmittedUserMessage[]): QueuedEntry[];
9
9
  export declare function queuedMessageEntries(session: AgentSession | undefined, autoUserMessages: readonly SubmittedUserMessage[], deferredUserMessages: readonly SubmittedUserMessage[]): QueuedEntry[];
10
+ export declare function cloneSubmittedUserMessage(message: SubmittedUserMessage): SubmittedUserMessage;
@@ -50,3 +50,11 @@ export function queuedMessageEntries(session, autoUserMessages, deferredUserMess
50
50
  ...deferredQueuedMessageEntries(deferredUserMessages),
51
51
  ];
52
52
  }
53
+ export function cloneSubmittedUserMessage(message) {
54
+ return {
55
+ id: message.id,
56
+ promptText: message.promptText,
57
+ displayText: message.displayText,
58
+ images: message.images.map((image) => ({ ...image })),
59
+ };
60
+ }
@@ -119,7 +119,6 @@ export declare class AppTabsController {
119
119
  private restoreInputState;
120
120
  private inputStateFromText;
121
121
  private restoreDeferredUserMessages;
122
- private cloneSubmittedUserMessage;
123
122
  private runtimeForTab;
124
123
  private findTabForSession;
125
124
  private findTabBySessionPath;
@@ -133,6 +132,7 @@ export declare class AppTabsController {
133
132
  private updateTabFromSession;
134
133
  private sessionPath;
135
134
  private sessionTitle;
135
+ private sessionIsFork;
136
136
  private sessionTitleFromParts;
137
137
  private updatedSessionTitle;
138
138
  private sessionActivity;
@@ -143,6 +143,7 @@ export declare class AppTabsController {
143
143
  private restoredTabs;
144
144
  private defaultSessionTitleFromPath;
145
145
  private loadSessionTitles;
146
+ private loadForkedSessionPaths;
146
147
  private scheduleRestoredTabTitleRefresh;
147
148
  private refreshRestoredTabTitles;
148
149
  private loadTabs;
@@ -6,6 +6,7 @@ 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 { tabPanelRows } from "../rendering/tab-line-renderer.js";
9
+ import { cloneSubmittedUserMessage } from "./queued-message-entries.js";
9
10
  const TAB_STATE_VERSION = 4;
10
11
  const MAX_RESTORED_TABS = 8;
11
12
  const BACKGROUND_PREWARM_TAB_LIMIT = 2;
@@ -180,7 +181,9 @@ export class AppTabsController {
180
181
  await this.saveTabs();
181
182
  return;
182
183
  }
183
- const restoredTabs = this.restoredTabs(saved);
184
+ const restoredSessionPaths = saved.tabs.map((tab) => tab.path);
185
+ const forkedSessionPaths = await this.loadForkedSessionPaths(restoredSessionPaths);
186
+ const restoredTabs = this.restoredTabs(saved, new Map(), forkedSessionPaths);
184
187
  if (restoredTabs.length === 0) {
185
188
  this.clearStartupTabPlaceholders();
186
189
  await this.saveTabs();
@@ -198,7 +201,6 @@ export class AppTabsController {
198
201
  this.replaceTabs(restoredTabs, desiredPath);
199
202
  this.restorePersistedInputStates(saved);
200
203
  this.restorePersistedQueuedUserMessages(saved);
201
- const restoredSessionPaths = saved.tabs.map((tab) => tab.path);
202
204
  if (explicitSessionPath && currentPath)
203
205
  this.ensureCurrentSessionTab(runtime.session);
204
206
  if (!desiredPath) {
@@ -1017,14 +1019,14 @@ export class AppTabsController {
1017
1019
  return;
1018
1020
  const autoMessages = this.host.captureAutoUserMessages?.() ?? [];
1019
1021
  if (autoMessages.length > 0) {
1020
- this.autoUserMessagesByTabId.set(this.activeTabId, autoMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1022
+ this.autoUserMessagesByTabId.set(this.activeTabId, autoMessages.map(cloneSubmittedUserMessage));
1021
1023
  }
1022
1024
  else {
1023
1025
  this.autoUserMessagesByTabId.delete(this.activeTabId);
1024
1026
  }
1025
1027
  const deferredMessages = this.host.captureDeferredUserMessages?.() ?? [];
1026
1028
  if (deferredMessages.length > 0) {
1027
- this.deferredUserMessagesByTabId.set(this.activeTabId, deferredMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1029
+ this.deferredUserMessagesByTabId.set(this.activeTabId, deferredMessages.map(cloneSubmittedUserMessage));
1028
1030
  }
1029
1031
  else {
1030
1032
  this.deferredUserMessagesByTabId.delete(this.activeTabId);
@@ -1040,14 +1042,6 @@ export class AppTabsController {
1040
1042
  this.host.restoreAutoUserMessages?.(this.autoUserMessagesByTabId.get(tabId) ?? []);
1041
1043
  this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
1042
1044
  }
1043
- cloneSubmittedUserMessage(message) {
1044
- return {
1045
- id: message.id,
1046
- promptText: message.promptText,
1047
- displayText: message.displayText,
1048
- images: message.images.map((image) => ({ ...image })),
1049
- };
1050
- }
1051
1045
  async runtimeForTab(tab) {
1052
1046
  const existing = this.runtimesByTabId.get(tab.id);
1053
1047
  if (existing)
@@ -1168,10 +1162,10 @@ export class AppTabsController {
1168
1162
  for (const tab of saved.tabs) {
1169
1163
  const sessionPath = resolve(tab.path);
1170
1164
  if (tab.autoUserMessages && tab.autoUserMessages.length > 0) {
1171
- autoMessagesByPath.set(sessionPath, tab.autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1165
+ autoMessagesByPath.set(sessionPath, tab.autoUserMessages.map(cloneSubmittedUserMessage));
1172
1166
  }
1173
1167
  if (tab.deferredUserMessages && tab.deferredUserMessages.length > 0) {
1174
- deferredMessagesByPath.set(sessionPath, tab.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1168
+ deferredMessagesByPath.set(sessionPath, tab.deferredUserMessages.map(cloneSubmittedUserMessage));
1175
1169
  }
1176
1170
  }
1177
1171
  for (const tab of this.tabItems) {
@@ -1207,6 +1201,7 @@ export class AppTabsController {
1207
1201
  id: createId("tab"),
1208
1202
  title: this.sessionTitle(session),
1209
1203
  ...(options.titlePlaceholder ? { titlePlaceholder: options.titlePlaceholder } : {}),
1204
+ ...(this.sessionIsFork(session) ? { isFork: true } : {}),
1210
1205
  status: "active",
1211
1206
  activity: this.sessionActivity(session),
1212
1207
  ...(sessionPath ? { sessionPath } : {}),
@@ -1216,6 +1211,10 @@ export class AppTabsController {
1216
1211
  const previousSessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
1217
1212
  const sessionPath = this.sessionPath(session);
1218
1213
  tab.title = this.updatedSessionTitle(tab.title, this.sessionTitle(session), previousSessionPath, sessionPath, tab.titlePlaceholder !== undefined);
1214
+ if (this.sessionIsFork(session))
1215
+ tab.isFork = true;
1216
+ else
1217
+ delete tab.isFork;
1219
1218
  tab.activity = this.sessionActivity(session);
1220
1219
  if (sessionPath)
1221
1220
  tab.sessionPath = sessionPath;
@@ -1226,6 +1225,10 @@ export class AppTabsController {
1226
1225
  sessionTitle(session) {
1227
1226
  return this.sessionTitleFromParts(session.sessionId, session.sessionName);
1228
1227
  }
1228
+ sessionIsFork(session) {
1229
+ const sessionManager = session.sessionManager;
1230
+ return typeof sessionManager?.getHeader?.()?.parentSession === "string";
1231
+ }
1229
1232
  sessionTitleFromParts(sessionId, sessionName) {
1230
1233
  const name = sessionName?.trim();
1231
1234
  return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : sessionId.slice(0, 8);
@@ -1269,7 +1272,7 @@ export class AppTabsController {
1269
1272
  initialVisible: true,
1270
1273
  });
1271
1274
  }
1272
- restoredTabs(saved, sessionTitles = new Map()) {
1275
+ restoredTabs(saved, sessionTitles = new Map(), forkedSessionPaths = new Set()) {
1273
1276
  const tabs = [];
1274
1277
  const seen = new Set();
1275
1278
  for (const tab of saved.tabs) {
@@ -1286,6 +1289,7 @@ export class AppTabsController {
1286
1289
  tabs.push({
1287
1290
  id: createId("tab"),
1288
1291
  title: title || "session",
1292
+ ...(forkedSessionPaths.has(sessionPath) ? { isFork: true } : {}),
1289
1293
  status: "waiting",
1290
1294
  sessionPath,
1291
1295
  });
@@ -1308,6 +1312,13 @@ export class AppTabsController {
1308
1312
  }));
1309
1313
  return new Map(entries.filter((entry) => entry !== undefined));
1310
1314
  }
1315
+ async loadForkedSessionPaths(sessionPaths) {
1316
+ const uniquePaths = [...new Set(sessionPaths.map((sessionPath) => resolve(sessionPath)))].slice(0, MAX_RESTORED_TABS);
1317
+ const entries = await Promise.all(uniquePaths.map(async (sessionPath) => {
1318
+ return await readSessionHasParent(sessionPath) ? sessionPath : undefined;
1319
+ }));
1320
+ return new Set(entries.filter((entry) => entry !== undefined));
1321
+ }
1311
1322
  scheduleRestoredTabTitleRefresh(sessionPaths) {
1312
1323
  if (sessionPaths.length === 0)
1313
1324
  return;
@@ -1316,18 +1327,29 @@ export class AppTabsController {
1316
1327
  }, 0).unref?.();
1317
1328
  }
1318
1329
  async refreshRestoredTabTitles(sessionPaths) {
1319
- const titles = await this.loadSessionTitles(sessionPaths);
1320
- if (titles.size === 0)
1330
+ const [titles, forkedSessionPaths] = await Promise.all([
1331
+ this.loadSessionTitles(sessionPaths),
1332
+ this.loadForkedSessionPaths(sessionPaths),
1333
+ ]);
1334
+ if (titles.size === 0 && forkedSessionPaths.size === 0)
1321
1335
  return;
1322
1336
  let changed = false;
1323
1337
  for (const tab of this.tabItems) {
1324
1338
  const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
1325
1339
  const title = sessionPath ? titles.get(sessionPath) : undefined;
1326
- if (!title || tab.title === title)
1327
- continue;
1328
- tab.title = title;
1329
- delete tab.titlePlaceholder;
1330
- changed = true;
1340
+ if (title && tab.title !== title) {
1341
+ tab.title = title;
1342
+ delete tab.titlePlaceholder;
1343
+ changed = true;
1344
+ }
1345
+ const isFork = sessionPath ? forkedSessionPaths.has(sessionPath) : false;
1346
+ if (tab.isFork !== isFork) {
1347
+ if (isFork)
1348
+ tab.isFork = true;
1349
+ else
1350
+ delete tab.isFork;
1351
+ changed = true;
1352
+ }
1331
1353
  }
1332
1354
  if (!changed)
1333
1355
  return;
@@ -1443,11 +1465,11 @@ export class AppTabsController {
1443
1465
  }
1444
1466
  const autoUserMessages = this.autoUserMessagesByTabId.get(tab.id);
1445
1467
  if (autoUserMessages && autoUserMessages.length > 0) {
1446
- persistedTab.autoUserMessages = autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
1468
+ persistedTab.autoUserMessages = autoUserMessages.map(cloneSubmittedUserMessage);
1447
1469
  }
1448
1470
  const deferredUserMessages = this.deferredUserMessagesByTabId.get(tab.id);
1449
1471
  if (deferredUserMessages && deferredUserMessages.length > 0) {
1450
- persistedTab.deferredUserMessages = deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
1472
+ persistedTab.deferredUserMessages = deferredUserMessages.map(cloneSubmittedUserMessage);
1451
1473
  }
1452
1474
  tabs.push(persistedTab);
1453
1475
  }
@@ -1700,6 +1722,27 @@ async function readSessionTitleChunk(file, position, byteCount, options = {}) {
1700
1722
  }
1701
1723
  return undefined;
1702
1724
  }
1725
+ async function readSessionHasParent(sessionPath) {
1726
+ let file;
1727
+ try {
1728
+ file = await openFile(sessionPath, "r");
1729
+ const buffer = Buffer.alloc(4096);
1730
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1731
+ if (bytesRead <= 0)
1732
+ return false;
1733
+ const firstLine = buffer.subarray(0, bytesRead).toString("utf8").split("\n", 1)[0]?.trim();
1734
+ if (!firstLine)
1735
+ return false;
1736
+ const parsed = JSON.parse(firstLine);
1737
+ return isRecord(parsed) && parsed.type === "session" && typeof parsed.parentSession === "string";
1738
+ }
1739
+ catch {
1740
+ return false;
1741
+ }
1742
+ finally {
1743
+ await file?.close();
1744
+ }
1745
+ }
1703
1746
  function validSessionTitle(value) {
1704
1747
  const title = value?.trim();
1705
1748
  return title && !LOADING_TAB_TITLE_PATTERN.test(title) ? title : undefined;
@@ -324,6 +324,7 @@ export type SessionTab = {
324
324
  id: string;
325
325
  title: string;
326
326
  titlePlaceholder?: "loading" | "new";
327
+ isFork?: boolean;
327
328
  status: SessionTabStatus;
328
329
  activity?: SessionActivity;
329
330
  attention?: SessionTabAttention;
@@ -1,4 +1,4 @@
1
- import { complete } from "@earendil-works/pi-ai";
1
+ import { complete } from "@earendil-works/pi-ai/compat";
2
2
  const TITLE_SYSTEM_PROMPT = [
3
3
  "You name Pi coding-agent sessions from the user's first request.",
4
4
  "Return only one concise title, without quotes, markdown, emoji, or explanations.",
@@ -1,4 +1,4 @@
1
- import { formatCompactProgressBar } from "../context-progress-bar.js";
1
+ import { compactProgressBarSegments, formatCompactProgressBar } from "../context-progress-bar.js";
2
2
  import { parseArgsText, stringArg } from "./utils.js";
3
3
  export const renderCompressTool = (input) => {
4
4
  const topic = stringArg(input, ["topic"]);
@@ -6,6 +6,7 @@ export const renderCompressTool = (input) => {
6
6
  const header = joinHeaderParts(topic ? { text: topic } : undefined, summary) ?? { text: "" };
7
7
  return {
8
8
  ...(header.text ? { headerArgs: header.text } : {}),
9
+ ...(header.segments && header.segments.length > 0 ? { headerArgSegments: header.segments } : {}),
9
10
  collapsedBody: "",
10
11
  expandedText: fullCompressResponse(input.output, input.status),
11
12
  };
@@ -19,14 +20,14 @@ function formatCompressSummary(input) {
19
20
  const parsed = parseArgsText(output);
20
21
  if (!isRecord(parsed))
21
22
  return { text: oneLine(output) };
22
- return formatCompressSuccess(parsed);
23
+ return formatCompressSuccess(parsed, input.colors);
23
24
  }
24
25
  function fullCompressResponse(output, status) {
25
26
  if (output.trim())
26
27
  return output.trimEnd();
27
28
  return status === "running" ? "running…" : "(empty)";
28
29
  }
29
- function formatCompressSuccess(result) {
30
+ function formatCompressSuccess(result, colors) {
30
31
  const tokensSaved = numberValue(result.tokensSaved);
31
32
  const contextPercent = numberValue(result.contextPercent);
32
33
  const contextTokens = numberValue(result.contextTokens);
@@ -42,7 +43,7 @@ function formatCompressSuccess(result) {
42
43
  const barPercent = compressedContextPercent ?? contextPercent;
43
44
  const parts = [
44
45
  { text: tokensSaved != null ? `saved ${formatCompactNumber(tokensSaved)}` : "compressed" },
45
- barPercent != null ? progressPart(barPercent, originalContextTokens) : undefined,
46
+ barPercent != null ? progressPart(barPercent, originalContextTokens, colors) : undefined,
46
47
  compressedContextPercent != null && contextPercent != null ? { text: `context ${formatPercent(contextPercent)}` } : undefined,
47
48
  itemCount != null ? { text: `${itemCount} ${plural(itemCount, "item")}` } : undefined,
48
49
  summaryTokens != null ? { text: `${formatCompactNumber(summaryTokens)} summary tokens` } : undefined,
@@ -52,9 +53,17 @@ function formatCompressSuccess(result) {
52
53
  ].filter((part) => Boolean(part));
53
54
  return joinHeaderParts(...parts) ?? { text: "compressed" };
54
55
  }
55
- function progressPart(percent, previousContextTokens) {
56
+ function progressPart(percent, previousContextTokens, colors) {
56
57
  const text = `${formatCompactProgressBar(percent)} ${formatPercent(percent)}${previousContextTokens != null ? ` of ${formatCompactNumber(previousContextTokens)}` : ""}`;
57
- return { text };
58
+ return {
59
+ text,
60
+ ...(colors ? {
61
+ segments: compactProgressBarSegments(0, percent, {
62
+ fill: colors.statusForeground,
63
+ track: colors.statusDotBase,
64
+ }),
65
+ } : {}),
66
+ };
58
67
  }
59
68
  function inferContextTokens(contextPercent, contextWindow) {
60
69
  if (contextPercent == null || contextWindow == null || contextWindow <= 0)
@@ -71,7 +80,19 @@ function joinHeaderParts(...parts) {
71
80
  if (defined.length === 0)
72
81
  return undefined;
73
82
  const separator = " · ";
74
- return { text: defined.map((part) => part.text).join(separator) };
83
+ let offset = 0;
84
+ const segments = [];
85
+ for (const [index, part] of defined.entries()) {
86
+ if (index > 0)
87
+ offset += separator.length;
88
+ if (part.segments)
89
+ segments.push(...part.segments.map((segment) => ({ ...segment, start: segment.start + offset, end: segment.end + offset })));
90
+ offset += part.text.length;
91
+ }
92
+ return {
93
+ text: defined.map((part) => part.text).join(separator),
94
+ ...(segments.length > 0 ? { segments } : {}),
95
+ };
75
96
  }
76
97
  function numberValue(value) {
77
98
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;