pi-ui-extend 0.1.69 → 0.1.71

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 (50) hide show
  1. package/dist/app/app.js +22 -1
  2. package/dist/app/commands/command-host.d.ts +7 -0
  3. package/dist/app/commands/command-host.js +10 -1
  4. package/dist/app/commands/command-model-actions.d.ts +1 -1
  5. package/dist/app/commands/command-model-actions.js +36 -2
  6. package/dist/app/commands/command-navigation-actions.d.ts +1 -1
  7. package/dist/app/commands/command-navigation-actions.js +40 -6
  8. package/dist/app/commands/command-session-actions.d.ts +1 -1
  9. package/dist/app/commands/command-session-actions.js +56 -0
  10. package/dist/app/commands/shell-controller.d.ts +9 -2
  11. package/dist/app/commands/shell-controller.js +32 -21
  12. package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
  13. package/dist/app/extensions/extension-actions-controller.js +25 -7
  14. package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
  15. package/dist/app/extensions/extension-ui-controller.js +104 -64
  16. package/dist/app/input/input-action-controller.d.ts +4 -1
  17. package/dist/app/input/input-action-controller.js +68 -27
  18. package/dist/app/input/input-controller.d.ts +3 -0
  19. package/dist/app/input/input-controller.js +45 -1
  20. package/dist/app/input/input-paste-handler.d.ts +3 -0
  21. package/dist/app/input/input-paste-handler.js +21 -12
  22. package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
  23. package/dist/app/input/prompt-enhancer-controller.js +11 -5
  24. package/dist/app/input/voice-controller.d.ts +4 -0
  25. package/dist/app/input/voice-controller.js +76 -35
  26. package/dist/app/popup/popup-action-controller.d.ts +3 -0
  27. package/dist/app/popup/popup-action-controller.js +43 -8
  28. package/dist/app/session/queued-message-controller.d.ts +5 -4
  29. package/dist/app/session/queued-message-controller.js +32 -27
  30. package/dist/app/session/request-history.d.ts +2 -0
  31. package/dist/app/session/request-history.js +22 -15
  32. package/dist/app/session/session-event-controller.js +28 -8
  33. package/dist/app/session/session-history.js +8 -2
  34. package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
  35. package/dist/app/session/session-lifecycle-controller.js +91 -19
  36. package/dist/app/session/tabs-controller.d.ts +26 -0
  37. package/dist/app/session/tabs-controller.js +469 -96
  38. package/dist/app/terminal/terminal-controller.d.ts +4 -0
  39. package/dist/app/terminal/terminal-controller.js +38 -11
  40. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  41. package/dist/app/workspace/workspace-actions-controller.js +37 -9
  42. package/dist/input-editor.d.ts +2 -0
  43. package/dist/input-editor.js +17 -0
  44. package/docs/concurrency.md +76 -0
  45. package/external/pi-tools-suite/package.json +3 -3
  46. package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
  47. package/external/pi-tools-suite/src/todo/index.ts +2 -2
  48. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +6 -0
  49. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  50. package/package.json +4 -4
package/dist/app/app.js CHANGED
@@ -221,6 +221,7 @@ export class PiUiExtendApp {
221
221
  runtime: () => this.runtime,
222
222
  inputEditor: () => this.inputEditor,
223
223
  activeInputTabId: () => this.tabsController.activeInputTabId(),
224
+ isInputTabOwnedByRuntime: (tabId, runtime, session) => this.tabsController.inputTabOwnedByRuntime(tabId, runtime, session),
224
225
  inputStateForTab: (tabId) => this.tabsController.inputStateForTab(tabId),
225
226
  setInputStateForTab: (tabId, state) => this.tabsController.setInputStateForTab(tabId, state),
226
227
  promptEnhancerConfig: () => this.pixConfig.promptEnhancer,
@@ -239,6 +240,7 @@ export class PiUiExtendApp {
239
240
  render: () => this.render(),
240
241
  });
241
242
  this.voiceController = new AppVoiceController({
243
+ activeInputScope: () => this.tabsController.activeInputTabId(),
242
244
  insertTranscript: (text) => this.insertVoiceTranscript(text),
243
245
  setPartialTranscript: (text) => this.setVoicePartialTranscript(text),
244
246
  addSystemMessage: (message) => this.addVoiceSystemMessage(message),
@@ -320,6 +322,7 @@ export class PiUiExtendApp {
320
322
  this.extensionUiController = new ExtensionUiController({
321
323
  theme: this.theme,
322
324
  activeExtensionUiScope: () => this.activeExtensionUiScope(),
325
+ isExtensionUiScopeActive: (scopeKey) => !this.tabsController.isSwitching() && this.activeExtensionUiScope() === scopeKey,
323
326
  isRunning: () => this.running,
324
327
  render: () => this.render(),
325
328
  showToast: (message, kind, options) => this.showToast(message, kind, options),
@@ -335,6 +338,7 @@ export class PiUiExtendApp {
335
338
  });
336
339
  this.extensionActions = new AppExtensionActionsController({
337
340
  isRunning: () => this.running,
341
+ runtime: () => this.runtime,
338
342
  getInput: () => this.input,
339
343
  setInput: (value) => this.setInput(value),
340
344
  awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
@@ -430,6 +434,7 @@ export class PiUiExtendApp {
430
434
  setInput: (value) => this.inputEditor.setText(value),
431
435
  insertInput: (value) => this.inputEditor.insert(value),
432
436
  attachImage: (data, mimeType) => this.inputEditor.attachImage(data, mimeType),
437
+ requeueAutoUserMessageForSession: (session, message) => this.tabsController.requeueAutoUserMessageForSession(session, message),
433
438
  onDeferredUserMessagesChanged: () => this.tabsController.persistActiveDeferredUserMessages(),
434
439
  });
435
440
  this.editorLayoutRenderer = new EditorLayoutRenderer({
@@ -648,6 +653,7 @@ export class PiUiExtendApp {
648
653
  this.shellController = new AppShellController({
649
654
  cwd: this.options.cwd,
650
655
  isRunning: () => this.running,
656
+ activeScopeKey: () => this.tabsController.activeInputTabId(),
651
657
  addEntry: (entry) => this.addEntry(entry),
652
658
  touchEntry: (entry) => this.touchEntry(entry),
653
659
  setStatus: (status) => this.setStatus(status),
@@ -658,6 +664,7 @@ export class PiUiExtendApp {
658
664
  });
659
665
  this.inputActions = new AppInputActionController({
660
666
  runtime: () => this.runtime,
667
+ inputScopeKey: () => this.tabsController.activeInputTabId(),
661
668
  isRunning: () => this.running,
662
669
  isSessionSwitching: () => this.tabsController.isSwitching(),
663
670
  inputEditor: () => this.inputEditor,
@@ -688,6 +695,7 @@ export class PiUiExtendApp {
688
695
  this.inputController = new AppInputController({
689
696
  inputEditor: this.inputEditor,
690
697
  cwd: this.options.cwd,
698
+ inputScopeKey: () => this.tabsController.activeInputTabId(),
691
699
  handleExtensionTerminalInput: (data) => this.extensionUiController.handleTerminalInput(data),
692
700
  extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
693
701
  getInput: () => this.input,
@@ -718,6 +726,7 @@ export class PiUiExtendApp {
718
726
  setRunning: (running) => {
719
727
  this.running = running;
720
728
  },
729
+ cancelPendingTabLifecycle: () => this.tabsController.cancelPendingLifecycleWork(),
721
730
  runtime: () => this.runtime,
722
731
  saveInputStateForQuit: () => this.tabsController.saveInputStateForQuit(),
723
732
  disposeInactiveRuntimesForQuit: () => this.tabsController.disposeInactiveRuntimes((runtime) => this.terminalController.disposeRuntimeForQuit(runtime)),
@@ -874,11 +883,17 @@ export class PiUiExtendApp {
874
883
  await this.sessionLifecycle.awaitCurrentSessionExtensions(runtime);
875
884
  }
876
885
  async activateRuntime(runtime, options) {
886
+ this.extensionUiController.cancelCustomUi(this.activeExtensionUiScope());
887
+ void this.voiceController.stopRecording();
877
888
  this.runtime = runtime;
878
889
  runtime.setRebindSession(async () => {
890
+ if (!this.running || this.runtime !== runtime)
891
+ return;
879
892
  await this.bindCurrentSession({ awaitExtensions: false });
880
893
  });
881
894
  runtime.setBeforeSessionInvalidate(() => {
895
+ if (!this.running || this.runtime !== runtime)
896
+ return;
882
897
  this.extensionUiController.clearWidgets(this.activeExtensionUiScope());
883
898
  });
884
899
  await this.bindCurrentSession(options);
@@ -929,7 +944,7 @@ export class PiUiExtendApp {
929
944
  restoreTabInputState(state) {
930
945
  this.requestHistory.resetNavigation();
931
946
  this.popupMenus.resetInputMenuDismissals();
932
- this.inputEditor.setDraftState(state);
947
+ this.inputEditor.restoreDraftState(state);
933
948
  this.autocompleteController.dispose();
934
949
  }
935
950
  async clearPersistedInputDraft() {
@@ -1130,6 +1145,8 @@ export class PiUiExtendApp {
1130
1145
  });
1131
1146
  }
1132
1147
  showToast(message, kind = "info", options) {
1148
+ if (!this.running)
1149
+ return;
1133
1150
  this.toastController.showToast(message, kind, options);
1134
1151
  }
1135
1152
  toastNotifierForScope(scopeKey) {
@@ -1154,6 +1171,8 @@ export class PiUiExtendApp {
1154
1171
  clearTimeout(this.scheduledRenderTimer);
1155
1172
  this.scheduledRenderTimer = undefined;
1156
1173
  }
1174
+ if (!this.running)
1175
+ return;
1157
1176
  this.autocompleteController.observeInput();
1158
1177
  this.syncScrollAfterInputEditorChange();
1159
1178
  this.renderController.render();
@@ -1163,6 +1182,8 @@ export class PiUiExtendApp {
1163
1182
  return;
1164
1183
  this.scheduledRenderTimer = setTimeout(() => {
1165
1184
  this.scheduledRenderTimer = undefined;
1185
+ if (!this.running)
1186
+ return;
1166
1187
  this.syncScrollAfterInputEditorChange();
1167
1188
  this.renderController.render();
1168
1189
  }, 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);