pi-ui-extend 0.1.70 → 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 (49) 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/tool-descriptions.ts +1 -1
  49. package/package.json +4 -4
@@ -75,6 +75,9 @@ export class ExtensionUiController {
75
75
  this.extensionWidgets.delete(key);
76
76
  }
77
77
  }
78
+ cancelCustomUi(scopeKey = this.activeScopeKey()) {
79
+ this.cancelActiveCustomUi(this.normalizeScopeKey(scopeKey));
80
+ }
78
81
  suppressWidget(key) {
79
82
  const scopedKey = this.scopedWidgetKey(this.activeScopeKey(), key);
80
83
  const widget = this.extensionWidgets.get(scopedKey);
@@ -85,7 +88,7 @@ export class ExtensionUiController {
85
88
  }
86
89
  handleTerminalInput(data) {
87
90
  const active = this.activeCustomUiForActiveScope();
88
- if (active) {
91
+ if (active?.component) {
89
92
  if (data === "\u0003")
90
93
  return { consume: false };
91
94
  try {
@@ -117,7 +120,7 @@ export class ExtensionUiController {
117
120
  }
118
121
  renderActiveCustomUi(width) {
119
122
  const active = this.activeCustomUiForActiveScope();
120
- if (!active)
123
+ if (!active?.component)
121
124
  return undefined;
122
125
  try {
123
126
  return active.component.render(width);
@@ -128,7 +131,7 @@ export class ExtensionUiController {
128
131
  }
129
132
  activeCustomUiUsesEditor() {
130
133
  const active = this.activeCustomUiForActiveScope();
131
- if (!active)
134
+ if (!active?.component)
132
135
  return false;
133
136
  try {
134
137
  return active.component.usesEditor?.() === true;
@@ -139,7 +142,7 @@ export class ExtensionUiController {
139
142
  }
140
143
  handleCustomUiMouse(event) {
141
144
  const active = this.activeCustomUiForActiveScope();
142
- if (!active)
145
+ if (!active?.component)
143
146
  return false;
144
147
  try {
145
148
  return active.component.handleMouse?.(event) === true;
@@ -149,17 +152,18 @@ export class ExtensionUiController {
149
152
  return true;
150
153
  }
151
154
  }
152
- widgetTuiHandle() {
153
- const activeScopeToastNotifier = this.host.toastNotifierForScope?.(this.activeScopeKey()) ?? this.host.toastNotifier;
155
+ widgetTuiHandle(scopeKey = this.activeScopeKey()) {
156
+ const activeScopeToastNotifier = this.host.toastNotifierForScope?.(scopeKey) ?? this.host.toastNotifier;
157
+ const menuController = this.scopedMenuController(scopeKey);
154
158
  return {
155
159
  requestRender: () => {
156
- if (this.host.isRunning())
160
+ if (this.host.isRunning() && this.isScopeActive(scopeKey))
157
161
  this.host.render();
158
162
  },
159
163
  showToast: activeScopeToastNotifier.show,
160
164
  toast: activeScopeToastNotifier,
161
- showMenu: this.host.menuController.show,
162
- menu: this.host.menuController,
165
+ showMenu: menuController.show,
166
+ menu: menuController,
163
167
  pix: {
164
168
  delegatedEditorInput: true,
165
169
  inputMouse: true,
@@ -173,13 +177,14 @@ export class ExtensionUiController {
173
177
  this.host.showToast(message, isToastKind(type) ? type : "info", { scopeKey: contextScopeKey });
174
178
  };
175
179
  const extensionTheme = this.createExtensionTheme();
180
+ const menuController = this.scopedMenuController(contextScopeKey);
176
181
  const renderIfRunning = () => {
177
- if (this.host.isRunning())
182
+ if (this.host.isRunning() && this.isScopeActive(contextScopeKey))
178
183
  this.host.render();
179
184
  };
180
185
  return {
181
- select: async (title, options, opts) => await this.selectDialog(title, options, opts),
182
- confirm: async (title, message, opts) => await this.confirmDialog(title, message, opts),
186
+ select: async (title, options, opts) => await this.selectDialog(title, options, opts, contextScopeKey),
187
+ confirm: async (title, message, opts) => await this.confirmDialog(title, message, opts, contextScopeKey),
183
188
  input: async (title, placeholder, opts) => await this.inputDialog(title, placeholder, opts, contextScopeKey),
184
189
  notify,
185
190
  toast: scopedToastNotifier,
@@ -194,8 +199,8 @@ export class ExtensionUiController {
194
199
  renderAboveInput: (key, content) => {
195
200
  this.setAboveInputWidget(key, content, contextScopeKey);
196
201
  },
197
- showMenu: this.host.menuController.show,
198
- menu: this.host.menuController,
202
+ showMenu: menuController.show,
203
+ menu: menuController,
199
204
  onTerminalInput: (handler) => {
200
205
  const terminalInputHandler = { scopeKey: contextScopeKey, handler: handler };
201
206
  this.terminalInputHandlers.add(terminalInputHandler);
@@ -204,12 +209,16 @@ export class ExtensionUiController {
204
209
  };
205
210
  },
206
211
  setStatus: (_key, text) => {
212
+ if (!this.isScopeActive(contextScopeKey))
213
+ return;
207
214
  if (text)
208
215
  this.host.showToast(text, "info", { scopeKey: contextScopeKey });
209
216
  this.host.restoreSessionStatus();
210
217
  renderIfRunning();
211
218
  },
212
219
  setWorkingMessage: (message) => {
220
+ if (!this.isScopeActive(contextScopeKey))
221
+ return;
213
222
  if (message)
214
223
  this.host.showToast(message, "info", { scopeKey: contextScopeKey });
215
224
  this.host.restoreSessionStatus();
@@ -224,19 +233,25 @@ export class ExtensionUiController {
224
233
  setFooter: () => undefined,
225
234
  setHeader: () => undefined,
226
235
  setTitle: (title) => {
236
+ if (!this.isScopeActive(contextScopeKey))
237
+ return;
227
238
  process.title = title;
228
239
  renderIfRunning();
229
240
  },
230
241
  custom: (async (factory) => await this.showCustomUi(factory, { scopeKey: contextScopeKey })),
231
242
  pasteToEditor: (text) => {
243
+ if (!this.isScopeActive(contextScopeKey))
244
+ return;
232
245
  this.host.setInput(text);
233
246
  renderIfRunning();
234
247
  },
235
248
  setEditorText: (text) => {
249
+ if (!this.isScopeActive(contextScopeKey))
250
+ return;
236
251
  this.host.setInput(text);
237
252
  renderIfRunning();
238
253
  },
239
- getEditorText: () => this.host.getInput(),
254
+ getEditorText: () => this.isScopeActive(contextScopeKey) ? this.host.getInput() : "",
240
255
  editor: async (title, prefill) => await this.editorDialog(title, prefill, contextScopeKey),
241
256
  addAutocompleteProvider: () => undefined,
242
257
  setEditorComponent: () => undefined,
@@ -247,8 +262,10 @@ export class ExtensionUiController {
247
262
  getAllThemes: () => Object.keys(THEMES).map((themeName) => ({ name: themeName, path: undefined })),
248
263
  getTheme: () => undefined,
249
264
  setTheme: () => ({ success: false, error: "Theme switching is not implemented in pix extension UI yet." }),
250
- getToolsExpanded: () => this.host.entries.some((entry) => entry.kind === "tool" && entry.expanded),
265
+ getToolsExpanded: () => this.isScopeActive(contextScopeKey) && this.host.entries.some((entry) => entry.kind === "tool" && entry.expanded),
251
266
  setToolsExpanded: (expanded) => {
267
+ if (!this.isScopeActive(contextScopeKey))
268
+ return;
252
269
  for (const entry of this.host.entries) {
253
270
  if (entry.kind === "tool") {
254
271
  entry.expanded = expanded;
@@ -265,21 +282,23 @@ export class ExtensionUiController {
265
282
  clearAboveInputWidget(key, scopeKey = this.activeScopeKey()) {
266
283
  this.setWidget(key, undefined, { placement: "aboveEditor", scopeKey });
267
284
  }
268
- async selectDialog(title, options, opts) {
269
- if (opts?.signal?.aborted)
285
+ async selectDialog(title, options, opts, scopeKey = this.activeScopeKey()) {
286
+ if (opts?.signal?.aborted || !this.isScopeActive(scopeKey))
270
287
  return undefined;
271
288
  return await this.withDialogAutoDismiss(this.host.menuController.select(title, options, { preserveStatus: true }), opts, () => {
272
- this.host.menuController.close();
289
+ if (this.isScopeActive(scopeKey))
290
+ this.host.menuController.close();
273
291
  });
274
292
  }
275
- async confirmDialog(title, message, opts) {
276
- if (opts?.signal?.aborted)
293
+ async confirmDialog(title, message, opts, scopeKey = this.activeScopeKey()) {
294
+ if (opts?.signal?.aborted || !this.isScopeActive(scopeKey))
277
295
  return false;
278
296
  const selected = await this.withDialogAutoDismiss(this.host.menuController.show([
279
297
  { value: true, label: "Yes", description: message },
280
298
  { value: false, label: "No" },
281
299
  ], { title, searchable: false, preserveStatus: true }), opts, () => {
282
- this.host.menuController.close();
300
+ if (this.isScopeActive(scopeKey))
301
+ this.host.menuController.close();
283
302
  });
284
303
  return selected === true;
285
304
  }
@@ -306,10 +325,10 @@ export class ExtensionUiController {
306
325
  return undefined;
307
326
  if (!this.host.isRunning())
308
327
  return undefined;
328
+ if (!this.isScopeActive(scopeKey))
329
+ return undefined;
309
330
  if (this.activeCustomUis.has(scopeKey))
310
331
  throw new Error("Another extension custom UI is already active.");
311
- const savedInput = this.host.getInput();
312
- this.host.setInput(options.initialValue);
313
332
  const promise = this.showCustomUi((_tui, _theme, _keybindings, done) => {
314
333
  let settled = false;
315
334
  const finish = (value) => {
@@ -333,7 +352,7 @@ export class ExtensionUiController {
333
352
  return { consume: false, data };
334
353
  },
335
354
  };
336
- }, { savedInput, scopeKey });
355
+ }, { editorInput: options.initialValue, scopeKey });
337
356
  return await this.withDialogAutoDismiss(promise, options.opts, () => {
338
357
  this.cancelActiveCustomUi(scopeKey);
339
358
  });
@@ -375,79 +394,76 @@ export class ExtensionUiController {
375
394
  if (!this.host.isRunning())
376
395
  return undefined;
377
396
  const scopeKey = this.normalizeScopeKey(options.scopeKey);
397
+ if (!this.isScopeActive(scopeKey))
398
+ return undefined;
378
399
  if (this.activeCustomUis.has(scopeKey))
379
400
  throw new Error("Another extension custom UI is already active.");
380
401
  const savedInput = options.savedInput ?? this.host.getInput();
381
402
  return await new Promise((resolve, reject) => {
382
- let settled = false;
403
+ const active = {
404
+ key: CUSTOM_UI_WIDGET_KEY,
405
+ scopeKey,
406
+ savedInput,
407
+ settled: false,
408
+ resolve: (value) => resolve(value),
409
+ reject,
410
+ };
411
+ this.activeCustomUis.set(scopeKey, active);
412
+ if (options.editorInput !== undefined)
413
+ this.host.setInput(options.editorInput);
383
414
  const done = (value) => {
384
- if (settled)
415
+ if (active.settled || this.activeCustomUis.get(scopeKey) !== active)
385
416
  return;
386
- settled = true;
387
- this.finishActiveCustomUi(scopeKey, value, { resolve: true });
388
- resolve(value);
417
+ this.finishActiveCustomUi(active, this.isScopeActive(scopeKey) ? value : undefined, { resolve: true });
389
418
  };
390
419
  void (async () => {
391
420
  try {
392
- const component = await factory(this.widgetTuiHandle(), this.createExtensionTheme(), {}, done);
393
- if (settled) {
421
+ const component = await factory(this.widgetTuiHandle(scopeKey), this.createExtensionTheme(), {}, done);
422
+ if (active.settled || this.activeCustomUis.get(scopeKey) !== active || !this.isScopeActive(scopeKey)) {
394
423
  component.dispose?.();
424
+ if (!active.settled && this.activeCustomUis.get(scopeKey) === active) {
425
+ this.finishActiveCustomUi(active, undefined, { resolve: true });
426
+ }
395
427
  return;
396
428
  }
397
- this.activeCustomUis.set(scopeKey, {
398
- key: CUSTOM_UI_WIDGET_KEY,
399
- scopeKey,
400
- component,
401
- savedInput,
402
- resolve: (value) => {
403
- if (settled)
404
- return;
405
- settled = true;
406
- resolve(value);
407
- },
408
- reject: (error) => {
409
- if (settled)
410
- return;
411
- settled = true;
412
- reject(error);
413
- },
414
- });
429
+ active.component = component;
415
430
  if (this.host.isRunning())
416
431
  this.host.render();
417
432
  }
418
433
  catch (error) {
419
- if (settled)
434
+ if (active.settled || this.activeCustomUis.get(scopeKey) !== active)
420
435
  return;
421
- settled = true;
422
- reject(error);
436
+ this.finishActiveCustomUi(active, error, { resolve: false });
423
437
  }
424
438
  })();
425
439
  });
426
440
  }
427
441
  cancelActiveCustomUi(scopeKey = this.activeScopeKey()) {
428
- this.finishActiveCustomUi(scopeKey, undefined, { resolve: true });
442
+ const active = this.activeCustomUis.get(scopeKey);
443
+ if (active)
444
+ this.finishActiveCustomUi(active, undefined, { resolve: true });
429
445
  }
430
446
  rejectActiveCustomUi(error) {
431
447
  const active = this.activeCustomUiForActiveScope();
432
448
  if (!active)
433
449
  return;
434
- this.finishActiveCustomUi(active.scopeKey, error, { resolve: false });
450
+ this.finishActiveCustomUi(active, error, { resolve: false });
435
451
  }
436
- finishActiveCustomUi(scopeKey, value, options) {
437
- const active = this.activeCustomUis.get(scopeKey);
438
- if (!active)
452
+ finishActiveCustomUi(active, value, options) {
453
+ if (active.settled || this.activeCustomUis.get(active.scopeKey) !== active)
439
454
  return;
440
- this.activeCustomUis.delete(scopeKey);
441
- if (this.host.getInput() !== active.savedInput)
455
+ active.settled = true;
456
+ this.activeCustomUis.delete(active.scopeKey);
457
+ if (this.isScopeActive(active.scopeKey) && this.host.getInput() !== active.savedInput)
442
458
  this.host.setInput(active.savedInput);
443
459
  try {
444
- active.component.dispose?.();
460
+ active.component?.dispose?.();
445
461
  }
446
462
  catch {
447
463
  // Ignore extension cleanup failures while closing focused UI.
448
464
  }
449
465
  try {
450
- active.component.invalidate?.();
466
+ active.component?.invalidate?.();
451
467
  }
452
468
  catch {
453
469
  // Ignore extension invalidation failures while closing focused UI.
@@ -456,11 +472,35 @@ export class ExtensionUiController {
456
472
  active.resolve(value);
457
473
  else
458
474
  active.reject(value);
459
- if (this.host.isRunning())
475
+ if (this.host.isRunning() && this.isScopeActive(active.scopeKey))
460
476
  this.host.render();
461
477
  }
462
478
  activeCustomUiForActiveScope() {
463
- return this.activeCustomUis.get(this.activeScopeKey());
479
+ const scopeKey = this.activeScopeKey();
480
+ if (!this.isScopeActive(scopeKey))
481
+ return undefined;
482
+ return this.activeCustomUis.get(scopeKey);
483
+ }
484
+ isScopeActive(scopeKey) {
485
+ return this.host.isExtensionUiScopeActive?.(scopeKey) ?? this.activeScopeKey() === scopeKey;
486
+ }
487
+ scopedMenuController(scopeKey) {
488
+ return {
489
+ show: async (items, options) => {
490
+ if (!this.isScopeActive(scopeKey))
491
+ return undefined;
492
+ return await this.host.menuController.show(items, options);
493
+ },
494
+ select: async (title, options, menuOptions) => {
495
+ if (!this.isScopeActive(scopeKey))
496
+ return undefined;
497
+ return await this.host.menuController.select(title, options, menuOptions);
498
+ },
499
+ close: () => {
500
+ if (this.isScopeActive(scopeKey))
501
+ this.host.menuController.close();
502
+ },
503
+ };
464
504
  }
465
505
  activeScopeKey() {
466
506
  return this.normalizeScopeKey(this.host.activeExtensionUiScope?.());
@@ -8,6 +8,7 @@ import { type InteractiveShellCommandResult } from "../commands/shell-command.js
8
8
  import type { Entry, SessionActivity } from "../types.js";
9
9
  export type AppInputActionControllerHost = {
10
10
  runtime(): AgentSessionRuntime | undefined;
11
+ inputScopeKey?(): string | undefined;
11
12
  isRunning(): boolean;
12
13
  isSessionSwitching(): boolean;
13
14
  inputEditor(): InputEditor;
@@ -35,7 +36,7 @@ export declare class AppInputActionController {
35
36
  private readonly popupMenus;
36
37
  private readonly popupActions;
37
38
  private readonly queuedMessages;
38
- private abortInFlight;
39
+ private readonly abortsInFlight;
39
40
  constructor(host: AppInputActionControllerHost, popupMenus: AppPopupMenuController, popupActions: AppPopupActionController, queuedMessages: AppQueuedMessageController);
40
41
  handleEnter(): void;
41
42
  queueInputFromEditor(): Promise<void>;
@@ -48,4 +49,6 @@ export declare class AppInputActionController {
48
49
  private submitInput;
49
50
  private submitShellInput;
50
51
  private submitShellCommand;
52
+ private isInputScopeActive;
53
+ private isSessionScopeActive;
51
54
  }
@@ -7,7 +7,7 @@ export class AppInputActionController {
7
7
  popupMenus;
8
8
  popupActions;
9
9
  queuedMessages;
10
- abortInFlight = false;
10
+ abortsInFlight = new Set();
11
11
  constructor(host, popupMenus, popupActions, queuedMessages) {
12
12
  this.host = host;
13
13
  this.popupMenus = popupMenus;
@@ -22,7 +22,10 @@ export class AppInputActionController {
22
22
  void this.submitInput();
23
23
  }
24
24
  async queueInputFromEditor() {
25
+ const inputScopeKey = this.host.inputScopeKey?.();
25
26
  await this.host.stopVoiceInput();
27
+ if (this.host.inputScopeKey && this.host.inputScopeKey() !== inputScopeKey)
28
+ return;
26
29
  if (this.popupMenus.syncActivePopupMenu())
27
30
  this.popupMenus.cancelActivePopupMenu();
28
31
  const inputEditor = this.host.inputEditor();
@@ -36,9 +39,8 @@ export class AppInputActionController {
36
39
  const message = this.queuedMessages.createSubmittedUserMessage(promptText, displayText, images);
37
40
  this.host.requestHistory().add(message.displayText);
38
41
  inputEditor.clear();
39
- await this.host.clearPersistedInputDraft();
40
- this.host.render();
41
42
  this.queuedMessages.deferUserMessage(message);
43
+ await this.host.clearPersistedInputDraft();
42
44
  if (this.host.isRunning())
43
45
  this.host.render();
44
46
  }
@@ -91,7 +93,7 @@ export class AppInputActionController {
91
93
  // Relay the user-initiated abort to extensions (e.g. the terminal-bell
92
94
  // extension) so they can suppress the attention bell for this turn.
93
95
  this.host.emitSessionAborted();
94
- if (this.abortInFlight) {
96
+ if (this.abortsInFlight.has(session)) {
95
97
  session.agent.abort();
96
98
  if (options.stopIfAlreadyAborting)
97
99
  await this.host.stop();
@@ -99,13 +101,13 @@ export class AppInputActionController {
99
101
  this.host.render();
100
102
  return;
101
103
  }
102
- this.abortInFlight = true;
104
+ this.abortsInFlight.add(session);
103
105
  this.queuedMessages.restoreQueuedMessagesToEditorForAbort();
104
106
  this.host.setStatus("aborting");
105
107
  this.host.addSessionAbortedEntry();
106
108
  this.host.render();
107
109
  let restoreTimer = setTimeout(() => {
108
- if (!this.abortInFlight || this.host.runtime()?.session !== session || !this.host.isRunning())
110
+ if (!this.abortsInFlight.has(session) || this.host.runtime()?.session !== session || !this.host.isRunning())
109
111
  return;
110
112
  this.restoreSessionState(session);
111
113
  this.host.render();
@@ -116,16 +118,23 @@ export class AppInputActionController {
116
118
  await session.abort();
117
119
  }
118
120
  catch (error) {
119
- this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
121
+ if (this.host.runtime()?.session === session) {
122
+ this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
123
+ }
124
+ else {
125
+ this.host.showToast(`Abort failed in a background tab: ${stringifyUnknown(error)}`, "error");
126
+ }
120
127
  }
121
128
  finally {
122
129
  if (restoreTimer)
123
130
  clearTimeout(restoreTimer);
124
131
  restoreTimer = undefined;
125
- this.abortInFlight = false;
126
- this.restoreSessionState(this.host.runtime()?.session);
127
- if (this.host.isRunning())
128
- this.host.render();
132
+ this.abortsInFlight.delete(session);
133
+ if (this.host.runtime()?.session === session) {
134
+ this.restoreSessionState(session);
135
+ if (this.host.isRunning())
136
+ this.host.render();
137
+ }
129
138
  }
130
139
  }
131
140
  restoreSessionState(session) {
@@ -136,7 +145,11 @@ export class AppInputActionController {
136
145
  return session?.isStreaming || session?.isCompacting ? "running" : "idle";
137
146
  }
138
147
  async submitInput() {
148
+ const inputScopeKey = this.host.inputScopeKey?.();
139
149
  await this.host.stopVoiceInput();
150
+ if (this.host.inputScopeKey && this.host.inputScopeKey() !== inputScopeKey)
151
+ return;
152
+ const runtime = this.host.runtime();
140
153
  const inputEditor = this.host.inputEditor();
141
154
  const rawPromptText = inputEditor.promptText;
142
155
  const rawDisplayText = inputEditor.expandedText;
@@ -144,14 +157,14 @@ export class AppInputActionController {
144
157
  const displayText = rawDisplayText.trimEnd();
145
158
  const images = [...inputEditor.images];
146
159
  if (this.host.isShellCommandRunning()) {
147
- await this.submitShellInput(rawDisplayText, images.length);
160
+ await this.submitShellInput(rawDisplayText, images.length, inputScopeKey);
148
161
  return;
149
162
  }
150
163
  if (!promptText && images.length === 0)
151
164
  return;
152
165
  const shellCommand = bangShellCommandFromInput(promptText);
153
166
  if (shellCommand !== undefined) {
154
- await this.submitShellCommand(shellCommand.command, displayText, images.length, shellCommand.interactive ? "interactive" : "chat");
167
+ await this.submitShellCommand(shellCommand.command, displayText, images.length, shellCommand.interactive ? "interactive" : "chat", inputScopeKey);
155
168
  return;
156
169
  }
157
170
  if (this.host.isSessionSwitching()) {
@@ -166,18 +179,24 @@ export class AppInputActionController {
166
179
  const message = this.queuedMessages.createSubmittedUserMessage(promptText, displayText, images);
167
180
  this.host.requestHistory().add(message.displayText);
168
181
  inputEditor.clear();
169
- await this.host.clearPersistedInputDraft();
182
+ const clearPersistedDraft = this.host.clearPersistedInputDraft();
170
183
  this.host.render();
171
184
  try {
172
- await this.queuedMessages.submitUserMessage(message);
185
+ await this.queuedMessages.submitUserMessage(message, runtime?.session);
173
186
  }
174
187
  catch (error) {
175
- this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
188
+ if (!runtime || this.host.runtime() === runtime) {
189
+ this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
190
+ }
191
+ else {
192
+ this.host.showToast(`Prompt failed in a background tab: ${stringifyUnknown(error)}`, "error");
193
+ }
176
194
  }
195
+ await clearPersistedDraft;
177
196
  if (this.host.isRunning())
178
197
  this.host.render();
179
198
  }
180
- async submitShellInput(text, imageCount) {
199
+ async submitShellInput(text, imageCount, inputScopeKey) {
181
200
  if (imageCount > 0) {
182
201
  this.host.showToast("Shell stdin cannot include pasted images", "warning");
183
202
  this.host.render();
@@ -185,12 +204,17 @@ export class AppInputActionController {
185
204
  }
186
205
  const inputEditor = this.host.inputEditor();
187
206
  inputEditor.clear();
188
- await this.host.clearPersistedInputDraft();
189
- if (!this.host.sendShellInput(text))
207
+ const clearPersistedDraft = this.host.clearPersistedInputDraft();
208
+ const sent = this.host.sendShellInput(text);
209
+ await clearPersistedDraft;
210
+ if (!this.isInputScopeActive(inputScopeKey))
211
+ return;
212
+ if (!sent)
190
213
  this.host.showToast("No shell command is waiting for input", "info");
191
- this.host.render();
214
+ if (this.host.isRunning())
215
+ this.host.render();
192
216
  }
193
- async submitShellCommand(command, displayText, imageCount, mode) {
217
+ async submitShellCommand(command, displayText, imageCount, mode, inputScopeKey) {
194
218
  if (!command) {
195
219
  this.host.showToast(`Enter a shell command after ${mode === "interactive" ? "!!" : "!"}`, "info");
196
220
  this.host.render();
@@ -206,7 +230,8 @@ export class AppInputActionController {
206
230
  this.host.render();
207
231
  return;
208
232
  }
209
- const session = this.host.runtime()?.session;
233
+ const runtime = this.host.runtime();
234
+ const session = runtime?.session;
210
235
  if (session?.isStreaming || session?.isCompacting) {
211
236
  this.host.showToast("Wait for the current session turn to finish before running shell commands", "info");
212
237
  this.host.render();
@@ -215,26 +240,42 @@ export class AppInputActionController {
215
240
  const inputEditor = this.host.inputEditor();
216
241
  this.host.requestHistory().add(displayText);
217
242
  inputEditor.clear();
218
- await this.host.clearPersistedInputDraft();
243
+ const clearPersistedDraft = this.host.clearPersistedInputDraft();
219
244
  this.host.setStatus(`shell: ${command}`);
220
245
  this.host.render();
221
246
  try {
222
247
  if (mode === "chat") {
223
- await this.host.runChatShellCommand(command);
248
+ const result = this.host.runChatShellCommand(command);
249
+ await clearPersistedDraft;
250
+ await result;
224
251
  }
225
252
  else {
226
- const result = await this.host.runInteractiveShellCommand(command);
253
+ const runningCommand = this.host.runInteractiveShellCommand(command);
254
+ await clearPersistedDraft;
255
+ const result = await runningCommand;
256
+ if (!this.isSessionScopeActive(runtime, session, inputScopeKey))
257
+ return;
227
258
  const entryKind = result.error ? "error" : "system";
228
259
  this.host.addEntry({ id: createId(entryKind), kind: entryKind, text: formatShellCommandEntry(command, result, "!!") });
229
260
  }
230
261
  }
231
262
  catch (error) {
263
+ await clearPersistedDraft;
264
+ if (!this.isSessionScopeActive(runtime, session, inputScopeKey))
265
+ return;
232
266
  this.host.addEntry({ id: createId("error"), kind: "error", text: `Shell command failed: ${stringifyUnknown(error)}` });
233
267
  }
234
268
  finally {
235
- this.restoreSessionState(this.host.runtime()?.session);
269
+ if (this.isSessionScopeActive(runtime, session, inputScopeKey))
270
+ this.restoreSessionState(session);
236
271
  }
237
- if (this.host.isRunning())
272
+ if (this.host.isRunning() && this.isSessionScopeActive(runtime, session, inputScopeKey))
238
273
  this.host.render();
239
274
  }
275
+ isInputScopeActive(inputScopeKey) {
276
+ return !this.host.inputScopeKey || this.host.inputScopeKey() === inputScopeKey;
277
+ }
278
+ isSessionScopeActive(runtime, session, inputScopeKey) {
279
+ return this.isInputScopeActive(inputScopeKey) && this.host.runtime() === runtime && runtime?.session === session;
280
+ }
240
281
  }
@@ -5,6 +5,7 @@ type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
5
5
  export type InputControllerHost = {
6
6
  readonly inputEditor: InputEditor;
7
7
  readonly cwd: string;
8
+ inputScopeKey?(): string | undefined;
8
9
  handleExtensionTerminalInput(data: string): ExtensionTerminalInputResult;
9
10
  extensionInputUsesEditor?(): boolean;
10
11
  isShiftPressed?(): boolean;
@@ -32,12 +33,14 @@ export type InputControllerHost = {
32
33
  export declare class AppInputController {
33
34
  private readonly host;
34
35
  private inputBuffer;
36
+ private readonly inputDecoder;
35
37
  private readonly pasteHandler;
36
38
  constructor(host: InputControllerHost);
37
39
  handleChunk(chunk: Buffer): void;
38
40
  private consumeBufferedSharedEditorInput;
39
41
  private consumeSharedEditorInput;
40
42
  private drainInputBuffer;
43
+ private consumePrintableRun;
41
44
  private consumeBracketedPastePayload;
42
45
  private getEscapeSequences;
43
46
  private isPendingEscapeSequence;