pi-ui-extend 0.1.70 → 0.1.72

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 (58) hide show
  1. package/dist/app/app.d.ts +2 -0
  2. package/dist/app/app.js +39 -1
  3. package/dist/app/commands/command-host.d.ts +7 -0
  4. package/dist/app/commands/command-host.js +10 -1
  5. package/dist/app/commands/command-model-actions.d.ts +1 -1
  6. package/dist/app/commands/command-model-actions.js +36 -2
  7. package/dist/app/commands/command-navigation-actions.d.ts +1 -1
  8. package/dist/app/commands/command-navigation-actions.js +40 -6
  9. package/dist/app/commands/command-session-actions.d.ts +1 -1
  10. package/dist/app/commands/command-session-actions.js +56 -0
  11. package/dist/app/commands/shell-controller.d.ts +9 -2
  12. package/dist/app/commands/shell-controller.js +32 -21
  13. package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
  14. package/dist/app/extensions/extension-actions-controller.js +25 -7
  15. package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
  16. package/dist/app/extensions/extension-ui-controller.js +104 -64
  17. package/dist/app/icons.d.ts +1 -0
  18. package/dist/app/icons.js +2 -0
  19. package/dist/app/input/input-action-controller.d.ts +4 -1
  20. package/dist/app/input/input-action-controller.js +68 -27
  21. package/dist/app/input/input-controller.d.ts +3 -0
  22. package/dist/app/input/input-controller.js +45 -1
  23. package/dist/app/input/input-paste-handler.d.ts +3 -0
  24. package/dist/app/input/input-paste-handler.js +21 -12
  25. package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
  26. package/dist/app/input/prompt-enhancer-controller.js +11 -5
  27. package/dist/app/input/voice-controller.d.ts +4 -0
  28. package/dist/app/input/voice-controller.js +76 -35
  29. package/dist/app/popup/popup-action-controller.d.ts +3 -0
  30. package/dist/app/popup/popup-action-controller.js +43 -8
  31. package/dist/app/rendering/render-controller.js +1 -0
  32. package/dist/app/rendering/status-line-renderer.d.ts +3 -1
  33. package/dist/app/rendering/status-line-renderer.js +11 -0
  34. package/dist/app/screen/mouse-controller.d.ts +5 -1
  35. package/dist/app/screen/mouse-controller.js +14 -2
  36. package/dist/app/session/queued-message-controller.d.ts +5 -4
  37. package/dist/app/session/queued-message-controller.js +32 -27
  38. package/dist/app/session/request-history.d.ts +2 -0
  39. package/dist/app/session/request-history.js +22 -15
  40. package/dist/app/session/session-event-controller.js +28 -8
  41. package/dist/app/session/session-history.js +8 -2
  42. package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
  43. package/dist/app/session/session-lifecycle-controller.js +91 -19
  44. package/dist/app/session/tabs-controller.d.ts +26 -0
  45. package/dist/app/session/tabs-controller.js +469 -96
  46. package/dist/app/terminal/terminal-controller.d.ts +4 -0
  47. package/dist/app/terminal/terminal-controller.js +38 -11
  48. package/dist/app/types.d.ts +10 -0
  49. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  50. package/dist/app/workspace/workspace-actions-controller.js +37 -9
  51. package/dist/input-editor.d.ts +2 -0
  52. package/dist/input-editor.js +17 -0
  53. package/docs/concurrency.md +76 -0
  54. package/external/pi-tools-suite/package.json +3 -3
  55. package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
  56. package/external/pi-tools-suite/src/todo/index.ts +2 -2
  57. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  58. package/package.json +4 -4
package/dist/app/app.d.ts CHANGED
@@ -62,6 +62,7 @@ export declare class PiUiExtendApp {
62
62
  private subagentsPanelExpanded;
63
63
  private allThinkingExpanded;
64
64
  private superCompactTools;
65
+ private internalClipboardText;
65
66
  private voicePartialText;
66
67
  private resumeSessions;
67
68
  private resumeLoading;
@@ -86,6 +87,7 @@ export declare class PiUiExtendApp {
86
87
  private restoreTabInputState;
87
88
  private clearPersistedInputDraft;
88
89
  private insertVoiceTranscript;
90
+ private pasteInternalClipboard;
89
91
  private setVoicePartialTranscript;
90
92
  private addVoiceSystemMessage;
91
93
  private resetSessionView;
package/dist/app/app.js CHANGED
@@ -132,6 +132,7 @@ export class PiUiExtendApp {
132
132
  subagentsPanelExpanded = true;
133
133
  allThinkingExpanded = false;
134
134
  superCompactTools = false;
135
+ internalClipboardText;
135
136
  voicePartialText;
136
137
  resumeSessions = [];
137
138
  resumeLoading = false;
@@ -221,6 +222,7 @@ export class PiUiExtendApp {
221
222
  runtime: () => this.runtime,
222
223
  inputEditor: () => this.inputEditor,
223
224
  activeInputTabId: () => this.tabsController.activeInputTabId(),
225
+ isInputTabOwnedByRuntime: (tabId, runtime, session) => this.tabsController.inputTabOwnedByRuntime(tabId, runtime, session),
224
226
  inputStateForTab: (tabId) => this.tabsController.inputStateForTab(tabId),
225
227
  setInputStateForTab: (tabId, state) => this.tabsController.setInputStateForTab(tabId, state),
226
228
  promptEnhancerConfig: () => this.pixConfig.promptEnhancer,
@@ -239,6 +241,7 @@ export class PiUiExtendApp {
239
241
  render: () => this.render(),
240
242
  });
241
243
  this.voiceController = new AppVoiceController({
244
+ activeInputScope: () => this.tabsController.activeInputTabId(),
242
245
  insertTranscript: (text) => this.insertVoiceTranscript(text),
243
246
  setPartialTranscript: (text) => this.setVoicePartialTranscript(text),
244
247
  addSystemMessage: (message) => this.addVoiceSystemMessage(message),
@@ -308,6 +311,7 @@ export class PiUiExtendApp {
308
311
  voiceStatusWidgetActive: () => this.voiceController.statusWidgetActive(),
309
312
  conversationQuickScrollDirections: () => this.conversationQuickScrollDirections(),
310
313
  queueableInputActive: () => this.inputEditor.promptText.trimEnd().length > 0 || this.inputEditor.images.length > 0,
314
+ internalClipboardActive: () => Boolean(this.internalClipboardText),
311
315
  userMessageJumpMenuActive: () => this.popupMenus.directMenu === "user-message-jump",
312
316
  allThinkingExpandedActive: () => this.allThinkingExpanded,
313
317
  superCompactToolsActive: () => this.superCompactTools,
@@ -320,6 +324,7 @@ export class PiUiExtendApp {
320
324
  this.extensionUiController = new ExtensionUiController({
321
325
  theme: this.theme,
322
326
  activeExtensionUiScope: () => this.activeExtensionUiScope(),
327
+ isExtensionUiScopeActive: (scopeKey) => !this.tabsController.isSwitching() && this.activeExtensionUiScope() === scopeKey,
323
328
  isRunning: () => this.running,
324
329
  render: () => this.render(),
325
330
  showToast: (message, kind, options) => this.showToast(message, kind, options),
@@ -335,6 +340,7 @@ export class PiUiExtendApp {
335
340
  });
336
341
  this.extensionActions = new AppExtensionActionsController({
337
342
  isRunning: () => this.running,
343
+ runtime: () => this.runtime,
338
344
  getInput: () => this.input,
339
345
  setInput: (value) => this.setInput(value),
340
346
  awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
@@ -430,6 +436,7 @@ export class PiUiExtendApp {
430
436
  setInput: (value) => this.inputEditor.setText(value),
431
437
  insertInput: (value) => this.inputEditor.insert(value),
432
438
  attachImage: (data, mimeType) => this.inputEditor.attachImage(data, mimeType),
439
+ requeueAutoUserMessageForSession: (session, message) => this.tabsController.requeueAutoUserMessageForSession(session, message),
433
440
  onDeferredUserMessagesChanged: () => this.tabsController.persistActiveDeferredUserMessages(),
434
441
  });
435
442
  this.editorLayoutRenderer = new EditorLayoutRenderer({
@@ -608,6 +615,10 @@ export class PiUiExtendApp {
608
615
  this.render();
609
616
  });
610
617
  },
618
+ pasteInternalClipboard: () => this.pasteInternalClipboard(),
619
+ setInternalClipboardText: (text) => {
620
+ this.internalClipboardText = text;
621
+ },
611
622
  scrollConversationQuick: (direction) => this.scrollConversationQuick(direction),
612
623
  toggleAllThinkingExpanded: () => {
613
624
  this.allThinkingExpanded = !this.allThinkingExpanded;
@@ -648,6 +659,7 @@ export class PiUiExtendApp {
648
659
  this.shellController = new AppShellController({
649
660
  cwd: this.options.cwd,
650
661
  isRunning: () => this.running,
662
+ activeScopeKey: () => this.tabsController.activeInputTabId(),
651
663
  addEntry: (entry) => this.addEntry(entry),
652
664
  touchEntry: (entry) => this.touchEntry(entry),
653
665
  setStatus: (status) => this.setStatus(status),
@@ -658,6 +670,7 @@ export class PiUiExtendApp {
658
670
  });
659
671
  this.inputActions = new AppInputActionController({
660
672
  runtime: () => this.runtime,
673
+ inputScopeKey: () => this.tabsController.activeInputTabId(),
661
674
  isRunning: () => this.running,
662
675
  isSessionSwitching: () => this.tabsController.isSwitching(),
663
676
  inputEditor: () => this.inputEditor,
@@ -688,6 +701,7 @@ export class PiUiExtendApp {
688
701
  this.inputController = new AppInputController({
689
702
  inputEditor: this.inputEditor,
690
703
  cwd: this.options.cwd,
704
+ inputScopeKey: () => this.tabsController.activeInputTabId(),
691
705
  handleExtensionTerminalInput: (data) => this.extensionUiController.handleTerminalInput(data),
692
706
  extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
693
707
  getInput: () => this.input,
@@ -718,6 +732,7 @@ export class PiUiExtendApp {
718
732
  setRunning: (running) => {
719
733
  this.running = running;
720
734
  },
735
+ cancelPendingTabLifecycle: () => this.tabsController.cancelPendingLifecycleWork(),
721
736
  runtime: () => this.runtime,
722
737
  saveInputStateForQuit: () => this.tabsController.saveInputStateForQuit(),
723
738
  disposeInactiveRuntimesForQuit: () => this.tabsController.disposeInactiveRuntimes((runtime) => this.terminalController.disposeRuntimeForQuit(runtime)),
@@ -783,6 +798,7 @@ export class PiUiExtendApp {
783
798
  this.mouseController.statusContextTarget = undefined;
784
799
  this.mouseController.statusModelUsageTarget = undefined;
785
800
  this.mouseController.statusDraftQueueTarget = undefined;
801
+ this.mouseController.statusInternalClipboardTarget = undefined;
786
802
  this.mouseController.statusUserJumpTarget = undefined;
787
803
  this.mouseController.statusThinkingExpandTarget = undefined;
788
804
  this.mouseController.statusCompactToolsTarget = undefined;
@@ -874,11 +890,17 @@ export class PiUiExtendApp {
874
890
  await this.sessionLifecycle.awaitCurrentSessionExtensions(runtime);
875
891
  }
876
892
  async activateRuntime(runtime, options) {
893
+ this.extensionUiController.cancelCustomUi(this.activeExtensionUiScope());
894
+ void this.voiceController.stopRecording();
877
895
  this.runtime = runtime;
878
896
  runtime.setRebindSession(async () => {
897
+ if (!this.running || this.runtime !== runtime)
898
+ return;
879
899
  await this.bindCurrentSession({ awaitExtensions: false });
880
900
  });
881
901
  runtime.setBeforeSessionInvalidate(() => {
902
+ if (!this.running || this.runtime !== runtime)
903
+ return;
882
904
  this.extensionUiController.clearWidgets(this.activeExtensionUiScope());
883
905
  });
884
906
  await this.bindCurrentSession(options);
@@ -929,7 +951,7 @@ export class PiUiExtendApp {
929
951
  restoreTabInputState(state) {
930
952
  this.requestHistory.resetNavigation();
931
953
  this.popupMenus.resetInputMenuDismissals();
932
- this.inputEditor.setDraftState(state);
954
+ this.inputEditor.restoreDraftState(state);
933
955
  this.autocompleteController.dispose();
934
956
  }
935
957
  async clearPersistedInputDraft() {
@@ -951,6 +973,16 @@ export class PiUiExtendApp {
951
973
  this.inputEditor.insert(`${prefix}${transcript}${suffix}`);
952
974
  this.render();
953
975
  }
976
+ pasteInternalClipboard() {
977
+ if (!this.internalClipboardText) {
978
+ this.showToast("Nothing has been copied inside Pix yet.", "info");
979
+ return;
980
+ }
981
+ this.requestHistory.resetNavigation();
982
+ this.popupMenus.resetInputMenuDismissals();
983
+ this.inputEditor.attachPastedText(this.internalClipboardText);
984
+ this.render();
985
+ }
954
986
  setVoicePartialTranscript(text) {
955
987
  if (this.voicePartialText === text)
956
988
  return;
@@ -1130,6 +1162,8 @@ export class PiUiExtendApp {
1130
1162
  });
1131
1163
  }
1132
1164
  showToast(message, kind = "info", options) {
1165
+ if (!this.running)
1166
+ return;
1133
1167
  this.toastController.showToast(message, kind, options);
1134
1168
  }
1135
1169
  toastNotifierForScope(scopeKey) {
@@ -1154,6 +1188,8 @@ export class PiUiExtendApp {
1154
1188
  clearTimeout(this.scheduledRenderTimer);
1155
1189
  this.scheduledRenderTimer = undefined;
1156
1190
  }
1191
+ if (!this.running)
1192
+ return;
1157
1193
  this.autocompleteController.observeInput();
1158
1194
  this.syncScrollAfterInputEditorChange();
1159
1195
  this.renderController.render();
@@ -1163,6 +1199,8 @@ export class PiUiExtendApp {
1163
1199
  return;
1164
1200
  this.scheduledRenderTimer = setTimeout(() => {
1165
1201
  this.scheduledRenderTimer = undefined;
1202
+ if (!this.running)
1203
+ return;
1166
1204
  this.syncScrollAfterInputEditorChange();
1167
1205
  this.renderController.render();
1168
1206
  }, COALESCED_RENDER_DELAY_MS);
@@ -52,3 +52,10 @@ export type CommandControllerHost = {
52
52
  openNewTab(): Promise<void>;
53
53
  openSearchResultInNewTab(result: SessionSearchResult): Promise<void>;
54
54
  };
55
+ export type CommandScope = {
56
+ readonly runtime: AgentSessionRuntime | undefined;
57
+ readonly session: AgentSession | undefined;
58
+ };
59
+ export declare function captureCommandScope(host: CommandControllerHost): CommandScope;
60
+ export declare function isCommandRuntimeActive(host: CommandControllerHost, runtime: AgentSessionRuntime): boolean;
61
+ export declare function isCommandScopeActive(host: CommandControllerHost, scope: CommandScope): boolean;
@@ -1 +1,10 @@
1
- export {};
1
+ export function captureCommandScope(host) {
2
+ const runtime = host.runtime();
3
+ return { runtime, session: runtime?.session };
4
+ }
5
+ export function isCommandRuntimeActive(host, runtime) {
6
+ return host.isRunning() && host.runtime() === runtime;
7
+ }
8
+ export function isCommandScopeActive(host, scope) {
9
+ return host.isRunning() && host.runtime() === scope.runtime && scope.runtime?.session === scope.session;
10
+ }
@@ -1,4 +1,4 @@
1
- import type { CommandControllerHost } from "./command-host.js";
1
+ import { type CommandControllerHost } from "./command-host.js";
2
2
  import type { SessionModel, ThinkingLevel } from "../types.js";
3
3
  export declare class ModelCommandActions {
4
4
  private readonly host;
@@ -1,4 +1,5 @@
1
1
  import { getIdleRuntime, getRuntime } from "./command-runtime.js";
2
+ import { captureCommandScope, isCommandScopeActive, } from "./command-host.js";
2
3
  import { getProjectPixConfigPath, savePixAutocompleteModel, savePixDefaultModel, savePixDefaultThinking, saveProjectPixIgnoreContextFiles } from "../../config.js";
3
4
  import { createId } from "../id.js";
4
5
  import { isThinkingLevel, parseScopedModelRef } from "../model/model-ref.js";
@@ -41,6 +42,7 @@ export class ModelCommandActions {
41
42
  this.host.setSessionStatus(runtime.session);
42
43
  }
43
44
  async runModelSlashCommand(argumentsText) {
45
+ const scope = captureCommandScope(this.host);
44
46
  const modelRef = argumentsText.trim();
45
47
  if (!modelRef) {
46
48
  const selected = await this.host.showMenu(this.host.getModelMenuItems(""), {
@@ -48,6 +50,8 @@ export class ModelCommandActions {
48
50
  placeholder: "Search models",
49
51
  emptyText: "No matching models",
50
52
  });
53
+ if (!isCommandScopeActive(this.host, scope))
54
+ return;
51
55
  if (!selected) {
52
56
  this.host.setSessionStatus(this.host.runtime()?.session);
53
57
  this.host.render();
@@ -64,10 +68,14 @@ export class ModelCommandActions {
64
68
  if (!parsed)
65
69
  throw new Error("Model must use provider/model[:thinking] format");
66
70
  await runtime.services.modelRuntime.reloadConfig();
71
+ if (!isCommandScopeActive(this.host, scope))
72
+ return;
67
73
  const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
68
74
  if (!model)
69
75
  throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
70
76
  await this.runModelCommand(model);
77
+ if (!isCommandScopeActive(this.host, scope))
78
+ return;
71
79
  if (parsed.thinkingLevel !== undefined) {
72
80
  runtime.session.setThinkingLevel(parsed.thinkingLevel);
73
81
  this.addPersistentSystemEntry(runtime.session, `Selected thinking level ${runtime.session.thinkingLevel}`);
@@ -75,6 +83,7 @@ export class ModelCommandActions {
75
83
  }
76
84
  }
77
85
  async runDefaultModelSlashCommand(argumentsText) {
86
+ const scope = captureCommandScope(this.host);
78
87
  const modelRef = argumentsText.trim();
79
88
  if (!modelRef) {
80
89
  const selected = await this.host.showMenu(this.host.getModelMenuItems(""), {
@@ -82,6 +91,8 @@ export class ModelCommandActions {
82
91
  placeholder: "Search models",
83
92
  emptyText: "No matching models",
84
93
  });
94
+ if (!isCommandScopeActive(this.host, scope))
95
+ return;
85
96
  if (!selected) {
86
97
  this.host.setSessionStatus(this.host.runtime()?.session);
87
98
  this.host.render();
@@ -98,6 +109,8 @@ export class ModelCommandActions {
98
109
  if (!parsed)
99
110
  throw new Error("Model must use provider/model[:thinking] format");
100
111
  await runtime.services.modelRuntime.reloadConfig();
112
+ if (!isCommandScopeActive(this.host, scope))
113
+ return;
101
114
  const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
102
115
  if (!model)
103
116
  throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
@@ -105,6 +118,7 @@ export class ModelCommandActions {
105
118
  this.host.setSessionStatus(runtime.session);
106
119
  }
107
120
  async runAutocompleteSlashCommand(argumentsText) {
121
+ const scope = captureCommandScope(this.host);
108
122
  const modelRef = argumentsText.trim();
109
123
  if (!modelRef) {
110
124
  const saved = savePixAutocompleteModel("");
@@ -119,6 +133,8 @@ export class ModelCommandActions {
119
133
  if (!parsed)
120
134
  throw new Error("Model must use provider/model[:thinking] format, or run /autocomplete with no arguments to disable");
121
135
  await runtime.services.modelRuntime.reloadConfig();
136
+ if (!isCommandScopeActive(this.host, scope))
137
+ return;
122
138
  const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
123
139
  if (!model)
124
140
  throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
@@ -161,6 +177,7 @@ export class ModelCommandActions {
161
177
  const runtime = getIdleRuntime(this.host, "scoped-models");
162
178
  if (!runtime)
163
179
  return;
180
+ const scope = captureCommandScope(this.host);
164
181
  const value = argumentsText.trim();
165
182
  if (!value) {
166
183
  const enabledModels = runtime.services.settingsManager.getEnabledModels();
@@ -200,6 +217,8 @@ export class ModelCommandActions {
200
217
  const scopedModels = [];
201
218
  const invalidRefs = [];
202
219
  await runtime.services.modelRuntime.reloadConfig();
220
+ if (!isCommandScopeActive(this.host, scope))
221
+ return;
203
222
  for (const ref of refs) {
204
223
  const parsed = parseScopedModelRef(ref);
205
224
  const model = parsed ? runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId) : undefined;
@@ -219,6 +238,7 @@ export class ModelCommandActions {
219
238
  this.host.setSessionStatus(runtime.session);
220
239
  }
221
240
  async runThinkingSlashCommand(argumentsText) {
241
+ const scope = captureCommandScope(this.host);
222
242
  const level = argumentsText.trim();
223
243
  if (!level) {
224
244
  const selected = await this.host.showMenu(this.host.getThinkingMenuItems(""), {
@@ -226,6 +246,8 @@ export class ModelCommandActions {
226
246
  placeholder: "Search thinking levels",
227
247
  emptyText: "No matching thinking levels",
228
248
  });
249
+ if (!isCommandScopeActive(this.host, scope))
250
+ return;
229
251
  if (!selected) {
230
252
  this.host.setSessionStatus(this.host.runtime()?.session);
231
253
  this.host.render();
@@ -240,6 +262,7 @@ export class ModelCommandActions {
240
262
  await this.runThinkingCommand(level);
241
263
  }
242
264
  async runDefaultThinkingSlashCommand(argumentsText) {
265
+ const scope = captureCommandScope(this.host);
243
266
  const level = argumentsText.trim();
244
267
  if (!level) {
245
268
  const selected = await this.host.showMenu(this.host.getThinkingMenuItems(""), {
@@ -247,6 +270,8 @@ export class ModelCommandActions {
247
270
  placeholder: "Search thinking levels",
248
271
  emptyText: "No matching thinking levels",
249
272
  });
273
+ if (!isCommandScopeActive(this.host, scope))
274
+ return;
250
275
  if (!selected) {
251
276
  this.host.setSessionStatus(this.host.runtime()?.session);
252
277
  this.host.render();
@@ -264,10 +289,13 @@ export class ModelCommandActions {
264
289
  const runtime = getRuntime(this.host, "model");
265
290
  if (!runtime)
266
291
  return;
292
+ const scope = captureCommandScope(this.host);
267
293
  const ref = this.host.modelRef(model);
268
294
  this.host.setStatus(`selecting model ${ref}`);
269
295
  this.host.render();
270
296
  await runtime.session.setModel(model);
297
+ if (!isCommandScopeActive(this.host, scope))
298
+ return;
271
299
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected model ${ref}` });
272
300
  if (runtime.session.isStreaming) {
273
301
  this.host.addEntry({
@@ -279,7 +307,9 @@ export class ModelCommandActions {
279
307
  this.host.setSessionStatus(runtime.session);
280
308
  return;
281
309
  }
282
- await this.reloadAfterModelChange(runtime.session, ref);
310
+ await this.reloadAfterModelChange(runtime.session, ref, scope);
311
+ if (!isCommandScopeActive(this.host, scope))
312
+ return;
283
313
  this.host.setSessionStatus(runtime.session);
284
314
  }
285
315
  async runThinkingCommand(level) {
@@ -296,11 +326,13 @@ export class ModelCommandActions {
296
326
  appendPixSystemDisplayEntry(session, text);
297
327
  this.host.addEntry({ id: createId("system"), kind: "system", text });
298
328
  }
299
- async reloadAfterModelChange(session, ref) {
329
+ async reloadAfterModelChange(session, ref, scope) {
300
330
  this.host.setStatus(`reloading resources for ${ref}`);
301
331
  this.host.render();
302
332
  try {
303
333
  await session.reload();
334
+ if (!isCommandScopeActive(this.host, scope))
335
+ return;
304
336
  this.host.addEntry({
305
337
  id: createId("system"),
306
338
  kind: "system",
@@ -309,6 +341,8 @@ export class ModelCommandActions {
309
341
  this.host.toast.success("Model changed and resources reloaded");
310
342
  }
311
343
  catch (error) {
344
+ if (!isCommandScopeActive(this.host, scope))
345
+ return;
312
346
  this.host.addEntry({
313
347
  id: createId("error"),
314
348
  kind: "error",
@@ -1,5 +1,5 @@
1
1
  import type { SessionInfo } from "@earendil-works/pi-coding-agent";
2
- import type { CommandControllerHost } from "./command-host.js";
2
+ import { type CommandControllerHost } from "./command-host.js";
3
3
  import { type ResumeSessionLoaderOptions } from "../session/resume-session-loader.js";
4
4
  import type { PopupMenuPlacement } from "../types.js";
5
5
  export declare function formatHistoryMenuLabel(text: string): string;
@@ -1,4 +1,5 @@
1
1
  import { resolve } from "node:path";
2
+ import { captureCommandScope, isCommandRuntimeActive, isCommandScopeActive, } from "./command-host.js";
2
3
  import { getIdleRuntime, getRuntime } from "./command-runtime.js";
3
4
  import { createId } from "../id.js";
4
5
  import { isRecord } from "../guards.js";
@@ -51,7 +52,11 @@ export class NavigationCommandActions {
51
52
  this.host.setStatus("forking session");
52
53
  this.host.render();
53
54
  await this.host.awaitCurrentSessionExtensions(runtime);
55
+ if (!isCommandRuntimeActive(this.host, runtime))
56
+ return;
54
57
  const result = await runtime.fork(entryId);
58
+ if (!isCommandRuntimeActive(this.host, runtime))
59
+ return;
55
60
  if (result.cancelled) {
56
61
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
57
62
  this.host.setSessionStatus(runtime.session);
@@ -76,7 +81,11 @@ export class NavigationCommandActions {
76
81
  this.host.setStatus("cloning session");
77
82
  this.host.render();
78
83
  await this.host.awaitCurrentSessionExtensions(runtime);
84
+ if (!isCommandRuntimeActive(this.host, runtime))
85
+ return;
79
86
  const result = await runtime.fork(leafId, { position: "at" });
87
+ if (!isCommandRuntimeActive(this.host, runtime))
88
+ return;
80
89
  if (result.cancelled) {
81
90
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Clone cancelled." });
82
91
  this.host.setSessionStatus(runtime.session);
@@ -96,7 +105,10 @@ export class NavigationCommandActions {
96
105
  }
97
106
  this.host.setStatus("navigating tree");
98
107
  this.host.render();
108
+ const scope = captureCommandScope(this.host);
99
109
  const result = await runtime.session.navigateTree(targetId);
110
+ if (!isCommandScopeActive(this.host, scope))
111
+ return;
100
112
  if (result.aborted) {
101
113
  this.host.toast.info("Tree navigation cancelled");
102
114
  this.host.setSessionStatus(runtime.session);
@@ -118,6 +130,7 @@ export class NavigationCommandActions {
118
130
  const runtime = getRuntime(this.host, "jump");
119
131
  if (!runtime)
120
132
  return;
133
+ const scope = captureCommandScope(this.host);
121
134
  this.host.openDirectPopupMenu("user-message-jump", { preserveStatus: true });
122
135
  this.host.setDirectPopupMenuQuery(argumentsText.trim());
123
136
  this.host.render();
@@ -125,13 +138,17 @@ export class NavigationCommandActions {
125
138
  await this.host.refreshUserMessageJumpMenuItems();
126
139
  }
127
140
  catch (error) {
141
+ if (!isCommandScopeActive(this.host, scope))
142
+ return;
128
143
  this.host.toast.error(`Could not load jump messages: ${error instanceof Error ? error.message : String(error)}`);
129
144
  }
130
145
  finally {
131
- this.host.render();
146
+ if (isCommandScopeActive(this.host, scope))
147
+ this.host.render();
132
148
  }
133
149
  }
134
150
  async runHistoryCommand(argumentsText) {
151
+ const scope = captureCommandScope(this.host);
135
152
  const query = argumentsText.trim();
136
153
  const matches = this.host.requestHistory().searchMatches(query, 100);
137
154
  if (matches.length === 0) {
@@ -157,6 +174,8 @@ export class NavigationCommandActions {
157
174
  minScorePerCharacter: 8,
158
175
  preferKeyboardLayoutMatches: true,
159
176
  });
177
+ if (!isCommandScopeActive(this.host, scope))
178
+ return;
160
179
  if (!selected) {
161
180
  this.host.setSessionStatus(this.host.runtime()?.session);
162
181
  return;
@@ -170,6 +189,7 @@ export class NavigationCommandActions {
170
189
  const runtime = getIdleRuntime(this.host, "search");
171
190
  if (!runtime)
172
191
  return;
192
+ const scope = captureCommandScope(this.host);
173
193
  const query = argumentsText.trim();
174
194
  if (!query) {
175
195
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Usage: /search <text>" });
@@ -184,6 +204,8 @@ export class NavigationCommandActions {
184
204
  const results = await searchSessions(query, {
185
205
  cwd: this.host.options.cwd,
186
206
  onProgress: (loaded, total) => {
207
+ if (!isCommandScopeActive(this.host, scope))
208
+ return;
187
209
  const progressText = total > 0 ? `searching sessions… ${loaded}/${total}` : "searching sessions…";
188
210
  if (progressText === lastProgressText)
189
211
  return;
@@ -192,6 +214,8 @@ export class NavigationCommandActions {
192
214
  this.host.render();
193
215
  },
194
216
  });
217
+ if (!isCommandScopeActive(this.host, scope))
218
+ return;
195
219
  if (results.length === 0) {
196
220
  this.host.addEntry({ id: createId("system"), kind: "system", text: `No sessions found for: ${query}` });
197
221
  this.host.toast.info("No matching sessions");
@@ -205,6 +229,8 @@ export class NavigationCommandActions {
205
229
  emptyText: "No matching search results",
206
230
  searchable: true,
207
231
  });
232
+ if (!isCommandScopeActive(this.host, scope))
233
+ return;
208
234
  if (!selected) {
209
235
  this.host.setSessionStatus(runtime.session);
210
236
  return;
@@ -212,6 +238,8 @@ export class NavigationCommandActions {
212
238
  await this.host.openSearchResultInNewTab(selected);
213
239
  }
214
240
  catch (error) {
241
+ if (!isCommandScopeActive(this.host, scope))
242
+ return;
215
243
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Session search failed: ${error instanceof Error ? error.message : String(error)}` });
216
244
  this.host.toast.error("Session search failed");
217
245
  this.host.setSessionStatus(runtime.session);
@@ -231,7 +259,11 @@ export class NavigationCommandActions {
231
259
  this.host.setStatus("switching session");
232
260
  this.host.render();
233
261
  await this.host.awaitCurrentSessionExtensions(runtime);
262
+ if (!isCommandRuntimeActive(this.host, runtime))
263
+ return;
234
264
  const result = await runtime.switchSession(resolvedSessionPath);
265
+ if (!isCommandRuntimeActive(this.host, runtime))
266
+ return;
235
267
  if (result.cancelled) {
236
268
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
237
269
  this.host.setSessionStatus(runtime.session);
@@ -275,28 +307,30 @@ export class NavigationCommandActions {
275
307
  }
276
308
  this.host.render();
277
309
  const loadId = ++this.resumeLoadId;
278
- void this.loadResumeSessionsInBackground({ loadId, preserveStatus, session: runtime.session });
310
+ void this.loadResumeSessionsInBackground({ loadId, preserveStatus, scope: captureCommandScope(this.host) });
279
311
  }
280
312
  async loadResumeSessionsInBackground(options) {
281
313
  try {
282
314
  await nextTick();
315
+ if (!isCommandScopeActive(this.host, options.scope))
316
+ return;
283
317
  await this.resumeSessionLoader({
284
318
  cwd: this.host.options.cwd,
285
319
  onChunk: (sessions, progress) => {
286
- if (options.loadId !== this.resumeLoadId)
320
+ if (options.loadId !== this.resumeLoadId || !isCommandScopeActive(this.host, options.scope))
287
321
  return;
288
322
  this.host.setResumeSessions([...sessions]);
289
323
  if (progress.done) {
290
324
  this.host.setResumeLoading(false);
291
325
  if (!options.preserveStatus)
292
- this.host.setSessionStatus(options.session);
326
+ this.host.setSessionStatus(options.scope.session);
293
327
  }
294
328
  this.host.render();
295
329
  },
296
330
  });
297
331
  }
298
332
  catch (error) {
299
- if (options.loadId !== this.resumeLoadId)
333
+ if (options.loadId !== this.resumeLoadId || !isCommandScopeActive(this.host, options.scope))
300
334
  return;
301
335
  this.host.setResumeLoading(false);
302
336
  this.host.setDirectPopupMenu(undefined);
@@ -307,7 +341,7 @@ export class NavigationCommandActions {
307
341
  if (!options.preserveStatus)
308
342
  this.host.toast.error("Failed to load sessions");
309
343
  if (!options.preserveStatus)
310
- this.host.setSessionStatus(options.session);
344
+ this.host.setSessionStatus(options.scope.session);
311
345
  this.host.render();
312
346
  }
313
347
  }
@@ -1,4 +1,4 @@
1
- import type { CommandControllerHost } from "./command-host.js";
1
+ import { type CommandControllerHost } from "./command-host.js";
2
2
  export declare class SessionCommandActions {
3
3
  private readonly host;
4
4
  constructor(host: CommandControllerHost);