pi-ui-extend 0.1.34 → 0.1.36

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 (73) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.d.ts +1 -0
  3. package/dist/app/app.js +12 -2
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.d.ts +1 -0
  6. package/dist/app/commands/command-model-actions.js +32 -0
  7. package/dist/app/commands/command-navigation-actions.js +3 -0
  8. package/dist/app/commands/command-session-actions.js +2 -0
  9. package/dist/app/constants.d.ts +2 -1
  10. package/dist/app/constants.js +6 -1
  11. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  12. package/dist/app/extensions/extension-actions-controller.js +4 -0
  13. package/dist/app/input/input-controller.d.ts +5 -1
  14. package/dist/app/input/input-controller.js +122 -16
  15. package/dist/app/input/input-paste-handler.js +3 -1
  16. package/dist/app/input/terminal-edit-shortcuts.d.ts +21 -0
  17. package/dist/app/input/terminal-edit-shortcuts.js +92 -16
  18. package/dist/app/popup/popup-action-controller.d.ts +1 -0
  19. package/dist/app/popup/popup-action-controller.js +1 -0
  20. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  21. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  22. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  23. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  24. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  25. package/dist/app/rendering/conversation-viewport.js +41 -5
  26. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  27. package/dist/app/rendering/editor-panels.js +27 -10
  28. package/dist/app/runtime.d.ts +1 -0
  29. package/dist/app/runtime.js +33 -14
  30. package/dist/app/session/session-event-controller.d.ts +7 -0
  31. package/dist/app/session/session-event-controller.js +78 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  33. package/dist/app/session/session-lifecycle-controller.js +7 -0
  34. package/dist/app/session/tabs-controller.d.ts +1 -0
  35. package/dist/app/session/tabs-controller.js +4 -1
  36. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  37. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  38. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  39. package/dist/app/terminal/terminal-controller.js +91 -2
  40. package/dist/app/todo/todo-model.js +2 -0
  41. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  42. package/dist/app/todo/todo-widget-controller.js +17 -7
  43. package/dist/app/types.d.ts +4 -0
  44. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  45. package/dist/app/workspace/workspace-actions-controller.js +1 -0
  46. package/dist/bundled-extensions/question/tui.js +8 -1
  47. package/dist/bundled-extensions/session-title/index.js +65 -14
  48. package/dist/input-editor-files.js +23 -4
  49. package/dist/markdown-format.d.ts +4 -1
  50. package/dist/markdown-format.js +76 -9
  51. package/external/pi-tools-suite/README.md +71 -1
  52. package/external/pi-tools-suite/package.json +5 -5
  53. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  55. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  56. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  57. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  58. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  59. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  60. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  61. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  62. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  63. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  64. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  65. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  66. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  67. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  68. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  69. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  70. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  71. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  72. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  73. package/package.json +7 -5
@@ -26,6 +26,8 @@ export class AppSessionEventController {
26
26
  historyEntries = [];
27
27
  historyWindowStart = 0;
28
28
  currentThinkingEntryId;
29
+ currentThinkingEntryStartedAt;
30
+ thinkingElapsedRenderTimer;
29
31
  assistantMessageClosed = false;
30
32
  assistantTextBuffer = "";
31
33
  constructor(host) {
@@ -44,6 +46,7 @@ export class AppSessionEventController {
44
46
  currentAssistantTextBlockContentIndex: this.currentAssistantTextBlockContentIndex,
45
47
  assistantTextBlocksByContentIndex: new Map(this.assistantTextBlocksByContentIndex),
46
48
  currentThinkingEntryId: this.currentThinkingEntryId,
49
+ currentThinkingEntryStartedAt: this.currentThinkingEntryStartedAt,
47
50
  assistantMessageClosed: this.assistantMessageClosed,
48
51
  assistantTextBuffer: this.assistantTextBuffer,
49
52
  entryRenderVersions: new Map(this.entryRenderVersions),
@@ -71,6 +74,8 @@ export class AppSessionEventController {
71
74
  for (const [key, value] of state.assistantTextBlocksByContentIndex)
72
75
  this.assistantTextBlocksByContentIndex.set(key, value);
73
76
  this.currentThinkingEntryId = state.currentThinkingEntryId;
77
+ this.currentThinkingEntryStartedAt = state.currentThinkingEntryStartedAt;
78
+ this.syncThinkingElapsedRenderTimer();
74
79
  this.assistantMessageClosed = state.assistantMessageClosed;
75
80
  this.assistantTextBuffer = state.assistantTextBuffer;
76
81
  this.entryRenderVersions.clear();
@@ -94,6 +99,8 @@ export class AppSessionEventController {
94
99
  this.assistantTextBlocksByContentIndex.clear();
95
100
  this.finalizedToolCallContentIndexes.clear();
96
101
  this.currentThinkingEntryId = undefined;
102
+ this.currentThinkingEntryStartedAt = undefined;
103
+ this.stopThinkingElapsedRenderTimer();
97
104
  this.assistantMessageClosed = false;
98
105
  this.assistantTextBuffer = "";
99
106
  this.olderHistoryLoader = undefined;
@@ -563,6 +570,7 @@ export class AppSessionEventController {
563
570
  this.handleToolCallStreamUpdate(assistantEvent.contentIndex, assistantEvent.partial, assistantEvent.toolCall);
564
571
  break;
565
572
  case "done":
573
+ this.reconcileAssistantTextFromFinalMessage(assistantEvent.message);
566
574
  this.renderAssistantToolCallsFromMessage(assistantEvent.message);
567
575
  this.finishCurrentThinkingEntry();
568
576
  this.flushAssistantTextBuffer(true);
@@ -754,12 +762,15 @@ export class AppSessionEventController {
754
762
  : undefined;
755
763
  if (!entry || entry.kind !== "thinking") {
756
764
  const level = this.currentThinkingLevel();
765
+ const startedAt = this.currentThinkingEntryStartedAt ?? Date.now();
766
+ this.currentThinkingEntryStartedAt = startedAt;
757
767
  entry = {
758
768
  id: createId("thinking"),
759
769
  kind: "thinking",
760
770
  text: "",
761
771
  expanded: this.host.toolDefaultExpanded(THINKING_TOOL_NAME),
762
772
  ...(level === undefined ? {} : { level }),
773
+ startedAt,
763
774
  status: "running",
764
775
  };
765
776
  this.addEntry(entry);
@@ -770,17 +781,24 @@ export class AppSessionEventController {
770
781
  delete entry.level;
771
782
  else
772
783
  entry.level = level;
784
+ entry.startedAt ??= this.currentThinkingEntryStartedAt ?? Date.now();
785
+ this.currentThinkingEntryStartedAt = entry.startedAt;
786
+ delete entry.finishedAt;
773
787
  entry.status = "running";
774
788
  entry.text += delta;
789
+ this.startThinkingElapsedRenderTimer();
775
790
  this.touchEntry(entry);
776
791
  }
777
792
  finishCurrentThinkingEntry() {
778
793
  const entry = this.currentThinkingEntryId ? this.findEntry(this.currentThinkingEntryId) : undefined;
779
794
  if (entry?.kind === "thinking" && entry.status !== "done") {
780
795
  entry.status = "done";
796
+ entry.finishedAt ??= Date.now();
781
797
  this.touchEntry(entry);
782
798
  }
783
799
  this.currentThinkingEntryId = undefined;
800
+ this.currentThinkingEntryStartedAt = undefined;
801
+ this.stopThinkingElapsedRenderTimer();
784
802
  }
785
803
  reconcileThinkingText(content) {
786
804
  let entry = this.currentThinkingEntryId
@@ -788,12 +806,15 @@ export class AppSessionEventController {
788
806
  : undefined;
789
807
  if (!entry || entry.kind !== "thinking") {
790
808
  const level = this.currentThinkingLevel();
809
+ const startedAt = this.currentThinkingEntryStartedAt ?? Date.now();
810
+ this.currentThinkingEntryStartedAt = startedAt;
791
811
  entry = {
792
812
  id: createId("thinking"),
793
813
  kind: "thinking",
794
814
  text: "",
795
815
  expanded: this.host.toolDefaultExpanded(THINKING_TOOL_NAME),
796
816
  ...(level === undefined ? {} : { level }),
817
+ startedAt,
797
818
  status: "running",
798
819
  };
799
820
  this.addEntry(entry);
@@ -806,13 +827,63 @@ export class AppSessionEventController {
806
827
  delete entry.level;
807
828
  else
808
829
  entry.level = level;
830
+ entry.startedAt ??= this.currentThinkingEntryStartedAt ?? Date.now();
831
+ this.currentThinkingEntryStartedAt = entry.startedAt;
832
+ delete entry.finishedAt;
809
833
  entry.status = "running";
834
+ this.startThinkingElapsedRenderTimer();
810
835
  this.touchEntry(entry);
811
836
  }
812
837
  }
838
+ syncThinkingElapsedRenderTimer() {
839
+ const entry = this.currentThinkingEntryId ? this.findEntry(this.currentThinkingEntryId) : undefined;
840
+ if (entry?.kind === "thinking" && entry.status === "running" && entry.startedAt !== undefined) {
841
+ this.startThinkingElapsedRenderTimer();
842
+ return;
843
+ }
844
+ this.stopThinkingElapsedRenderTimer();
845
+ }
846
+ startThinkingElapsedRenderTimer() {
847
+ if (this.thinkingElapsedRenderTimer)
848
+ return;
849
+ this.thinkingElapsedRenderTimer = setInterval(() => {
850
+ if (!this.host.isRunning()) {
851
+ this.stopThinkingElapsedRenderTimer();
852
+ return;
853
+ }
854
+ this.host.scheduleRender();
855
+ }, 1000);
856
+ this.thinkingElapsedRenderTimer.unref?.();
857
+ }
858
+ stopThinkingElapsedRenderTimer() {
859
+ if (!this.thinkingElapsedRenderTimer)
860
+ return;
861
+ clearInterval(this.thinkingElapsedRenderTimer);
862
+ this.thinkingElapsedRenderTimer = undefined;
863
+ }
813
864
  currentThinkingLevel() {
814
865
  return this.host.runtime()?.session.thinkingLevel;
815
866
  }
867
+ reconcileAssistantTextFromFinalMessage(message) {
868
+ const openContentIndex = this.currentAssistantTextBlockContentIndex;
869
+ if (openContentIndex !== undefined) {
870
+ const content = assistantTextContentAt(message, openContentIndex);
871
+ if (content !== undefined)
872
+ this.reconcileAssistantTextBlock(content, openContentIndex);
873
+ return;
874
+ }
875
+ const textBlocks = assistantTextContents(message);
876
+ if (textBlocks.length !== 1)
877
+ return;
878
+ const entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
879
+ if (entry?.kind !== "assistant")
880
+ return;
881
+ const visibleText = assistantStreamVisibleTextForCompleteBlock(textBlocks[0] ?? "", false);
882
+ if (!visibleText || entry.text === visibleText)
883
+ return;
884
+ entry.text = visibleText;
885
+ this.touchEntry(entry);
886
+ }
816
887
  renderAssistantToolCallsFromMessage(message) {
817
888
  if (!isRecord(message) || !Array.isArray(message.content))
818
889
  return;
@@ -893,6 +964,8 @@ export class AppSessionEventController {
893
964
  this.currentAssistantTextBlockStartLength = undefined;
894
965
  this.currentAssistantTextBlockContentIndex = undefined;
895
966
  this.currentThinkingEntryId = undefined;
967
+ this.currentThinkingEntryStartedAt = undefined;
968
+ this.stopThinkingElapsedRenderTimer();
896
969
  this.assistantTextBuffer = "";
897
970
  this.assistantTextBlocksByContentIndex.clear();
898
971
  this.finalizedToolCallContentIndexes.clear();
@@ -916,6 +989,11 @@ function assistantTextContentAt(value, contentIndex) {
916
989
  const block = value.content[contentIndex];
917
990
  return isRecord(block) && block.type === "text" && typeof block.text === "string" ? block.text : undefined;
918
991
  }
992
+ function assistantTextContents(value) {
993
+ if (!isRecord(value) || !Array.isArray(value.content))
994
+ return [];
995
+ return value.content.flatMap((block) => (isRecord(block) && block.type === "text" && typeof block.text === "string" ? [block.text] : []));
996
+ }
919
997
  function assistantStreamVisibleTextForCompleteBlock(text, hasVisibleTextBeforeBlock) {
920
998
  let buffer = text;
921
999
  let visibleText = "";
@@ -61,6 +61,7 @@ export declare class AppSessionLifecycleController {
61
61
  constructor(host: AppSessionLifecycleHost);
62
62
  start(): Promise<void>;
63
63
  bindCurrentSession(options?: BindCurrentSessionOptions): Promise<void>;
64
+ awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime | undefined): Promise<void>;
64
65
  unsubscribeSession(): void;
65
66
  afterSessionReplacement(message?: string): void;
66
67
  private loadReplacementHistory;
@@ -85,6 +85,13 @@ export class AppSessionLifecycleController {
85
85
  }
86
86
  await bindPromise;
87
87
  }
88
+ async awaitCurrentSessionExtensions(runtime = this.host.runtime()) {
89
+ if (!runtime)
90
+ return;
91
+ if (this.extensionBindRuntime !== runtime || this.extensionBindSession !== runtime.session)
92
+ return;
93
+ await this.extensionBindPromise;
94
+ }
88
95
  unsubscribeSession() {
89
96
  this.unsubscribe?.();
90
97
  }
@@ -18,6 +18,7 @@ export type AppTabsControllerHost = {
18
18
  runtime(): AgentSessionRuntime | undefined;
19
19
  createRuntimeForNewSession(): Promise<AgentSessionRuntime>;
20
20
  createRuntimeForSession(sessionPath: string): Promise<AgentSessionRuntime>;
21
+ awaitCurrentSessionExtensions?(runtime?: AgentSessionRuntime): Promise<void>;
21
22
  activateRuntime(runtime: AgentSessionRuntime, options?: BindCurrentSessionOptions): Promise<void>;
22
23
  disposeRuntime(runtime: AgentSessionRuntime): Promise<void>;
23
24
  isRunning(): boolean;
@@ -641,7 +641,9 @@ export class AppTabsController {
641
641
  void this.saveTabs();
642
642
  this.scheduleTabPrewarm();
643
643
  const cachedView = this.sessionViewsByTabId.get(target.id);
644
- if (cachedView && this.host.restoreSessionView) {
644
+ const cachedViewNeedsHistoryReload = this.tabIdsNeedingHistoryReload.has(target.id)
645
+ && this.sessionActivity(targetRuntime.session) !== "running";
646
+ if (cachedView && this.host.restoreSessionView && !cachedViewNeedsHistoryReload) {
645
647
  this.host.restoreSessionView(cachedView);
646
648
  this.restoreDeferredUserMessages(target.id);
647
649
  this.host.setSessionStatus(targetRuntime.session);
@@ -728,6 +730,7 @@ export class AppTabsController {
728
730
  this.activeTabId = tab.id;
729
731
  this.host.setStatus("starting new session");
730
732
  this.host.render();
733
+ await this.host.awaitCurrentSessionExtensions?.(runtime);
731
734
  const result = await runtime.newSession();
732
735
  if (result.cancelled) {
733
736
  this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
@@ -13,7 +13,9 @@ export declare class AppSubagentsWidgetController {
13
13
  private pollTimer;
14
14
  private pollInFlight;
15
15
  private currentRunDir;
16
+ private currentRunDirs;
16
17
  private state;
18
+ private readonly runFreshnessByRunDir;
17
19
  private readonly taskPreviewsByRunDir;
18
20
  private readonly snapshotByRunDir;
19
21
  private refreshGeneration;
@@ -32,10 +34,16 @@ export declare class AppSubagentsWidgetController {
32
34
  private schedulePoll;
33
35
  private poll;
34
36
  private refreshFromFiles;
35
- private findActiveRegistryRunDirForCurrentSession;
37
+ private findActiveRegistryRunDirsForCurrentSession;
36
38
  private registryRunDirsNewestFirst;
37
- private clearMissingRunAndMaybeSelectReplacement;
38
39
  private clearCachedRun;
40
+ private buildStateFromRuns;
41
+ private orderRuns;
42
+ private orderRunDirs;
43
+ private rememberRunFreshness;
44
+ private runFreshness;
45
+ private runFreshnessFromAgents;
46
+ private parseRunTimestamp;
39
47
  private updateState;
40
48
  private isCurrentGeneration;
41
49
  private shouldContinuePolling;
@@ -10,7 +10,9 @@ export class AppSubagentsWidgetController {
10
10
  pollTimer;
11
11
  pollInFlight = false;
12
12
  currentRunDir;
13
+ currentRunDirs = [];
13
14
  state;
15
+ runFreshnessByRunDir = new Map();
14
16
  taskPreviewsByRunDir = new Map();
15
17
  snapshotByRunDir = new Map();
16
18
  refreshGeneration = 0;
@@ -38,6 +40,8 @@ export class AppSubagentsWidgetController {
38
40
  reset() {
39
41
  this.refreshGeneration++;
40
42
  this.currentRunDir = undefined;
43
+ this.currentRunDirs = [];
44
+ this.runFreshnessByRunDir.clear();
41
45
  this.taskPreviewsByRunDir.clear();
42
46
  this.snapshotByRunDir.clear();
43
47
  this.updateState(undefined);
@@ -59,15 +63,14 @@ export class AppSubagentsWidgetController {
59
63
  }
60
64
  this.currentRunDir = runDir;
61
65
  this.snapshotByRunDir.set(runDir, normalizedDetails);
66
+ this.rememberRunFreshness(runDir, this.runFreshnessFromAgents(normalizedDetails.agents) ?? Date.now());
62
67
  if (activeSubagentStates(normalizedDetails.agents).length > 0) {
63
- this.state = {
64
- runDir,
65
- agents: normalizedDetails.agents,
66
- ...(normalizedDetails.tasks === undefined ? {} : { tasks: normalizedDetails.tasks }),
68
+ this.currentRunDirs = this.orderRunDirs([runDir, ...this.currentRunDirs]);
69
+ this.updateState(this.buildStateFromRuns([normalizedDetails], {
67
70
  live: false,
68
71
  snapshotOnly: true,
69
72
  checkedAt: Date.now(),
70
- };
73
+ }));
71
74
  }
72
75
  void this.refreshFromFiles(this.refreshGeneration);
73
76
  this.schedulePoll(0);
@@ -88,6 +91,7 @@ export class AppSubagentsWidgetController {
88
91
  }))
89
92
  .filter((run) => activeSubagentStates(run.agents).length > 0);
90
93
  for (const run of activeRuns) {
94
+ this.rememberRunFreshness(run.runDir, this.runFreshnessFromAgents(run.agents) ?? data.checkedAt);
91
95
  this.snapshotByRunDir.set(run.runDir, {
92
96
  runDir: run.runDir,
93
97
  agents: run.agents,
@@ -97,22 +101,21 @@ export class AppSubagentsWidgetController {
97
101
  if (run.tasks)
98
102
  this.taskPreviewsByRunDir.set(run.runDir, run.tasks);
99
103
  }
100
- const preferred = activeRuns.find((run) => run.runDir === this.currentRunDir) ?? activeRuns[0];
101
- if (!preferred) {
104
+ if (activeRuns.length === 0) {
102
105
  this.currentRunDir = undefined;
106
+ this.currentRunDirs = [];
103
107
  this.updateState(undefined);
104
108
  this.stopPolling();
105
109
  return;
106
110
  }
107
- this.currentRunDir = preferred.runDir;
108
- this.updateState({
109
- runDir: preferred.runDir,
110
- agents: preferred.agents,
111
- ...(preferred.tasks === undefined ? {} : { tasks: preferred.tasks }),
111
+ const orderedRuns = this.orderRuns(activeRuns);
112
+ this.currentRunDir = orderedRuns[0]?.runDir;
113
+ this.currentRunDirs = orderedRuns.map((run) => run.runDir);
114
+ this.updateState(this.buildStateFromRuns(orderedRuns, {
112
115
  live: true,
113
116
  snapshotOnly: false,
114
117
  checkedAt: data.checkedAt,
115
- });
118
+ }));
116
119
  this.startFileWatcher();
117
120
  this.schedulePoll(SUBAGENTS_POLL_INTERVAL_MS);
118
121
  }
@@ -182,71 +185,77 @@ export class AppSubagentsWidgetController {
182
185
  }
183
186
  }
184
187
  async refreshFromFiles(generation = this.refreshGeneration) {
185
- let runDir = this.currentRunDir;
186
- if (!runDir) {
187
- const registry = await readSubagentRegistry(this.host.cwd);
188
- if (!this.isCurrentGeneration(generation))
189
- return;
190
- runDir = await this.findActiveRegistryRunDirForCurrentSession(registry, generation);
191
- if (!this.isCurrentGeneration(generation))
192
- return;
193
- if (runDir)
194
- this.currentRunDir = runDir;
195
- }
196
- if (!runDir) {
197
- if (!this.isCurrentGeneration(generation))
198
- return;
199
- this.updateState(undefined);
188
+ const registry = await readSubagentRegistry(this.host.cwd);
189
+ if (!this.isCurrentGeneration(generation))
200
190
  return;
201
- }
202
- this.currentRunDir = runDir;
203
- const fileState = await readSubagentRunStateFromFiles(runDir, { includeLineCounts: false });
191
+ const runDirs = await this.findActiveRegistryRunDirsForCurrentSession(registry, generation);
204
192
  if (!this.isCurrentGeneration(generation))
205
193
  return;
206
- if (!fileState) {
207
- await this.clearMissingRunAndMaybeSelectReplacement(runDir, generation);
194
+ if (runDirs.length === 0) {
195
+ this.currentRunDir = undefined;
196
+ this.currentRunDirs = [];
197
+ this.updateState(undefined);
208
198
  return;
209
199
  }
210
- const activeAgents = activeSubagentStates(fileState.agents);
211
- if (activeAgents.length === 0) {
212
- if (allSubagentStatesTerminal(fileState.agents) || fileState.agents.length === 0) {
200
+ const runs = [];
201
+ for (const runDir of runDirs) {
202
+ const fileState = await readSubagentRunStateFromFiles(runDir, { includeLineCounts: false });
203
+ if (!this.isCurrentGeneration(generation))
204
+ return;
205
+ if (!fileState) {
213
206
  this.clearCachedRun(runDir);
214
- this.currentRunDir = undefined;
215
- this.updateState(undefined);
207
+ continue;
208
+ }
209
+ const activeAgents = activeSubagentStates(fileState.agents);
210
+ if (activeAgents.length === 0) {
211
+ if (allSubagentStatesTerminal(fileState.agents) || fileState.agents.length === 0)
212
+ this.clearCachedRun(runDir);
213
+ continue;
216
214
  }
215
+ const tasks = this.taskPreviewsByRunDir.get(runDir);
216
+ runs.push({
217
+ runDir,
218
+ agents: fileState.agents,
219
+ ...(tasks === undefined ? {} : { tasks }),
220
+ });
221
+ }
222
+ if (runs.length === 0) {
223
+ this.currentRunDir = undefined;
224
+ this.currentRunDirs = [];
225
+ this.updateState(undefined);
217
226
  return;
218
227
  }
219
- const tasks = this.taskPreviewsByRunDir.get(runDir);
220
- this.updateState({
221
- runDir,
222
- agents: fileState.agents,
223
- ...(tasks === undefined ? {} : { tasks }),
228
+ const orderedRuns = this.orderRuns(runs);
229
+ this.currentRunDir = orderedRuns[0]?.runDir;
230
+ this.currentRunDirs = orderedRuns.map((run) => run.runDir);
231
+ this.updateState(this.buildStateFromRuns(orderedRuns, {
224
232
  live: true,
225
233
  snapshotOnly: false,
226
234
  checkedAt: Date.now(),
227
- });
235
+ }));
228
236
  }
229
- async findActiveRegistryRunDirForCurrentSession(registry, generation) {
237
+ async findActiveRegistryRunDirsForCurrentSession(registry, generation) {
230
238
  if (!registry)
231
- return undefined;
239
+ return [];
232
240
  const sessionFile = this.host.sessionFile();
233
241
  if (!sessionFile)
234
- return undefined;
242
+ return [];
235
243
  const candidateRunDirs = this.registryRunDirsNewestFirst(registry);
244
+ const matchingRunDirs = [];
236
245
  for (const runDir of candidateRunDirs) {
237
246
  if (!this.isCurrentGeneration(generation))
238
- return undefined;
247
+ return [];
239
248
  if (!(await subagentRunHasParentSession(runDir, sessionFile)))
240
249
  continue;
241
250
  if (!this.isCurrentGeneration(generation))
242
- return undefined;
251
+ return [];
243
252
  const state = await readSubagentRunStateFromFiles(runDir, { includeLineCounts: false });
244
253
  if (!this.isCurrentGeneration(generation))
245
- return undefined;
254
+ return [];
246
255
  if (activeSubagentStates(state?.agents ?? []).length > 0)
247
- return runDir;
256
+ matchingRunDirs.push(runDir);
248
257
  }
249
- return undefined;
258
+ return matchingRunDirs;
250
259
  }
251
260
  registryRunDirsNewestFirst(registry) {
252
261
  const runDirs = [];
@@ -263,30 +272,89 @@ export class AppSubagentsWidgetController {
263
272
  addRunDir(registry.latestRunDir);
264
273
  Object.values(registry.runs)
265
274
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
266
- .forEach((run) => addRunDir(run.runDir));
275
+ .forEach((run) => {
276
+ this.rememberRunFreshness(run.runDir, this.parseRunTimestamp(run.updatedAt) ?? this.parseRunTimestamp(run.createdAt));
277
+ addRunDir(run.runDir);
278
+ });
267
279
  return runDirs;
268
280
  }
269
- async clearMissingRunAndMaybeSelectReplacement(runDir, generation) {
270
- this.clearCachedRun(runDir);
271
- if (this.currentRunDir === runDir)
272
- this.currentRunDir = undefined;
273
- const registry = await readSubagentRegistry(this.host.cwd);
274
- if (!this.isCurrentGeneration(generation))
275
- return;
276
- const replacementRunDir = await this.findActiveRegistryRunDirForCurrentSession(registry, generation);
277
- if (!this.isCurrentGeneration(generation))
278
- return;
279
- if (replacementRunDir) {
280
- this.currentRunDir = replacementRunDir;
281
- await this.refreshFromFiles(generation);
282
- return;
283
- }
284
- this.updateState(undefined);
285
- }
286
281
  clearCachedRun(runDir) {
282
+ this.runFreshnessByRunDir.delete(runDir);
287
283
  this.snapshotByRunDir.delete(runDir);
288
284
  this.taskPreviewsByRunDir.delete(runDir);
289
285
  }
286
+ buildStateFromRuns(runs, meta) {
287
+ const activeRuns = runs
288
+ .map((run) => ({ ...run, agents: activeSubagentStates(run.agents) }))
289
+ .filter((run) => run.agents.length > 0);
290
+ if (activeRuns.length === 0)
291
+ return undefined;
292
+ const primary = activeRuns[0];
293
+ if (!primary)
294
+ return undefined;
295
+ return {
296
+ runs: activeRuns.map((run) => ({
297
+ runDir: run.runDir,
298
+ agents: run.agents,
299
+ ...(run.tasks === undefined ? {} : { tasks: run.tasks }),
300
+ })),
301
+ runDir: primary.runDir,
302
+ agents: activeRuns.flatMap((run) => run.agents),
303
+ ...(primary.tasks === undefined ? {} : { tasks: primary.tasks }),
304
+ ...meta,
305
+ };
306
+ }
307
+ orderRuns(runs) {
308
+ const order = this.orderRunDirs(runs.map((run) => run.runDir));
309
+ const priority = new Map(order.map((runDir, index) => [runDir, index]));
310
+ return [...runs].sort((a, b) => {
311
+ const freshness = this.runFreshness(b) - this.runFreshness(a);
312
+ if (freshness !== 0)
313
+ return freshness;
314
+ return (priority.get(a.runDir) ?? Number.MAX_SAFE_INTEGER) - (priority.get(b.runDir) ?? Number.MAX_SAFE_INTEGER);
315
+ });
316
+ }
317
+ orderRunDirs(runDirs) {
318
+ const seen = new Set();
319
+ const preferred = [this.currentRunDir, ...this.currentRunDirs, ...runDirs].filter((runDir) => Boolean(runDir));
320
+ const ordered = [];
321
+ for (const runDir of preferred) {
322
+ if (seen.has(runDir))
323
+ continue;
324
+ seen.add(runDir);
325
+ ordered.push(runDir);
326
+ }
327
+ return ordered;
328
+ }
329
+ rememberRunFreshness(runDir, freshness) {
330
+ if (!Number.isFinite(freshness))
331
+ return;
332
+ const next = freshness ?? 0;
333
+ const previous = this.runFreshnessByRunDir.get(runDir) ?? Number.NEGATIVE_INFINITY;
334
+ if (next >= previous)
335
+ this.runFreshnessByRunDir.set(runDir, next);
336
+ }
337
+ runFreshness(run) {
338
+ return this.runFreshnessFromAgents(run.agents)
339
+ ?? this.runFreshnessByRunDir.get(run.runDir)
340
+ ?? 0;
341
+ }
342
+ runFreshnessFromAgents(agents) {
343
+ let freshest;
344
+ for (const agent of agents) {
345
+ const parsed = this.parseRunTimestamp(agent.startedAt);
346
+ if (parsed === undefined)
347
+ continue;
348
+ freshest = freshest === undefined ? parsed : Math.max(freshest, parsed);
349
+ }
350
+ return freshest;
351
+ }
352
+ parseRunTimestamp(value) {
353
+ if (!value)
354
+ return undefined;
355
+ const parsed = Date.parse(value);
356
+ return Number.isFinite(parsed) ? parsed : undefined;
357
+ }
290
358
  updateState(next) {
291
359
  const previous = stringifyUnknown(this.state);
292
360
  const serializedNext = stringifyUnknown(next);
@@ -298,6 +366,9 @@ export class AppSubagentsWidgetController {
298
366
  return generation === this.refreshGeneration;
299
367
  }
300
368
  shouldContinuePolling() {
369
+ const runs = this.state?.runs;
370
+ if (runs?.length)
371
+ return runs.some((run) => activeSubagentStates(run.agents).length > 0);
301
372
  return activeSubagentStates(this.state?.agents ?? []).length > 0;
302
373
  }
303
374
  eventMatchesCurrentSession(eventSessionFile) {
@@ -24,6 +24,10 @@ export declare class AppTerminalController {
24
24
  private terminalEnabled;
25
25
  private interactiveSuspended;
26
26
  private stopPromise;
27
+ private keyboardProtocolNegotiationBuffer;
28
+ private keyboardProtocolBufferFlushTimer;
29
+ private kittyProtocolActive;
30
+ private modifyOtherKeysActive;
27
31
  private readonly enterInteractiveSequence;
28
32
  private readonly exitInteractiveSequence;
29
33
  constructor(host: AppTerminalControllerHost);
@@ -40,4 +44,10 @@ export declare class AppTerminalController {
40
44
  private scheduleForcedProcessExit;
41
45
  private readonly onResize;
42
46
  private readonly onInputData;
47
+ private beginKeyboardProtocolNegotiation;
48
+ private filterKeyboardProtocolNegotiationInput;
49
+ private handleKeyboardProtocolNegotiationResponse;
50
+ private enableModifyOtherKeysFallback;
51
+ private setKeyboardProtocolNegotiationBuffer;
52
+ private clearKeyboardProtocolNegotiationBuffer;
43
53
  }