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.
- package/dist/app/app.js +22 -1
- package/dist/app/commands/command-host.d.ts +7 -0
- package/dist/app/commands/command-host.js +10 -1
- package/dist/app/commands/command-model-actions.d.ts +1 -1
- package/dist/app/commands/command-model-actions.js +36 -2
- package/dist/app/commands/command-navigation-actions.d.ts +1 -1
- package/dist/app/commands/command-navigation-actions.js +40 -6
- package/dist/app/commands/command-session-actions.d.ts +1 -1
- package/dist/app/commands/command-session-actions.js +56 -0
- package/dist/app/commands/shell-controller.d.ts +9 -2
- package/dist/app/commands/shell-controller.js +32 -21
- package/dist/app/extensions/extension-actions-controller.d.ts +3 -0
- package/dist/app/extensions/extension-actions-controller.js +25 -7
- package/dist/app/extensions/extension-ui-controller.d.ts +5 -1
- package/dist/app/extensions/extension-ui-controller.js +104 -64
- package/dist/app/input/input-action-controller.d.ts +4 -1
- package/dist/app/input/input-action-controller.js +68 -27
- package/dist/app/input/input-controller.d.ts +3 -0
- package/dist/app/input/input-controller.js +45 -1
- package/dist/app/input/input-paste-handler.d.ts +3 -0
- package/dist/app/input/input-paste-handler.js +21 -12
- package/dist/app/input/prompt-enhancer-controller.d.ts +1 -0
- package/dist/app/input/prompt-enhancer-controller.js +11 -5
- package/dist/app/input/voice-controller.d.ts +4 -0
- package/dist/app/input/voice-controller.js +76 -35
- package/dist/app/popup/popup-action-controller.d.ts +3 -0
- package/dist/app/popup/popup-action-controller.js +43 -8
- package/dist/app/session/queued-message-controller.d.ts +5 -4
- package/dist/app/session/queued-message-controller.js +32 -27
- package/dist/app/session/request-history.d.ts +2 -0
- package/dist/app/session/request-history.js +22 -15
- package/dist/app/session/session-event-controller.js +28 -8
- package/dist/app/session/session-history.js +8 -2
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -0
- package/dist/app/session/session-lifecycle-controller.js +91 -19
- package/dist/app/session/tabs-controller.d.ts +26 -0
- package/dist/app/session/tabs-controller.js +469 -96
- package/dist/app/terminal/terminal-controller.d.ts +4 -0
- package/dist/app/terminal/terminal-controller.js +38 -11
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +37 -9
- package/dist/input-editor.d.ts +2 -0
- package/dist/input-editor.js +17 -0
- package/docs/concurrency.md +76 -0
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +29 -14
- package/external/pi-tools-suite/src/todo/index.ts +2 -2
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- 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?.(
|
|
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:
|
|
162
|
-
menu:
|
|
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:
|
|
198
|
-
menu:
|
|
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.
|
|
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.
|
|
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
|
-
}, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
450
|
+
this.finishActiveCustomUi(active, error, { resolve: false });
|
|
435
451
|
}
|
|
436
|
-
finishActiveCustomUi(
|
|
437
|
-
|
|
438
|
-
if (!active)
|
|
452
|
+
finishActiveCustomUi(active, value, options) {
|
|
453
|
+
if (active.settled || this.activeCustomUis.get(active.scopeKey) !== active)
|
|
439
454
|
return;
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.host.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
189
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
const result = this.host.runChatShellCommand(command);
|
|
249
|
+
await clearPersistedDraft;
|
|
250
|
+
await result;
|
|
224
251
|
}
|
|
225
252
|
else {
|
|
226
|
-
const
|
|
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.
|
|
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;
|