@timurproko/a1 0.2.7-dev.806 → 0.2.7-dev.809
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/session-shell/dialog-coordinator.d.ts +42 -0
- package/dist/app/session-shell/dialog-coordinator.js +133 -0
- package/dist/app/session-shell/dock-chrome.d.ts +34 -0
- package/dist/app/session-shell/dock-chrome.js +109 -0
- package/dist/app/session-shell/exit-notice.d.ts +1 -1
- package/dist/app/session-shell/exit-notice.js +1 -1
- package/dist/app/session-shell/extension-surface.d.ts +30 -0
- package/dist/app/session-shell/extension-surface.js +105 -0
- package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
- package/dist/app/session-shell/input-surface-switch.js +85 -0
- package/dist/app/session-shell/login-interaction.d.ts +27 -0
- package/dist/app/session-shell/login-interaction.js +91 -0
- package/dist/app/session-shell/prompt-submission.d.ts +48 -0
- package/dist/app/session-shell/prompt-submission.js +276 -0
- package/dist/app/session-shell/resident-tab-coordinator.d.ts +41 -0
- package/dist/app/session-shell/resident-tab-coordinator.js +120 -0
- package/dist/app/session-shell/session-concern.d.ts +22 -0
- package/dist/app/session-shell/session-concern.js +29 -0
- package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
- package/dist/app/session-shell/session-lifecycle.js +49 -0
- package/dist/app/session-shell/session-shell-root.d.ts +1 -1
- package/dist/app/session-shell/session-shell-root.js +182 -931
- package/dist/app/session-shell/session-shell.d.ts +2 -8
- package/dist/app/session-shell/session-shell.js +209 -897
- package/dist/app/session-shell/settings-bindings.d.ts +17 -0
- package/dist/app/session-shell/settings-bindings.js +91 -0
- package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
- package/dist/app/session-shell/shell-editor-factory.js +102 -0
- package/dist/app/session-shell/shell-host.d.ts +6 -0
- package/dist/app/session-shell/shell-host.js +26 -4
- package/dist/app/session-shell/shell-layout.d.ts +64 -0
- package/dist/app/session-shell/shell-layout.js +18 -0
- package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
- package/dist/app/session-shell/suggestion-surface.js +63 -0
- package/dist/app/session-shell/transcript-document.d.ts +78 -0
- package/dist/app/session-shell/transcript-document.js +365 -0
- package/dist/app/session-shell/viewport-layout.d.ts +31 -0
- package/dist/app/session-shell/viewport-layout.js +160 -0
- package/dist/app/session-shell/workflow-output.d.ts +33 -0
- package/dist/app/session-shell/workflow-output.js +174 -0
- package/dist/app/session-shell/workflow-router.d.ts +25 -0
- package/dist/app/session-shell/workflow-router.js +39 -0
- package/dist/composition/owned-ui.d.ts +6 -25
- package/dist/composition/owned-ui.js +48 -242
- package/dist/composition/resident-tab-bridge.d.ts +16 -0
- package/dist/composition/resident-tab-bridge.js +159 -0
- package/dist/composition/session-backend.d.ts +19 -0
- package/dist/composition/session-backend.js +1 -0
- package/dist/composition/session-runtime-registry.d.ts +20 -0
- package/dist/composition/session-runtime-registry.js +85 -0
- package/dist/composition/settings-route-host.d.ts +6 -1
- package/dist/composition/settings-route-host.js +7 -6
- package/dist/composition/startup-route.d.ts +32 -0
- package/dist/composition/startup-route.js +72 -0
- package/dist/features/owned-ui/settings-app.d.ts +4 -1
- package/dist/features/owned-ui/settings-app.js +9 -3
- package/dist/features/resident-tab/shortcut-declarations.js +1 -1
- package/dist/features/resident-tab/status-deriver.d.ts +4 -3
- package/dist/features/resident-tab/status-deriver.js +2 -5
- package/dist/foundation/release/bootstrap.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
- package/dist/foundation/resident-tabs/bridge-client.js +2 -1
- package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
- package/dist/foundation/resident-tabs/fixture-control.js +35 -0
- package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
- package/dist/foundation/resident-tabs/progress-observation.js +119 -0
- package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
- package/dist/foundation/terminal-cleanup/index.js +1 -1
- package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
- package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +4 -77
- package/dist/integrations/pi/engine/adapter.js +85 -393
- package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
- package/dist/integrations/pi/engine/command-dispatch.js +57 -0
- package/dist/integrations/pi/engine/host.js +2 -2
- package/dist/integrations/pi/engine/ports.d.ts +56 -0
- package/dist/integrations/pi/engine/ports.js +89 -0
- package/dist/integrations/pi/engine/session-runtime.js +5 -2
- package/dist/integrations/pi/engine/view-state.d.ts +42 -0
- package/dist/integrations/pi/engine/view-state.js +126 -0
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/product-identity.d.ts +2 -2
- package/dist/product-identity.js +2 -2
- package/dist/product-identity.json +6 -0
- package/dist/ui/settings/manager.d.ts +6 -0
- package/dist/ui/settings/manager.js +25 -0
- package/docs/architecture/boundaries.md +14 -0
- package/docs/architecture/resident-tabs.md +23 -18
- package/package.json +2 -1
|
@@ -2,18 +2,25 @@ import { ResponseCopyCoordinator } from "./response-copy-coordinator.js";
|
|
|
2
2
|
import { createResponseCopyExecutor, responseCopyDestination } from "./response-copy-transport.js";
|
|
3
3
|
import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
|
|
4
4
|
import { PromptHistoryController } from "./prompt-history-controller.js";
|
|
5
|
-
import {
|
|
5
|
+
import { SessionDialogCoordinator } from "./dialog-coordinator.js";
|
|
6
|
+
import { LoginInteraction } from "./login-interaction.js";
|
|
7
|
+
import { PromptSubmission } from "./prompt-submission.js";
|
|
8
|
+
import { ResidentTabCoordinator } from "./resident-tab-coordinator.js";
|
|
9
|
+
import { SessionConcerns } from "./session-concern.js";
|
|
10
|
+
import { composeExitText, SessionStopLatch } from "./session-lifecycle.js";
|
|
11
|
+
import { openWorkflowSelector } from "./workflow-router.js";
|
|
12
|
+
import { bindSessionSettings } from "./settings-bindings.js";
|
|
13
|
+
import { promptSuggestionSurface, routeResponseToSuggestions, suggestionModelKey, suggestionPresentationBlockReason, } from "./suggestion-surface.js";
|
|
6
14
|
import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
15
|
+
import { OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
|
|
16
|
+
import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
|
|
9
17
|
import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
|
|
10
18
|
import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
|
|
11
19
|
import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
|
|
12
20
|
import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
|
|
13
21
|
import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
|
|
14
|
-
import { createPiShellAuthProviderSelector,
|
|
15
|
-
import {
|
|
16
|
-
import { OwnedUiTerminalHost, sessionTerminalHostOptions, } from "./shell-host.js";
|
|
22
|
+
import { createPiShellAuthProviderSelector, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
23
|
+
import { OwnedUiTerminalHost, componentRuntime, sessionTerminalHostOptions, } from "./shell-host.js";
|
|
17
24
|
import { runImageWorker } from "./image-preparation-client.js";
|
|
18
25
|
import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
|
|
19
26
|
import { writeSystemClipboardText, } from "./system-clipboard.js";
|
|
@@ -22,6 +29,7 @@ import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-
|
|
|
22
29
|
const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
|
|
23
30
|
export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
|
|
24
31
|
export { OwnedUiTerminalHost, sessionTerminalHostOptions } from "./shell-host.js";
|
|
32
|
+
export { formatSessionResumeCommand, quoteCommandArgument } from "./session-lifecycle.js";
|
|
25
33
|
/**
|
|
26
34
|
* Presents one engine session: its transcript root, editor, controllers, submission, workflows, and dialogs.
|
|
27
35
|
* It reaches the terminal only through its terminal host's handle, and paints only while it is attached.
|
|
@@ -33,8 +41,10 @@ export class OwnedUiSessionPresenter {
|
|
|
33
41
|
root;
|
|
34
42
|
#host;
|
|
35
43
|
#terminal;
|
|
36
|
-
#
|
|
37
|
-
#
|
|
44
|
+
#resident;
|
|
45
|
+
#submission;
|
|
46
|
+
#login;
|
|
47
|
+
#concerns;
|
|
38
48
|
#listeners = new Set();
|
|
39
49
|
#unsubscribe;
|
|
40
50
|
#unsubscribePromptSuggestions;
|
|
@@ -47,19 +57,9 @@ export class OwnedUiSessionPresenter {
|
|
|
47
57
|
#promptHistory = null;
|
|
48
58
|
#promptHistoryImageSidecar;
|
|
49
59
|
#extensionBridge;
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#dialogId;
|
|
53
|
-
#dialogSource;
|
|
54
|
-
#routeHost;
|
|
55
|
-
#startupRoute;
|
|
56
|
-
#startupRouteTimer;
|
|
57
|
-
#residentProgressTimer;
|
|
58
|
-
#residentWorking = false;
|
|
59
|
-
#residentStalled = false;
|
|
60
|
-
#dialogHandle;
|
|
60
|
+
#stop = new SessionStopLatch();
|
|
61
|
+
#dialogs;
|
|
61
62
|
#sequence = 0;
|
|
62
|
-
#editorRevision = 0;
|
|
63
63
|
#started = false;
|
|
64
64
|
#disposed = false;
|
|
65
65
|
#released;
|
|
@@ -81,18 +81,12 @@ export class OwnedUiSessionPresenter {
|
|
|
81
81
|
#streamPresentation;
|
|
82
82
|
#inputCoordination;
|
|
83
83
|
#unsubscribeSettings;
|
|
84
|
-
#
|
|
85
|
-
#unbindTerminalSettings;
|
|
86
|
-
#unbindShutdownSettings;
|
|
84
|
+
#unbindSessionSettings;
|
|
87
85
|
#terminalProgressEnabled = false;
|
|
88
86
|
#programStatus;
|
|
89
|
-
#showImages = true;
|
|
90
|
-
#imageWidthCells = 80;
|
|
91
87
|
#fullscreenExitOutput = "transcript";
|
|
92
|
-
#waitingImages = new Map();
|
|
93
88
|
#lastClearTime = 0;
|
|
94
89
|
#lastEscapeTime = 0;
|
|
95
|
-
#activeLoginDialog;
|
|
96
90
|
#sessionGeneration;
|
|
97
91
|
#sessionBindingGeneration;
|
|
98
92
|
#suggestionModelKey;
|
|
@@ -106,11 +100,7 @@ export class OwnedUiSessionPresenter {
|
|
|
106
100
|
this.#presenters = options.presenters;
|
|
107
101
|
this.#sessionGeneration = this.backend.identity.sessionGeneration;
|
|
108
102
|
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
109
|
-
this.#suggestionModelKey =
|
|
110
|
-
this.#cwd = cwd;
|
|
111
|
-
this.#residentTabOptions = options.engine.residentTab;
|
|
112
|
-
this.#routeHost = routeHost ?? null;
|
|
113
|
-
this.#startupRoute = options.engine.startupRoute;
|
|
103
|
+
this.#suggestionModelKey = suggestionModelKey(this.backend.session.view());
|
|
114
104
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
115
105
|
this.#inputCoordination = inputPresentation?.coordination !== false;
|
|
116
106
|
this.#host = host;
|
|
@@ -118,9 +108,6 @@ export class OwnedUiSessionPresenter {
|
|
|
118
108
|
// Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
|
|
119
109
|
this.#dialogHost = this.#createDialogHost();
|
|
120
110
|
this.#lazySelectors = options.diagnostics?.lazySelectors;
|
|
121
|
-
this.#stopped = new Promise(resolve => {
|
|
122
|
-
this.#resolveStopped = resolve;
|
|
123
|
-
});
|
|
124
111
|
let promptSuggestionController = null;
|
|
125
112
|
// Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
|
|
126
113
|
this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
|
|
@@ -181,12 +168,12 @@ export class OwnedUiSessionPresenter {
|
|
|
181
168
|
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
182
169
|
}),
|
|
183
170
|
enableDockInputReuse: inputPresentation?.viewportReuse !== false,
|
|
184
|
-
footerSessionName:
|
|
185
|
-
hideSettingsCommand:
|
|
171
|
+
footerSessionName: options.engine.residentTab === undefined,
|
|
172
|
+
hideSettingsCommand: options.engine.residentTab !== undefined,
|
|
186
173
|
persistentHistory: this.#customViewport && promptHistory !== undefined,
|
|
187
174
|
...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
|
|
188
|
-
onSubmit: text => { void this.submit(text).catch(() => this.#
|
|
189
|
-
onPasteRejected: error => this.#
|
|
175
|
+
onSubmit: text => { void this.submit(text).catch(() => this.#submission.reportError()); },
|
|
176
|
+
onPasteRejected: error => this.#submission.reportError(error),
|
|
190
177
|
onInterrupt: () => { void this.interrupt(); },
|
|
191
178
|
onClear: () => { void this.clearOrExit(); },
|
|
192
179
|
onExit: () => { void this.shutdown(); },
|
|
@@ -198,11 +185,11 @@ export class OwnedUiSessionPresenter {
|
|
|
198
185
|
this.#terminal.requestRender();
|
|
199
186
|
},
|
|
200
187
|
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
201
|
-
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#
|
|
188
|
+
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#submission.reportError()); },
|
|
202
189
|
onDequeue: () => this.restoreQueuedInput(),
|
|
203
190
|
onEditorChange: text => {
|
|
204
|
-
this.#
|
|
205
|
-
this.#
|
|
191
|
+
this.#submission.noteEditorChange();
|
|
192
|
+
this.#resident.checkpoint(text);
|
|
206
193
|
promptSuggestionController?.abortPending();
|
|
207
194
|
if (text.length === 0) {
|
|
208
195
|
// Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
|
|
@@ -264,9 +251,27 @@ export class OwnedUiSessionPresenter {
|
|
|
264
251
|
// Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
|
|
265
252
|
// terminal no longer renders it.
|
|
266
253
|
constructed.push(() => this.root.dispose());
|
|
267
|
-
this.#
|
|
254
|
+
this.#resident = new ResidentTabCoordinator(options.engine.residentTab, { root: this.root, terminal: this.#terminal, disposed: () => this.#disposed });
|
|
255
|
+
constructed.push(() => this.#resident.dispose());
|
|
256
|
+
const startupRoute = options.engine.startupRoute;
|
|
257
|
+
this.#dialogs = new SessionDialogCoordinator({
|
|
258
|
+
terminal: this.#terminal,
|
|
259
|
+
root: this.root,
|
|
260
|
+
routeHost: routeHost ?? null,
|
|
261
|
+
disposed: () => this.#disposed,
|
|
262
|
+
exit: () => { void this.shutdown(); },
|
|
263
|
+
routeClosed: route => this.#resident.closeOwnedRoute(route),
|
|
264
|
+
...(startupRoute === undefined ? {} : { startupRoute }),
|
|
265
|
+
});
|
|
266
|
+
constructed.push(() => this.#dialogs.dispose());
|
|
268
267
|
const initialPiSettings = this.backend.settings.snapshot();
|
|
269
268
|
this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
|
|
269
|
+
this.#login = new LoginInteraction({
|
|
270
|
+
terminal: this.#terminal, root: this.root, programStatus: this.#programStatus,
|
|
271
|
+
syncProgress: () => this.#syncTerminalProgress(this.view()),
|
|
272
|
+
});
|
|
273
|
+
this.#submission = this.#createSubmission(cwd);
|
|
274
|
+
this.#resident.restoreInput(options.history?.imageSidecar);
|
|
270
275
|
this.#reloadPresentation = reloadPresentation;
|
|
271
276
|
const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
|
|
272
277
|
this.#streamPresentation = streamPresentationOptions?.scheduler === undefined
|
|
@@ -278,18 +283,7 @@ export class OwnedUiSessionPresenter {
|
|
|
278
283
|
generator: promptSuggestionOptions.generator,
|
|
279
284
|
enabled: promptSuggestionOptions.enabled(),
|
|
280
285
|
...(promptSuggestionOptions.diagnostics === undefined ? {} : { diagnostics: promptSuggestionOptions.diagnostics }),
|
|
281
|
-
surface:
|
|
282
|
-
canPresent: identity => this.#promptSuggestionPresentationBlockReason(identity) === null,
|
|
283
|
-
presentationBlockReason: identity => this.#promptSuggestionPresentationBlockReason(identity),
|
|
284
|
-
present: text => {
|
|
285
|
-
if (!this.root.canPresentPromptSuggestion())
|
|
286
|
-
return false;
|
|
287
|
-
this.root.setPromptSuggestion(text);
|
|
288
|
-
return true;
|
|
289
|
-
},
|
|
290
|
-
clear: () => this.root.setPromptSuggestion(null),
|
|
291
|
-
requestRender: () => this.#terminal.requestRender(),
|
|
292
|
-
},
|
|
286
|
+
surface: promptSuggestionSurface(this.root, identity => this.#promptSuggestionPresentationBlockReason(identity), () => this.#terminal.requestRender()),
|
|
293
287
|
});
|
|
294
288
|
this.#promptSuggestions = promptSuggestionController;
|
|
295
289
|
constructed.push(() => this.#promptSuggestions?.dispose());
|
|
@@ -312,35 +306,7 @@ export class OwnedUiSessionPresenter {
|
|
|
312
306
|
this.#terminal.requestRender();
|
|
313
307
|
});
|
|
314
308
|
constructed.push(this.#unsubscribeSkills);
|
|
315
|
-
this.#terminal.setHardwareCursor(initialPiSettings.showHardwareCursor);
|
|
316
|
-
this.#terminal.setClearOnShrink(initialPiSettings.clearOnShrink);
|
|
317
|
-
this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
|
|
318
309
|
this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
|
|
319
|
-
// Invariant: bare A1 prints only the resume hint at exit, so the pinned exit-output
|
|
320
|
-
// choice is hidden there and cannot be bound; the comparison profile binds and honors it.
|
|
321
|
-
this.#unbindShutdownSettings = this.backend.settings.productMode === "bare" ? () => { } : this.backend.settings.bindOwner("shutdown", {
|
|
322
|
-
fullscreenExitOutput: { apply() { } },
|
|
323
|
-
});
|
|
324
|
-
constructed.push(this.#unbindShutdownSettings);
|
|
325
|
-
this.#unbindTerminalSettings = this.backend.settings.bindOwner("terminal", {
|
|
326
|
-
showHardwareCursor: { apply: value => {
|
|
327
|
-
if (typeof value !== "boolean")
|
|
328
|
-
throw new TypeError("Hardware cursor setting is invalid");
|
|
329
|
-
this.#terminal.setHardwareCursor(value);
|
|
330
|
-
} },
|
|
331
|
-
clearOnShrink: { apply: value => {
|
|
332
|
-
if (typeof value !== "boolean")
|
|
333
|
-
throw new TypeError("Clear-on-shrink setting is invalid");
|
|
334
|
-
this.#terminal.setClearOnShrink(value);
|
|
335
|
-
} },
|
|
336
|
-
showTerminalProgress: { apply: value => {
|
|
337
|
-
if (typeof value !== "boolean")
|
|
338
|
-
throw new TypeError("Terminal progress setting is invalid");
|
|
339
|
-
this.#terminalProgressEnabled = value;
|
|
340
|
-
this.#syncTerminalProgress(this.view());
|
|
341
|
-
} },
|
|
342
|
-
});
|
|
343
|
-
constructed.push(this.#unbindTerminalSettings);
|
|
344
310
|
const applyViewportSettings = () => {
|
|
345
311
|
const snapshot = viewportSettings?.snapshot();
|
|
346
312
|
this.root.setViewportConfig(snapshot ?? {
|
|
@@ -354,67 +320,16 @@ export class OwnedUiSessionPresenter {
|
|
|
354
320
|
? viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
|
|
355
321
|
: () => { };
|
|
356
322
|
constructed.push(this.#unsubscribeSettings);
|
|
357
|
-
this.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
this.#
|
|
367
|
-
editorPaddingX: { apply: value => {
|
|
368
|
-
if (typeof value !== "number")
|
|
369
|
-
throw new TypeError("Editor padding is invalid");
|
|
370
|
-
this.root.setEditorPaddingX(value);
|
|
371
|
-
} },
|
|
372
|
-
autocompleteMaxVisible: { apply: value => {
|
|
373
|
-
if (typeof value !== "number")
|
|
374
|
-
throw new TypeError("Autocomplete maximum is invalid");
|
|
375
|
-
this.root.setAutocompleteMaxVisible(value);
|
|
376
|
-
} },
|
|
377
|
-
outputPad: { apply: value => {
|
|
378
|
-
if (value !== 0 && value !== 1)
|
|
379
|
-
throw new TypeError("Output padding is invalid");
|
|
380
|
-
this.root.setOutputPad(value);
|
|
381
|
-
} },
|
|
382
|
-
hideThinkingBlock: { apply: value => {
|
|
383
|
-
if (typeof value !== "boolean")
|
|
384
|
-
throw new TypeError("Thinking-block visibility is invalid");
|
|
385
|
-
this.root.setHideThinkingBlock(value);
|
|
386
|
-
} },
|
|
387
|
-
mermaidRenderingMode: { apply: value => {
|
|
388
|
-
if (value !== "off" && value !== "final" && value !== "streaming")
|
|
389
|
-
throw new TypeError("Mermaid mode is invalid");
|
|
390
|
-
this.root.setMermaidRenderingMode(value);
|
|
391
|
-
} },
|
|
392
|
-
showImages: { apply: value => {
|
|
393
|
-
if (typeof value !== "boolean")
|
|
394
|
-
throw new TypeError("Image visibility is invalid");
|
|
395
|
-
this.#showImages = value;
|
|
396
|
-
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
397
|
-
} },
|
|
398
|
-
imageWidthCells: { apply: value => {
|
|
399
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
400
|
-
throw new TypeError("Image width is invalid");
|
|
401
|
-
this.#imageWidthCells = value;
|
|
402
|
-
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
403
|
-
} },
|
|
404
|
-
...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
|
|
405
|
-
if (typeof value !== "boolean")
|
|
406
|
-
throw new TypeError("Fullscreen copy-on-select setting is invalid");
|
|
407
|
-
this.root.setFullscreenCopyOnSelect(value);
|
|
408
|
-
} } } : {
|
|
409
|
-
fullscreenWheelScrollLines: { apply: (value) => {
|
|
410
|
-
if (value !== "auto" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100)) {
|
|
411
|
-
throw new TypeError("Fullscreen wheel-scroll distance is invalid");
|
|
412
|
-
}
|
|
413
|
-
this.#terminal.setWheelScrollLines(value);
|
|
414
|
-
} },
|
|
415
|
-
}),
|
|
416
|
-
});
|
|
417
|
-
constructed.push(this.#unbindPiSettings);
|
|
323
|
+
this.#unbindSessionSettings = bindSessionSettings(this.backend.settings, initialPiSettings, {
|
|
324
|
+
terminal: this.#terminal,
|
|
325
|
+
root: this.root,
|
|
326
|
+
setTerminalProgress: enabled => {
|
|
327
|
+
this.#terminalProgressEnabled = enabled;
|
|
328
|
+
if (this.#terminal.running)
|
|
329
|
+
this.#syncTerminalProgress(this.view());
|
|
330
|
+
},
|
|
331
|
+
}, { customViewport: this.#customViewport });
|
|
332
|
+
constructed.push(this.#unbindSessionSettings);
|
|
418
333
|
if (this.#customViewport && promptHistory !== undefined) {
|
|
419
334
|
this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
|
|
420
335
|
const sidecar = this.#promptHistoryImageSidecar;
|
|
@@ -429,54 +344,29 @@ export class OwnedUiSessionPresenter {
|
|
|
429
344
|
});
|
|
430
345
|
constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
|
|
431
346
|
}
|
|
432
|
-
this.#extensionBridge =
|
|
433
|
-
runtime: {
|
|
434
|
-
getColumns: () => this.#terminal.viewport().columns,
|
|
435
|
-
getRows: () => this.#terminal.viewport().rows,
|
|
436
|
-
requestRender: () => this.#terminal.requestRender(),
|
|
437
|
-
},
|
|
438
|
-
agentDir: this.backend.identity.agentDir,
|
|
439
|
-
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
440
|
-
setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
|
|
441
|
-
showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
|
|
442
|
-
listenInput: handler => this.#terminal.addInputListener(handler),
|
|
443
|
-
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
444
|
-
replaceHeader: component => this.root.setExtensionHeader(component),
|
|
445
|
-
replaceFooter: component => this.root.setExtensionFooter(component),
|
|
446
|
-
setStatus: (key, text) => this.root.setExtensionStatus(key, text),
|
|
447
|
-
setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
|
|
448
|
-
notify: (message, type) => this.root.addExtensionNotification(message, type),
|
|
449
|
-
setTitle: title => this.#terminal.setTitle(title),
|
|
450
|
-
getEditorText: () => this.root.editor.getText(),
|
|
451
|
-
setEditorText: text => this.root.editor.setText(text),
|
|
452
|
-
pasteToEditor: text => this.root.editor.insertText(text),
|
|
453
|
-
addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
|
|
454
|
-
setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
|
|
455
|
-
getFooterData: () => this.root.extensionFooterData(),
|
|
456
|
-
getToolsExpanded: () => this.root.toolsExpanded,
|
|
457
|
-
setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
|
|
458
|
-
});
|
|
347
|
+
this.#extensionBridge = this.#createExtensionBridge();
|
|
459
348
|
constructed.push(() => this.#extensionBridge.dispose());
|
|
349
|
+
this.#concerns = this.#createConcerns();
|
|
460
350
|
constructed.push(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
|
|
461
351
|
this.backend.workflows.setWorkflowInteractionHost({
|
|
462
|
-
startLogin: request => this.#
|
|
463
|
-
prompt: request => this.#
|
|
464
|
-
notify: event => this.#
|
|
352
|
+
startLogin: request => this.#login.start(request),
|
|
353
|
+
prompt: request => this.#login.requestInput(request),
|
|
354
|
+
notify: event => this.#login.notify(event),
|
|
465
355
|
publish: message => {
|
|
466
356
|
this.root.appendWorkflowMessage(message);
|
|
467
357
|
this.#terminal.requestRender();
|
|
468
358
|
},
|
|
469
|
-
finishLogin: () => this.#
|
|
359
|
+
finishLogin: () => this.#login.finish(),
|
|
470
360
|
});
|
|
471
361
|
this.#installAutocompleteCommands();
|
|
472
362
|
this.#unsubscribe = this.backend.session.onEvent(event => {
|
|
473
363
|
// Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
|
|
474
364
|
if (this.#disposed) {
|
|
475
365
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
476
|
-
this.#
|
|
366
|
+
this.#stop.settleAfter(() => this.dispose());
|
|
477
367
|
return;
|
|
478
368
|
}
|
|
479
|
-
this.#
|
|
369
|
+
this.#resident.observeProgress(event);
|
|
480
370
|
// Performance: a streamed chunk names one block, and touching only that block is what keeps the
|
|
481
371
|
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
482
372
|
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
@@ -492,28 +382,14 @@ export class OwnedUiSessionPresenter {
|
|
|
492
382
|
}
|
|
493
383
|
if (event.type === "assistant-message-completed") {
|
|
494
384
|
this.root.noteCompletedAssistantMessage();
|
|
495
|
-
|
|
496
|
-
sessionId: event.sessionId,
|
|
497
|
-
sessionGeneration: event.sessionGeneration,
|
|
498
|
-
runSequence: event.runSequence,
|
|
499
|
-
responseSequence: event.responseSequence,
|
|
500
|
-
model: event.model,
|
|
501
|
-
};
|
|
502
|
-
if (event.model === null)
|
|
503
|
-
this.#promptSuggestions?.skip(identity, "no-model");
|
|
504
|
-
else
|
|
505
|
-
this.#promptSuggestions?.consider({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
506
|
-
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
507
|
-
: event.stopReason !== "stop" ? "incomplete-response"
|
|
508
|
-
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
509
|
-
: this.root.promptSuggestionPrepareBlockReason());
|
|
385
|
+
routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
|
|
510
386
|
}
|
|
511
387
|
const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
|
|
512
388
|
const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
|
|
513
389
|
? this.#syncBlock(event.block)
|
|
514
390
|
: semanticOnly ? this.view() : this.#syncView();
|
|
515
391
|
this.#syncTerminalProgress(view);
|
|
516
|
-
const currentModelKey =
|
|
392
|
+
const currentModelKey = suggestionModelKey(view);
|
|
517
393
|
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
|
|
518
394
|
this.#promptSuggestions?.invalidate();
|
|
519
395
|
this.#suggestionModelKey = currentModelKey;
|
|
@@ -522,24 +398,10 @@ export class OwnedUiSessionPresenter {
|
|
|
522
398
|
this.#promptSuggestions?.invalidate();
|
|
523
399
|
}
|
|
524
400
|
if (event.type === "agent-run-settled") {
|
|
525
|
-
|
|
526
|
-
sessionId: event.sessionId,
|
|
527
|
-
sessionGeneration: event.sessionGeneration,
|
|
528
|
-
runSequence: event.runSequence,
|
|
529
|
-
responseSequence: event.responseSequence,
|
|
530
|
-
model: event.model,
|
|
531
|
-
};
|
|
532
|
-
if (event.model === null)
|
|
533
|
-
this.#promptSuggestions?.skip(identity, "no-model");
|
|
534
|
-
else
|
|
535
|
-
this.#promptSuggestions?.settle({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
536
|
-
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
537
|
-
: event.stopReason !== "stop" ? "incomplete-response"
|
|
538
|
-
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
539
|
-
: this.root.promptSuggestionPrepareBlockReason());
|
|
401
|
+
routeResponseToSuggestions(this.#promptSuggestions, event, () => this.root.promptSuggestionPrepareBlockReason());
|
|
540
402
|
}
|
|
541
403
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
542
|
-
this.#
|
|
404
|
+
this.#stop.settleAfter(() => this.dispose());
|
|
543
405
|
});
|
|
544
406
|
constructed.push(this.#unsubscribe);
|
|
545
407
|
this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.extensions.bindClipboardWriter(async (text) => {
|
|
@@ -552,7 +414,7 @@ export class OwnedUiSessionPresenter {
|
|
|
552
414
|
});
|
|
553
415
|
constructed.push(this.#unbindClipboardWriter);
|
|
554
416
|
if (this.backend.session.view().lifecycle === "stopped")
|
|
555
|
-
this.#
|
|
417
|
+
this.#stop.resolve();
|
|
556
418
|
}
|
|
557
419
|
catch (error) {
|
|
558
420
|
for (const release of constructed.reverse()) {
|
|
@@ -645,13 +507,7 @@ export class OwnedUiSessionPresenter {
|
|
|
645
507
|
});
|
|
646
508
|
}
|
|
647
509
|
#promptSuggestionPresentationBlockReason(identity) {
|
|
648
|
-
|
|
649
|
-
if (this.#disposed)
|
|
650
|
-
return "disposed";
|
|
651
|
-
if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.identity.sessionGeneration
|
|
652
|
-
|| modelKey(view) !== `${identity.model.providerId}/${identity.model.modelId}`)
|
|
653
|
-
return "stale-identity";
|
|
654
|
-
return this.root.promptSuggestionPresentationBlockReason();
|
|
510
|
+
return suggestionPresentationBlockReason(identity, { disposed: this.#disposed, view: this.view(), sessionGeneration: this.backend.identity.sessionGeneration }, () => this.root.promptSuggestionPresentationBlockReason());
|
|
655
511
|
}
|
|
656
512
|
start() {
|
|
657
513
|
if (this.#started)
|
|
@@ -666,7 +522,7 @@ export class OwnedUiSessionPresenter {
|
|
|
666
522
|
setImmediate(() => {
|
|
667
523
|
if (this.#disposed || !this.#customViewport)
|
|
668
524
|
return;
|
|
669
|
-
this.#presentStartupRoute();
|
|
525
|
+
this.#dialogs.presentStartupRoute();
|
|
670
526
|
void this.#loadLazySelectors().then(selectors => selectors.prepare()).catch(() => { });
|
|
671
527
|
this.root.warmPastePreparation();
|
|
672
528
|
this.#copyExecutor?.warm();
|
|
@@ -678,145 +534,10 @@ export class OwnedUiSessionPresenter {
|
|
|
678
534
|
return () => this.#listeners.delete(listener);
|
|
679
535
|
}
|
|
680
536
|
waitUntilStopped() {
|
|
681
|
-
return this.#stopped;
|
|
537
|
+
return this.#stop.stopped;
|
|
682
538
|
}
|
|
683
539
|
async submit(text) {
|
|
684
|
-
return this.#
|
|
685
|
-
}
|
|
686
|
-
#submitWhenReady(draft, action) {
|
|
687
|
-
const previous = this.#waitingImages.get(draft);
|
|
688
|
-
if (previous !== undefined)
|
|
689
|
-
return previous.result;
|
|
690
|
-
if (!this.root.hasPendingPastes(draft))
|
|
691
|
-
return this.#guardSubmission(draft, action);
|
|
692
|
-
const controller = new AbortController();
|
|
693
|
-
const generation = this.backend.identity.sessionGeneration;
|
|
694
|
-
const result = this.#guardSubmission(draft, async () => {
|
|
695
|
-
await this.root.waitForPromptPastes(draft, controller.signal);
|
|
696
|
-
if (controller.signal.aborted || this.#disposed || this.backend.identity.sessionGeneration !== generation)
|
|
697
|
-
return rejected("image submission canceled");
|
|
698
|
-
return action();
|
|
699
|
-
}).finally(() => {
|
|
700
|
-
this.#waitingImages.delete(draft);
|
|
701
|
-
if (!this.#disposed)
|
|
702
|
-
this.#showWaitingImages();
|
|
703
|
-
});
|
|
704
|
-
this.#waitingImages.set(draft, { controller, result });
|
|
705
|
-
if (this.root.editor.getText() === draft)
|
|
706
|
-
this.root.editor.setText("");
|
|
707
|
-
this.#showWaitingImages();
|
|
708
|
-
return result;
|
|
709
|
-
}
|
|
710
|
-
#showWaitingImages() {
|
|
711
|
-
const count = this.#waitingImages.size;
|
|
712
|
-
this.root.setExtensionWidget("owned-image-preparation", count === 0 ? null : {
|
|
713
|
-
render: width => [...renderPiShellStatusText(`Waiting for images (${count} submission${count === 1 ? "" : "s"}) — Esc cancels; dequeue restores`, width)],
|
|
714
|
-
invalidate: () => { },
|
|
715
|
-
}, "aboveEditor");
|
|
716
|
-
this.#terminal.requestRender();
|
|
717
|
-
}
|
|
718
|
-
#cancelWaitingImages() {
|
|
719
|
-
for (const item of this.#waitingImages.values())
|
|
720
|
-
item.controller.abort();
|
|
721
|
-
}
|
|
722
|
-
#rememberInput(text, kind) {
|
|
723
|
-
this.root.editor.addToHistory(text);
|
|
724
|
-
if (this.#promptHistory !== null) {
|
|
725
|
-
const reusable = this.root.prepareHistoryText(text);
|
|
726
|
-
if (reusable.length === 0)
|
|
727
|
-
this.#promptHistory.rememberRecovery(text);
|
|
728
|
-
else {
|
|
729
|
-
this.#persistImageSidecars(text);
|
|
730
|
-
this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.identity.sessionId);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
// Rationale: write the image chip payloads referenced by an editor draft to the sidecar
|
|
735
|
-
// directory before their history row commits. Missing sidecar dependency = silent skip; the
|
|
736
|
-
// recall path treats an absent sidecar the same as a corrupt one.
|
|
737
|
-
#persistImageSidecars(text) {
|
|
738
|
-
const sidecar = this.#promptHistoryImageSidecar;
|
|
739
|
-
if (sidecar === undefined)
|
|
740
|
-
return;
|
|
741
|
-
for (const record of this.root.imageChipAttachmentsFromEditor(text)) {
|
|
742
|
-
sidecar.write(record.id, { tag: record.tag, data: record.image.data, mimeType: record.image.mimeType, savedAt: new Date().toISOString() });
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
async #submit(text) {
|
|
746
|
-
this.#promptSuggestions?.invalidate();
|
|
747
|
-
const displayInput = text.trim();
|
|
748
|
-
if (!displayInput)
|
|
749
|
-
return { outcome: "completed", diagnostic: null };
|
|
750
|
-
if (displayInput.startsWith("/"))
|
|
751
|
-
return this.#slashCommand(displayInput);
|
|
752
|
-
const prepared = this.root.preparePromptSubmission(displayInput);
|
|
753
|
-
assertPromptImages(prepared.images);
|
|
754
|
-
const input = prepared.text.trim();
|
|
755
|
-
if (input.startsWith("!")) {
|
|
756
|
-
const excludeFromContext = input.startsWith("!!");
|
|
757
|
-
const command = input.slice(excludeFromContext ? 2 : 1).trim();
|
|
758
|
-
if (command) {
|
|
759
|
-
this.#rememberInput(displayInput, "bash");
|
|
760
|
-
try {
|
|
761
|
-
const result = await this.backend.workflows.executeBashWorkflow(command, excludeFromContext);
|
|
762
|
-
const workflow = {
|
|
763
|
-
command: "debug",
|
|
764
|
-
outcome: result.cancelled ? "cancelled" : result.exitCode === 0 || result.exitCode === undefined ? "completed" : "failed",
|
|
765
|
-
message: result.cancelled ? "Bash command cancelled" : `Bash exited ${result.exitCode ?? 0}: ${command}`,
|
|
766
|
-
...(result.output ? { detail: result.output } : {}),
|
|
767
|
-
};
|
|
768
|
-
this.root.appendWorkflowResult(workflow);
|
|
769
|
-
this.#terminal.requestRender();
|
|
770
|
-
return workflow.outcome === "failed" ? rejected(workflow.message) : { outcome: "completed", diagnostic: null };
|
|
771
|
-
}
|
|
772
|
-
catch (error) {
|
|
773
|
-
const message = `Bash command failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
774
|
-
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message });
|
|
775
|
-
this.#terminal.requestRender();
|
|
776
|
-
return rejected(message);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
// Compatibility: match interactive Pi: input during compaction is queued steering; the engine
|
|
781
|
-
// shows it in the pending rows and delivers it when compaction ends.
|
|
782
|
-
const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
|
|
783
|
-
this.#rememberInput(this.root.omitUnsentPromptImages(displayInput).trim(), type);
|
|
784
|
-
this.root.resumeViewportFollowing();
|
|
785
|
-
const command = {
|
|
786
|
-
type,
|
|
787
|
-
correlationId: this.#correlation(type),
|
|
788
|
-
sessionId: this.backend.identity.sessionId,
|
|
789
|
-
text: input,
|
|
790
|
-
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
791
|
-
};
|
|
792
|
-
let submission;
|
|
793
|
-
try {
|
|
794
|
-
submission = await this.#residentTabOptions?.admit?.({
|
|
795
|
-
sessionId: command.sessionId,
|
|
796
|
-
sessionFile: this.#currentSessionFile(),
|
|
797
|
-
kind: type,
|
|
798
|
-
text: input,
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
if (this.root.editor.getText().length === 0)
|
|
803
|
-
this.root.editor.setText(displayInput);
|
|
804
|
-
const message = "Prompt was not sent because its recovery journal could not be synchronized.";
|
|
805
|
-
this.#reportSubmissionError(undefined, message);
|
|
806
|
-
return rejected(message);
|
|
807
|
-
}
|
|
808
|
-
const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
|
|
809
|
-
try {
|
|
810
|
-
const result = await this.#execute(command, displayInput);
|
|
811
|
-
if (submission !== undefined && result.outcome !== "rejected") {
|
|
812
|
-
this.#residentTabOptions?.dispatched?.();
|
|
813
|
-
await this.#residentTabOptions?.correlated?.(submission.id, command.correlationId).catch(() => undefined);
|
|
814
|
-
}
|
|
815
|
-
return result;
|
|
816
|
-
}
|
|
817
|
-
finally {
|
|
818
|
-
finishSending();
|
|
819
|
-
}
|
|
540
|
+
return this.#submission.submit(text);
|
|
820
541
|
}
|
|
821
542
|
async clearOrExit(now = Date.now()) {
|
|
822
543
|
this.#promptSuggestions?.abortPending();
|
|
@@ -829,8 +550,8 @@ export class OwnedUiSessionPresenter {
|
|
|
829
550
|
}
|
|
830
551
|
async interrupt(now = Date.now()) {
|
|
831
552
|
this.#promptSuggestions?.invalidate();
|
|
832
|
-
if (this.#
|
|
833
|
-
this.#
|
|
553
|
+
if (this.#submission.waiting) {
|
|
554
|
+
this.#submission.cancelWaiting();
|
|
834
555
|
return { outcome: "completed", diagnostic: null };
|
|
835
556
|
}
|
|
836
557
|
if (this.view().lifecycle === "busy")
|
|
@@ -857,23 +578,23 @@ export class OwnedUiSessionPresenter {
|
|
|
857
578
|
return { outcome: "completed", diagnostic: null };
|
|
858
579
|
}
|
|
859
580
|
async abort() {
|
|
860
|
-
return this.#execute(this.#simple("abort"));
|
|
581
|
+
return this.#submission.execute(this.#simple("abort"));
|
|
861
582
|
}
|
|
862
583
|
async retry() {
|
|
863
584
|
this.#promptSuggestions?.invalidate();
|
|
864
|
-
return this.#execute(this.#simple("retry"));
|
|
585
|
+
return this.#submission.execute(this.#simple("retry"));
|
|
865
586
|
}
|
|
866
587
|
async compact() {
|
|
867
588
|
this.#promptSuggestions?.invalidate();
|
|
868
|
-
return this.#execute(this.#simple("compact"));
|
|
589
|
+
return this.#submission.execute(this.#simple("compact"));
|
|
869
590
|
}
|
|
870
591
|
async newSession() {
|
|
871
592
|
this.#promptSuggestions?.invalidate();
|
|
872
|
-
return this.#execute(this.#simple("new-session"));
|
|
593
|
+
return this.#submission.execute(this.#simple("new-session"));
|
|
873
594
|
}
|
|
874
595
|
async resumeSession(sessionPath) {
|
|
875
596
|
this.#promptSuggestions?.invalidate();
|
|
876
|
-
return this.#execute({
|
|
597
|
+
return this.#submission.execute({
|
|
877
598
|
type: "resume-session",
|
|
878
599
|
correlationId: this.#correlation("resume"),
|
|
879
600
|
sessionId: this.backend.identity.sessionId,
|
|
@@ -882,7 +603,7 @@ export class OwnedUiSessionPresenter {
|
|
|
882
603
|
}
|
|
883
604
|
async setModel(providerId, modelId) {
|
|
884
605
|
this.#promptSuggestions?.invalidate();
|
|
885
|
-
return this.#execute({
|
|
606
|
+
return this.#submission.execute({
|
|
886
607
|
type: "set-model",
|
|
887
608
|
correlationId: this.#correlation("model"),
|
|
888
609
|
sessionId: this.backend.identity.sessionId,
|
|
@@ -890,7 +611,7 @@ export class OwnedUiSessionPresenter {
|
|
|
890
611
|
});
|
|
891
612
|
}
|
|
892
613
|
async setThinkingLevel(thinkingLevel) {
|
|
893
|
-
return this.#execute({
|
|
614
|
+
return this.#submission.execute({
|
|
894
615
|
type: "set-thinking-level",
|
|
895
616
|
correlationId: this.#correlation("thinking"),
|
|
896
617
|
sessionId: this.backend.identity.sessionId,
|
|
@@ -909,64 +630,13 @@ export class OwnedUiSessionPresenter {
|
|
|
909
630
|
return workflowAdapterResult(result);
|
|
910
631
|
}
|
|
911
632
|
async queueFollowUp() {
|
|
912
|
-
|
|
913
|
-
return this.#submitWhenReady(draft, () => this.#queueFollowUp(draft));
|
|
914
|
-
}
|
|
915
|
-
async #queueFollowUp(draft) {
|
|
916
|
-
const displayInput = draft.trim();
|
|
917
|
-
if (!displayInput)
|
|
918
|
-
return rejected("nothing to queue");
|
|
919
|
-
const prepared = this.root.preparePromptSubmission(displayInput);
|
|
920
|
-
assertPromptImages(prepared.images);
|
|
921
|
-
const text = prepared.text.trim();
|
|
922
|
-
this.#rememberInput(displayInput, "follow-up");
|
|
923
|
-
if (this.root.editor.getText() === draft)
|
|
924
|
-
this.root.editor.setText("");
|
|
925
|
-
this.root.resumeViewportFollowing();
|
|
926
|
-
return this.#execute({
|
|
927
|
-
type: "follow-up",
|
|
928
|
-
correlationId: this.#correlation("follow-up"),
|
|
929
|
-
sessionId: this.backend.identity.sessionId,
|
|
930
|
-
text,
|
|
931
|
-
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
932
|
-
}, displayInput);
|
|
633
|
+
return this.#submission.queueFollowUp(this.root.editor.getText());
|
|
933
634
|
}
|
|
934
635
|
restoreQueuedInput() {
|
|
935
|
-
|
|
936
|
-
this.#cancelWaitingImages();
|
|
937
|
-
if (queued.length === 0)
|
|
938
|
-
return;
|
|
939
|
-
this.root.editor.setText(queued.join("\n"));
|
|
940
|
-
this.#terminal.requestRender();
|
|
636
|
+
this.#submission.restoreQueued();
|
|
941
637
|
}
|
|
942
638
|
showSelector(title, options, onSelect, onCancel) {
|
|
943
|
-
this.#
|
|
944
|
-
this.#dialogSource = "local";
|
|
945
|
-
this.#dialogId = undefined;
|
|
946
|
-
const component = createPiShellSelector({
|
|
947
|
-
title,
|
|
948
|
-
options,
|
|
949
|
-
onSelect: id => {
|
|
950
|
-
onSelect(id);
|
|
951
|
-
this.#dialogHandle?.hide();
|
|
952
|
-
this.#dialogHandle = undefined;
|
|
953
|
-
this.#dialogSource = undefined;
|
|
954
|
-
},
|
|
955
|
-
onCancel: () => {
|
|
956
|
-
this.#dialogHandle?.hide();
|
|
957
|
-
this.#dialogHandle = undefined;
|
|
958
|
-
this.#dialogSource = undefined;
|
|
959
|
-
onCancel?.();
|
|
960
|
-
},
|
|
961
|
-
});
|
|
962
|
-
const handle = this.#terminal.showOverlay(component, {
|
|
963
|
-
width: "70%",
|
|
964
|
-
maxHeight: "80%",
|
|
965
|
-
anchor: "center",
|
|
966
|
-
inputCoordination: "owned",
|
|
967
|
-
});
|
|
968
|
-
this.#dialogHandle = handle;
|
|
969
|
-
return handle;
|
|
639
|
+
return this.#dialogs.showSelector(title, options, onSelect, onCancel);
|
|
970
640
|
}
|
|
971
641
|
// Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
|
|
972
642
|
#createDialogHost() {
|
|
@@ -1034,15 +704,12 @@ export class OwnedUiSessionPresenter {
|
|
|
1034
704
|
}
|
|
1035
705
|
/** The Skills dialog: a replacement input like the model selector, applying the chosen skill through the prompt path. */
|
|
1036
706
|
showSkillsSelector(skills = this.#skillSummaries()) {
|
|
1037
|
-
const close =
|
|
1038
|
-
this.root.setInputSurface(null);
|
|
1039
|
-
this.#terminal.requestRender();
|
|
1040
|
-
};
|
|
707
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1041
708
|
const component = createPiShellSkillsSelector({
|
|
1042
709
|
skills,
|
|
1043
710
|
onSelect: name => {
|
|
1044
711
|
close();
|
|
1045
|
-
void this.#
|
|
712
|
+
void this.#submission.sendSlashPrompt(skillPrompt(name), skillPrompt(name)).catch(() => this.#submission.reportError());
|
|
1046
713
|
},
|
|
1047
714
|
onCancel: close,
|
|
1048
715
|
});
|
|
@@ -1059,10 +726,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1059
726
|
this.#terminal.requestRender();
|
|
1060
727
|
return;
|
|
1061
728
|
}
|
|
1062
|
-
const close =
|
|
1063
|
-
this.root.setInputSurface(null);
|
|
1064
|
-
this.#terminal.requestRender();
|
|
1065
|
-
};
|
|
729
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1066
730
|
const component = createPiShellUserMessageSelector(options, selection => {
|
|
1067
731
|
close();
|
|
1068
732
|
void this.runWorkflow({ command: "fork", argument: "", selection });
|
|
@@ -1089,10 +753,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1089
753
|
this.#terminal.requestRender();
|
|
1090
754
|
return;
|
|
1091
755
|
}
|
|
1092
|
-
const close =
|
|
1093
|
-
this.root.setInputSurface(null);
|
|
1094
|
-
this.#terminal.requestRender();
|
|
1095
|
-
};
|
|
756
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1096
757
|
const component = createPiShellAuthProviderSelector("logout", options, selection => {
|
|
1097
758
|
close();
|
|
1098
759
|
void this.runWorkflow({ command: "logout", argument: "", selection });
|
|
@@ -1112,10 +773,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1112
773
|
void this.runWorkflow({ command: "login", argument: "", selection });
|
|
1113
774
|
return;
|
|
1114
775
|
}
|
|
1115
|
-
const close =
|
|
1116
|
-
this.root.setInputSurface(null);
|
|
1117
|
-
this.#terminal.requestRender();
|
|
1118
|
-
};
|
|
776
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1119
777
|
const labels = method.options.map(option => option.label);
|
|
1120
778
|
const component = createPiShellExtensionSelector(method.title, labels, label => {
|
|
1121
779
|
const selection = method.options.find(option => option.label === label)?.id;
|
|
@@ -1133,10 +791,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1133
791
|
}, initialSelectedId);
|
|
1134
792
|
}
|
|
1135
793
|
showLoginAuthTypeSelector() {
|
|
1136
|
-
const close =
|
|
1137
|
-
this.root.setInputSurface(null);
|
|
1138
|
-
this.#terminal.requestRender();
|
|
1139
|
-
};
|
|
794
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1140
795
|
const labels = ["Sign in with an account", "Sign in with an API key"];
|
|
1141
796
|
const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
|
|
1142
797
|
close();
|
|
@@ -1152,10 +807,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1152
807
|
this.#terminal.requestRender();
|
|
1153
808
|
return;
|
|
1154
809
|
}
|
|
1155
|
-
const close =
|
|
1156
|
-
this.root.setInputSurface(null);
|
|
1157
|
-
this.#terminal.requestRender();
|
|
1158
|
-
};
|
|
810
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1159
811
|
const component = createPiShellAuthProviderSelector("login", options, id => {
|
|
1160
812
|
close();
|
|
1161
813
|
void this.runWorkflow({ command: "login", argument: "", selection: id });
|
|
@@ -1182,12 +834,11 @@ export class OwnedUiSessionPresenter {
|
|
|
1182
834
|
return this.#shutdownPromise;
|
|
1183
835
|
}
|
|
1184
836
|
async #shutdown() {
|
|
1185
|
-
const
|
|
1186
|
-
if (
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
this.root.appendWorkflowStatus(residentTab.unavailableNotice ?? "Press Ctrl+C twice to leave; this agent keeps running.");
|
|
837
|
+
const detached = await this.#resident.detach();
|
|
838
|
+
if (detached === true)
|
|
839
|
+
return { outcome: "completed", diagnostic: null };
|
|
840
|
+
if (detached !== undefined) {
|
|
841
|
+
this.root.appendWorkflowStatus(detached);
|
|
1191
842
|
this.#terminal.requestRender();
|
|
1192
843
|
return rejected("resident tab detach is unavailable");
|
|
1193
844
|
}
|
|
@@ -1203,29 +854,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1203
854
|
return workflowAdapterResult(result);
|
|
1204
855
|
}
|
|
1205
856
|
finally {
|
|
1206
|
-
this.#
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
#restoreResidentInput(imageSidecar) {
|
|
1210
|
-
const recovered = this.#residentTabOptions?.recovered;
|
|
1211
|
-
if (recovered === undefined)
|
|
1212
|
-
return;
|
|
1213
|
-
const prompts = recovered.prompts
|
|
1214
|
-
.map(prompt => this.root.rehydrateHistoryText(prompt.text, id => imageSidecar?.readAttachment(id) ?? null))
|
|
1215
|
-
.filter(Boolean);
|
|
1216
|
-
if (prompts.length > 0) {
|
|
1217
|
-
this.root.editor.setText(prompts[0]);
|
|
1218
|
-
if (recovered.draft)
|
|
1219
|
-
this.root.editor.addToHistory(recovered.draft);
|
|
1220
|
-
for (const additional of prompts.slice(1))
|
|
1221
|
-
this.root.editor.addToHistory(additional);
|
|
1222
|
-
this.root.appendWorkflowStatus(recovered.draft
|
|
1223
|
-
? `Recovered an unfinished prompt; the prior draft and ${Math.max(0, prompts.length - 1)} additional prompt${prompts.length === 2 ? "" : "s"} are in editor history.`
|
|
1224
|
-
: `Recovered an unfinished prompt${prompts.length > 1 ? `; ${prompts.length - 1} additional prompts are in editor history` : ""}.`);
|
|
1225
|
-
}
|
|
1226
|
-
else if (recovered.draft) {
|
|
1227
|
-
this.root.editor.setText(recovered.draft);
|
|
1228
|
-
this.root.appendWorkflowStatus("Recovered the editor draft from the previous tab process.");
|
|
857
|
+
this.#stop.resolve();
|
|
1229
858
|
}
|
|
1230
859
|
}
|
|
1231
860
|
#currentSessionFile() {
|
|
@@ -1235,48 +864,6 @@ export class OwnedUiSessionPresenter {
|
|
|
1235
864
|
const resume = this.backend.identity.currentSessionResumeMetadata();
|
|
1236
865
|
return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
|
|
1237
866
|
}
|
|
1238
|
-
#residentTab() {
|
|
1239
|
-
return this.#residentTabOptions;
|
|
1240
|
-
}
|
|
1241
|
-
#observeResidentProgress(event) {
|
|
1242
|
-
const resident = this.#residentTabOptions;
|
|
1243
|
-
const seconds = resident?.stallNoticeSeconds;
|
|
1244
|
-
if (resident === undefined || seconds === undefined)
|
|
1245
|
-
return;
|
|
1246
|
-
if (event.type === "session-lifecycle") {
|
|
1247
|
-
this.#residentWorking = event.lifecycle === "busy";
|
|
1248
|
-
if (!this.#residentWorking) {
|
|
1249
|
-
if (this.#residentProgressTimer !== undefined)
|
|
1250
|
-
clearTimeout(this.#residentProgressTimer);
|
|
1251
|
-
this.#residentProgressTimer = undefined;
|
|
1252
|
-
this.#residentStalled = false;
|
|
1253
|
-
resident.reportStatus?.("idle");
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
else if (!this.#residentWorking || ![
|
|
1258
|
-
"transcript-block", "session-view", "assistant-message-completed", "agent-run-started",
|
|
1259
|
-
].includes(event.type)) {
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
if (this.#residentStalled) {
|
|
1263
|
-
this.#residentStalled = false;
|
|
1264
|
-
resident.reportStatus?.("working");
|
|
1265
|
-
}
|
|
1266
|
-
if (this.#residentProgressTimer !== undefined)
|
|
1267
|
-
clearTimeout(this.#residentProgressTimer);
|
|
1268
|
-
this.#residentProgressTimer = setTimeout(() => {
|
|
1269
|
-
this.#residentProgressTimer = undefined;
|
|
1270
|
-
if (this.#disposed || !this.#residentWorking)
|
|
1271
|
-
return;
|
|
1272
|
-
this.#residentStalled = true;
|
|
1273
|
-
resident.reportStatus?.("stalled");
|
|
1274
|
-
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
1275
|
-
this.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · use the tab restart action to restart`);
|
|
1276
|
-
this.#terminal.requestRender();
|
|
1277
|
-
}, seconds * 1_000);
|
|
1278
|
-
this.#residentProgressTimer.unref?.();
|
|
1279
|
-
}
|
|
1280
867
|
async runWorkflow(request) {
|
|
1281
868
|
if (request.command === "quit")
|
|
1282
869
|
return this.shutdown();
|
|
@@ -1297,70 +884,17 @@ export class OwnedUiSessionPresenter {
|
|
|
1297
884
|
if (request.command === "login" && request.selection !== undefined) {
|
|
1298
885
|
const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
|
|
1299
886
|
if (setup) {
|
|
1300
|
-
const close =
|
|
1301
|
-
|
|
1302
|
-
this.#terminal.requestRender();
|
|
1303
|
-
};
|
|
1304
|
-
const dialog = createPiShellLoginDialog({
|
|
1305
|
-
getColumns: () => this.#terminal.viewport().columns,
|
|
1306
|
-
getRows: () => this.#terminal.viewport().rows,
|
|
1307
|
-
requestRender: () => this.#terminal.requestRender(),
|
|
1308
|
-
}, setup.providerId, close, setup.providerName, setup.title);
|
|
887
|
+
const close = this.#dialogs.closeInputSurface;
|
|
888
|
+
const dialog = createPiShellLoginDialog(componentRuntime(this.#terminal), setup.providerId, close, setup.providerName, setup.title);
|
|
1309
889
|
dialog.showInfo(setup.message, [], true);
|
|
1310
890
|
this.root.setInputSurface(dialog);
|
|
1311
891
|
this.#terminal.requestRender();
|
|
1312
892
|
return { outcome: "completed", diagnostic: null };
|
|
1313
893
|
}
|
|
1314
894
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
return
|
|
1318
|
-
}
|
|
1319
|
-
if (request.command === "models" && request.selection === undefined) {
|
|
1320
|
-
this.showModelsDialog(request.argument.trim() || undefined);
|
|
1321
|
-
return { outcome: "completed", diagnostic: null };
|
|
1322
|
-
}
|
|
1323
|
-
if (request.command === "model" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
|
|
1324
|
-
this.showModelSelector();
|
|
1325
|
-
return { outcome: "completed", diagnostic: null };
|
|
1326
|
-
}
|
|
1327
|
-
if (request.command === "fork" && request.selection === undefined && request.confirmed === undefined) {
|
|
1328
|
-
this.showForkSelector();
|
|
1329
|
-
return { outcome: "completed", diagnostic: null };
|
|
1330
|
-
}
|
|
1331
|
-
if (request.command === "thinking" && request.selection === undefined && request.argument.trim().length === 0) {
|
|
1332
|
-
await this.showThinkingSelector();
|
|
1333
|
-
return { outcome: "completed", diagnostic: null };
|
|
1334
|
-
}
|
|
1335
|
-
if (request.command === "trust" && request.selection === undefined && request.confirmed === undefined) {
|
|
1336
|
-
this.showTrustSelector();
|
|
1337
|
-
return { outcome: "completed", diagnostic: null };
|
|
1338
|
-
}
|
|
1339
|
-
if (request.command === "settings" && request.selection === undefined && request.confirmed === undefined) {
|
|
1340
|
-
if (this.#residentTabOptions !== undefined)
|
|
1341
|
-
return rejected("Settings is available from the global Settings tab.");
|
|
1342
|
-
this.showSettingsSelector();
|
|
1343
|
-
return { outcome: "completed", diagnostic: null };
|
|
1344
|
-
}
|
|
1345
|
-
if (request.command === "resume" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
|
|
1346
|
-
await this.showSessionSelector();
|
|
1347
|
-
return { outcome: "completed", diagnostic: null };
|
|
1348
|
-
}
|
|
1349
|
-
if (request.command === "login" && request.selection === undefined && request.confirmed === undefined) {
|
|
1350
|
-
if (request.argument.trim().length === 0)
|
|
1351
|
-
this.showLoginAuthTypeSelector();
|
|
1352
|
-
else
|
|
1353
|
-
this.showLoginMethodSelector(request.argument);
|
|
1354
|
-
return { outcome: "completed", diagnostic: null };
|
|
1355
|
-
}
|
|
1356
|
-
if (request.command === "logout" && request.selection === undefined && request.confirmed === undefined) {
|
|
1357
|
-
await this.showLogoutSelector();
|
|
1358
|
-
return { outcome: "completed", diagnostic: null };
|
|
1359
|
-
}
|
|
1360
|
-
if (request.command === "tree" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
|
|
1361
|
-
await this.showTreeSelector();
|
|
1362
|
-
return { outcome: "completed", diagnostic: null };
|
|
1363
|
-
}
|
|
895
|
+
const selected = openWorkflowSelector(request, this, { residentTab: this.#resident.active });
|
|
896
|
+
if (selected !== undefined)
|
|
897
|
+
return selected;
|
|
1364
898
|
if (request.command === "reload") {
|
|
1365
899
|
const blocked = this.backend.workflows.reloadBlockedResult();
|
|
1366
900
|
if (blocked) {
|
|
@@ -1374,11 +908,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1374
908
|
? await import("./share-operation.js")
|
|
1375
909
|
: undefined;
|
|
1376
910
|
const shareSurface = shareDialog === undefined ? undefined
|
|
1377
|
-
: (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)(
|
|
1378
|
-
getColumns: () => this.#terminal.viewport().columns,
|
|
1379
|
-
getRows: () => this.#terminal.viewport().rows,
|
|
1380
|
-
requestRender: () => this.#terminal.requestRender(),
|
|
1381
|
-
}, "Creating gist…");
|
|
911
|
+
: (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)(componentRuntime(this.#terminal), "Creating gist…");
|
|
1382
912
|
const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
|
|
1383
913
|
const now = this.#reloadPresentation?.now ?? Date.now;
|
|
1384
914
|
const shownAt = now();
|
|
@@ -1462,10 +992,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1462
992
|
}
|
|
1463
993
|
showTrustSelector() {
|
|
1464
994
|
const context = this.backend.catalog.pinnedProjectTrustContext();
|
|
1465
|
-
const close =
|
|
1466
|
-
this.root.setInputSurface(null);
|
|
1467
|
-
this.#terminal.requestRender();
|
|
1468
|
-
};
|
|
995
|
+
const close = this.#dialogs.closeInputSurface;
|
|
1469
996
|
const component = createPiShellTrustSelector({
|
|
1470
997
|
...context,
|
|
1471
998
|
onSelect: selection => {
|
|
@@ -1529,21 +1056,13 @@ export class OwnedUiSessionPresenter {
|
|
|
1529
1056
|
this.#copyIntentSequence += 1;
|
|
1530
1057
|
this.#responseCopy?.dispose();
|
|
1531
1058
|
this.#copyExecutor?.dispose();
|
|
1532
|
-
this.#
|
|
1059
|
+
this.#submission.cancelWaiting();
|
|
1533
1060
|
let pasteCleanup = Promise.resolve();
|
|
1534
1061
|
let historyCleanup = Promise.resolve(true);
|
|
1535
1062
|
attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
|
|
1536
1063
|
attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
|
|
1537
1064
|
attempt(() => this.root.clearViewportPointerState());
|
|
1538
1065
|
attempt(() => this.#streamPresentation.dispose());
|
|
1539
|
-
attempt(() => {
|
|
1540
|
-
if (this.#startupRouteTimer !== undefined)
|
|
1541
|
-
clearTimeout(this.#startupRouteTimer);
|
|
1542
|
-
this.#startupRouteTimer = undefined;
|
|
1543
|
-
if (this.#residentProgressTimer !== undefined)
|
|
1544
|
-
clearTimeout(this.#residentProgressTimer);
|
|
1545
|
-
this.#residentProgressTimer = undefined;
|
|
1546
|
-
});
|
|
1547
1066
|
attempt(() => this.#promptSuggestions?.dispose());
|
|
1548
1067
|
attempt(() => this.#unsubscribePromptSuggestions());
|
|
1549
1068
|
attempt(() => this.#unsubscribePromptImages());
|
|
@@ -1551,21 +1070,18 @@ export class OwnedUiSessionPresenter {
|
|
|
1551
1070
|
attempt(() => this.#unsubscribeSettings());
|
|
1552
1071
|
let exitText = "";
|
|
1553
1072
|
attempt(() => {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
: [this.root.exitTranscript(terminal.columns), resumeHint].filter(Boolean).join("\n\n");
|
|
1073
|
+
exitText = composeExitText({
|
|
1074
|
+
terminalMode: terminal.mode,
|
|
1075
|
+
customViewport: this.#customViewport,
|
|
1076
|
+
exitMode: this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput,
|
|
1077
|
+
resume: this.backend.identity.currentSessionResumeMetadata(),
|
|
1078
|
+
transcript: () => this.root.exitTranscript(terminal.columns),
|
|
1079
|
+
});
|
|
1562
1080
|
});
|
|
1563
1081
|
attempt(() => this.#unbindClipboardWriter());
|
|
1564
|
-
attempt(() => this.#
|
|
1565
|
-
attempt(() => this.#unbindTerminalSettings());
|
|
1566
|
-
attempt(() => this.#unbindShutdownSettings());
|
|
1082
|
+
attempt(() => this.#unbindSessionSettings());
|
|
1567
1083
|
attempt(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
|
|
1568
|
-
|
|
1084
|
+
this.#concerns.dispose(attempt);
|
|
1569
1085
|
attempt(() => this.#extensionBridge.dispose());
|
|
1570
1086
|
attempt(() => this.#programStatus.clear());
|
|
1571
1087
|
// Invariant: the host settles a presenter only once the terminal is restored, so these never stall it.
|
|
@@ -1579,7 +1095,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1579
1095
|
}
|
|
1580
1096
|
await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
|
|
1581
1097
|
await boundedCleanup(() => pasteCleanup).catch(error => settled.push(error));
|
|
1582
|
-
await boundedCleanup(() => this.#
|
|
1098
|
+
await boundedCleanup(() => this.#resident.gracefulStop(this.#currentSessionFile()))
|
|
1583
1099
|
.catch(error => settled.push(error));
|
|
1584
1100
|
await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => settled.push(error));
|
|
1585
1101
|
return settled;
|
|
@@ -1587,14 +1103,6 @@ export class OwnedUiSessionPresenter {
|
|
|
1587
1103
|
this.#released = { exitText, failures, settle };
|
|
1588
1104
|
return this.#released;
|
|
1589
1105
|
}
|
|
1590
|
-
#settleStoppedLifecycle() {
|
|
1591
|
-
void this.dispose().then(() => this.#resolveStoppedLifecycle(), () => this.#resolveStoppedLifecycle());
|
|
1592
|
-
}
|
|
1593
|
-
#resolveStoppedLifecycle() {
|
|
1594
|
-
const resolve = this.#resolveStopped;
|
|
1595
|
-
this.#resolveStopped = undefined;
|
|
1596
|
-
resolve?.();
|
|
1597
|
-
}
|
|
1598
1106
|
// Invariant: incremental block updates notify listeners with the same complete view contract.
|
|
1599
1107
|
#syncBlock(block) {
|
|
1600
1108
|
this.root.applyTranscriptBlock(block);
|
|
@@ -1612,6 +1120,77 @@ export class OwnedUiSessionPresenter {
|
|
|
1612
1120
|
listener(view);
|
|
1613
1121
|
return view;
|
|
1614
1122
|
}
|
|
1123
|
+
// Rationale: extensions reach the shell only through this bridge; every surface they draw lands in the root.
|
|
1124
|
+
#createExtensionBridge() {
|
|
1125
|
+
return createPiExtensionUiBridge({
|
|
1126
|
+
runtime: componentRuntime(this.#terminal),
|
|
1127
|
+
agentDir: this.backend.identity.agentDir,
|
|
1128
|
+
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
1129
|
+
setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
|
|
1130
|
+
showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
|
|
1131
|
+
listenInput: handler => this.#terminal.addInputListener(handler),
|
|
1132
|
+
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
1133
|
+
replaceHeader: component => this.root.setExtensionHeader(component),
|
|
1134
|
+
replaceFooter: component => this.root.setExtensionFooter(component),
|
|
1135
|
+
setStatus: (key, text) => this.root.setExtensionStatus(key, text),
|
|
1136
|
+
setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
|
|
1137
|
+
notify: (message, type) => this.root.addExtensionNotification(message, type),
|
|
1138
|
+
setTitle: title => this.#terminal.setTitle(title),
|
|
1139
|
+
getEditorText: () => this.root.editor.getText(),
|
|
1140
|
+
setEditorText: text => this.root.editor.setText(text),
|
|
1141
|
+
pasteToEditor: text => this.root.editor.insertText(text),
|
|
1142
|
+
addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
|
|
1143
|
+
setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
|
|
1144
|
+
getFooterData: () => this.root.extensionFooterData(),
|
|
1145
|
+
getToolsExpanded: () => this.root.toolsExpanded,
|
|
1146
|
+
setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
#createSubmission(cwd) {
|
|
1150
|
+
return new PromptSubmission({
|
|
1151
|
+
backend: this.backend,
|
|
1152
|
+
root: this.root,
|
|
1153
|
+
terminal: this.#terminal,
|
|
1154
|
+
residentTab: this.#resident,
|
|
1155
|
+
cwd,
|
|
1156
|
+
promptHistory: () => this.#promptHistory,
|
|
1157
|
+
imageSidecar: () => this.#promptHistoryImageSidecar,
|
|
1158
|
+
disposed: () => this.#disposed,
|
|
1159
|
+
correlation: prefix => this.#correlation(prefix),
|
|
1160
|
+
lifecycle: () => this.view().lifecycle,
|
|
1161
|
+
currentSessionFile: () => this.#currentSessionFile(),
|
|
1162
|
+
slashCommand: text => this.#slashCommand(text),
|
|
1163
|
+
invalidateSuggestions: () => this.#promptSuggestions?.invalidate(),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
// Invariant: the order is the reset order on session replacement; copy intent before pastes before the input surface.
|
|
1167
|
+
#createConcerns() {
|
|
1168
|
+
return new SessionConcerns([
|
|
1169
|
+
{ onSessionReplaced: () => { this.#copyIntentSequence += 1; this.#responseCopy?.reset(); } },
|
|
1170
|
+
this.#submission,
|
|
1171
|
+
{ onSessionReplaced: () => this.root.resetPendingPastes() },
|
|
1172
|
+
{ onSessionReplaced: () => this.#promptSuggestions?.invalidate() },
|
|
1173
|
+
{ onSessionReplaced: () => { this.#sessionGeneration = this.backend.identity.sessionGeneration; this.#programStatus.reset(); } },
|
|
1174
|
+
{ onSessionReplaced: () => this.#rebindTranscript() },
|
|
1175
|
+
this.#login,
|
|
1176
|
+
{ onSessionReplaced: () => this.#extensionBridge.reset() },
|
|
1177
|
+
// Invariant: a replaced session takes its transient viewport and owned-route state with it.
|
|
1178
|
+
{ onSessionReplaced: () => this.root.resetViewport() },
|
|
1179
|
+
this.#dialogs,
|
|
1180
|
+
{ onSessionReplaced: () => this.root.setInputSurface(null) },
|
|
1181
|
+
{ onSessionReplaced: () => this.root.resetExtensionUi() },
|
|
1182
|
+
{ onSessionReplaced: () => this.root.resetWorkflowPresentation() },
|
|
1183
|
+
this.#resident,
|
|
1184
|
+
]);
|
|
1185
|
+
}
|
|
1186
|
+
// Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
|
|
1187
|
+
#rebindTranscript() {
|
|
1188
|
+
if (this.#sessionBindingGeneration === this.backend.identity.sessionBindingGeneration)
|
|
1189
|
+
return;
|
|
1190
|
+
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
1191
|
+
this.root.resetTranscript();
|
|
1192
|
+
this.#promptHistory?.reset(this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
|
|
1193
|
+
}
|
|
1615
1194
|
#syncTerminalProgress(view) {
|
|
1616
1195
|
if (!this.#terminal.running)
|
|
1617
1196
|
return;
|
|
@@ -1631,34 +1210,10 @@ export class OwnedUiSessionPresenter {
|
|
|
1631
1210
|
#syncView() {
|
|
1632
1211
|
this.#streamPresentation.noteImmediatePresentation();
|
|
1633
1212
|
const view = this.view();
|
|
1634
|
-
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration)
|
|
1635
|
-
this.#
|
|
1636
|
-
this.#responseCopy?.reset();
|
|
1637
|
-
this.#cancelWaitingImages();
|
|
1638
|
-
this.root.resetPendingPastes();
|
|
1639
|
-
this.#promptSuggestions?.invalidate();
|
|
1640
|
-
this.#sessionGeneration = this.backend.identity.sessionGeneration;
|
|
1641
|
-
this.#programStatus.reset();
|
|
1642
|
-
// Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
|
|
1643
|
-
if (this.#sessionBindingGeneration !== this.backend.identity.sessionBindingGeneration) {
|
|
1644
|
-
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
1645
|
-
this.root.resetTranscript();
|
|
1646
|
-
this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
|
|
1647
|
-
}
|
|
1648
|
-
this.#activeLoginDialog = undefined;
|
|
1649
|
-
this.#extensionBridge.reset();
|
|
1650
|
-
// Invariant: a replaced session takes its transient viewport and owned-route state with it.
|
|
1651
|
-
this.root.resetViewport();
|
|
1652
|
-
this.#dialogHandle?.hide();
|
|
1653
|
-
this.#dialogHandle = undefined;
|
|
1654
|
-
this.#dialogId = undefined;
|
|
1655
|
-
this.#dialogSource = undefined;
|
|
1656
|
-
this.root.setInputSurface(null);
|
|
1657
|
-
this.root.resetExtensionUi();
|
|
1658
|
-
this.root.resetWorkflowPresentation();
|
|
1659
|
-
}
|
|
1213
|
+
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration)
|
|
1214
|
+
this.#concerns.replaceSession();
|
|
1660
1215
|
this.root.update(view);
|
|
1661
|
-
this.#
|
|
1216
|
+
this.#dialogs.syncBackend(view.dialog);
|
|
1662
1217
|
this.#presentStartupTrustWarning(view);
|
|
1663
1218
|
this.#terminal.requestRender();
|
|
1664
1219
|
for (const listener of this.#listeners)
|
|
@@ -1676,108 +1231,34 @@ export class OwnedUiSessionPresenter {
|
|
|
1676
1231
|
this.#startupTrustHandled = true;
|
|
1677
1232
|
this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
|
|
1678
1233
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
if (pending === undefined || this.#disposed || !this.#terminal.running)
|
|
1234
|
+
openOwnedRoute(route) {
|
|
1235
|
+
if (this.#disposed)
|
|
1682
1236
|
return;
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
}, 25);
|
|
1688
|
-
this.#startupRouteTimer.unref?.();
|
|
1237
|
+
// Invariant: a route requested before the runtime runs (the resident Settings bridge route arrives
|
|
1238
|
+
// before start()) is retained, latest wins, and presented through the startup-route path once started.
|
|
1239
|
+
if (!this.#terminal.running) {
|
|
1240
|
+
this.#dialogs.retainStartupRoute(route);
|
|
1689
1241
|
return;
|
|
1690
1242
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
#openOwnedRoute(route, input, onClosed) {
|
|
1695
|
-
const surface = this.#routeHost?.open(route, input) ?? null;
|
|
1696
|
-
if (surface === null)
|
|
1697
|
-
return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
|
|
1698
|
-
if (!this.#terminal.running)
|
|
1699
|
-
return { outcome: "failed", diagnostic: "runtime is not active" };
|
|
1700
|
-
this.root.dismissNotice();
|
|
1701
|
-
this.#dialogHandle?.hide();
|
|
1702
|
-
this.#dialogSource = "route";
|
|
1703
|
-
const handle = this.#terminal.openRoute(surface, {
|
|
1704
|
-
exit: () => { void this.shutdown(); },
|
|
1705
|
-
closed: rendered => {
|
|
1706
|
-
if (this.#dialogHandle === handle) {
|
|
1707
|
-
this.#dialogHandle = undefined;
|
|
1708
|
-
this.#dialogId = undefined;
|
|
1709
|
-
this.#dialogSource = undefined;
|
|
1710
|
-
}
|
|
1711
|
-
if (rendered) {
|
|
1712
|
-
try {
|
|
1713
|
-
void onClosed?.()?.catch(() => undefined);
|
|
1714
|
-
}
|
|
1715
|
-
catch { /* Rationale: a failed acknowledgement stays pending. */ }
|
|
1716
|
-
this.#residentTabOptions?.closeOwnedRoute?.(route);
|
|
1717
|
-
}
|
|
1718
|
-
},
|
|
1719
|
-
});
|
|
1720
|
-
this.#dialogHandle = handle;
|
|
1721
|
-
this.#dialogId = surface.id;
|
|
1722
|
-
return { outcome: "completed", diagnostic: null };
|
|
1723
|
-
}
|
|
1724
|
-
#syncDialog(dialog) {
|
|
1725
|
-
if (!this.#terminal.running)
|
|
1726
|
-
return;
|
|
1727
|
-
if (dialog === null) {
|
|
1728
|
-
// Invariant: locally owned routes (notably /settings) are independent of backend
|
|
1729
|
-
// lifecycle/status events and remain open while an agent is working.
|
|
1730
|
-
if (this.#dialogSource !== "backend")
|
|
1731
|
-
return;
|
|
1732
|
-
this.#dialogHandle?.hide();
|
|
1733
|
-
this.#dialogHandle = undefined;
|
|
1734
|
-
this.#dialogId = undefined;
|
|
1735
|
-
this.#dialogSource = undefined;
|
|
1736
|
-
return;
|
|
1243
|
+
const result = this.#dialogs.openRoute(route);
|
|
1244
|
+
if (result.outcome !== "completed" && result.diagnostic !== null) {
|
|
1245
|
+
this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
|
|
1737
1246
|
}
|
|
1738
|
-
if (this.#dialogSource === "backend" && this.#dialogId === dialog.id)
|
|
1739
|
-
return;
|
|
1740
|
-
this.#dialogHandle?.hide();
|
|
1741
|
-
this.#dialogSource = "backend";
|
|
1742
|
-
const component = createPiShellDialog(dialog, {
|
|
1743
|
-
onSelect: () => {
|
|
1744
|
-
this.#dialogHandle?.hide();
|
|
1745
|
-
this.#dialogHandle = undefined;
|
|
1746
|
-
this.#dialogId = undefined;
|
|
1747
|
-
this.#dialogSource = undefined;
|
|
1748
|
-
},
|
|
1749
|
-
onCancel: () => {
|
|
1750
|
-
this.#dialogHandle?.hide();
|
|
1751
|
-
this.#dialogHandle = undefined;
|
|
1752
|
-
this.#dialogId = undefined;
|
|
1753
|
-
this.#dialogSource = undefined;
|
|
1754
|
-
},
|
|
1755
|
-
});
|
|
1756
|
-
this.#dialogHandle = this.#terminal.showOverlay(component, {
|
|
1757
|
-
width: "70%",
|
|
1758
|
-
maxHeight: "80%",
|
|
1759
|
-
anchor: "center",
|
|
1760
|
-
inputCoordination: "owned",
|
|
1761
|
-
});
|
|
1762
|
-
this.#dialogId = dialog.id;
|
|
1763
|
-
}
|
|
1764
|
-
openOwnedRoute(route) {
|
|
1765
|
-
this.#openOwnedRoute(route);
|
|
1766
1247
|
}
|
|
1767
1248
|
hotkeysPresentation() {
|
|
1768
1249
|
const presentation = this.root.hotkeysPresentation();
|
|
1769
|
-
return this.#
|
|
1250
|
+
return this.#resident.active ? { ...presentation, residentTabs: true } : presentation;
|
|
1770
1251
|
}
|
|
1771
1252
|
async #slashCommand(text) {
|
|
1772
1253
|
const body = text.slice(1).trim();
|
|
1773
1254
|
const separator = body.search(/\s/);
|
|
1774
1255
|
const name = separator < 0 ? body : body.slice(0, separator);
|
|
1775
1256
|
const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
|
|
1776
|
-
if (name === "settings" && this.#
|
|
1257
|
+
if (name === "settings" && this.#resident.active) {
|
|
1777
1258
|
return rejected("Settings is available from the global Settings tab.");
|
|
1778
1259
|
}
|
|
1779
|
-
if (this.#
|
|
1780
|
-
return this.#
|
|
1260
|
+
if (this.#dialogs.claimsRoute(name))
|
|
1261
|
+
return this.#dialogs.openRoute(name);
|
|
1781
1262
|
if (this.#isWorkflowRoute(name))
|
|
1782
1263
|
return this.runWorkflow({ command: name, argument });
|
|
1783
1264
|
if (this.#skillsCollapsed()) {
|
|
@@ -1785,17 +1266,10 @@ export class OwnedUiSessionPresenter {
|
|
|
1785
1266
|
return this.#runSkillsCommand(text, argument);
|
|
1786
1267
|
const rewritten = rewriteSkillsTunnelSubmission(text);
|
|
1787
1268
|
if (rewritten !== text)
|
|
1788
|
-
return this.#
|
|
1269
|
+
return this.#submission.sendSlashPrompt(rewritten, text);
|
|
1789
1270
|
}
|
|
1790
1271
|
// Compatibility: unknown slash input, prompt templates, skills, and extension commands remain Pi prompt input.
|
|
1791
|
-
this.#
|
|
1792
|
-
this.root.resumeViewportFollowing();
|
|
1793
|
-
return this.#execute({
|
|
1794
|
-
type: this.view().lifecycle === "busy" ? "steer" : "prompt",
|
|
1795
|
-
correlationId: this.#correlation("prompt-command"),
|
|
1796
|
-
sessionId: this.backend.identity.sessionId,
|
|
1797
|
-
text,
|
|
1798
|
-
});
|
|
1272
|
+
return this.#submission.sendSlashPrompt(text);
|
|
1799
1273
|
}
|
|
1800
1274
|
#installAutocompleteCommands() {
|
|
1801
1275
|
this.root.editor.setAutocompleteCommands(this.backend.workflows.workflowAutocompleteCommands());
|
|
@@ -1829,18 +1303,7 @@ export class OwnedUiSessionPresenter {
|
|
|
1829
1303
|
this.#terminal.requestRender();
|
|
1830
1304
|
return { outcome: "failed", diagnostic: message };
|
|
1831
1305
|
}
|
|
1832
|
-
return this.#
|
|
1833
|
-
}
|
|
1834
|
-
// Rationale: engine expands skill arguments; history retains typed input for recall.
|
|
1835
|
-
#submitSkillPrompt(prompt, typed) {
|
|
1836
|
-
this.#rememberInput(typed, "slash");
|
|
1837
|
-
this.root.resumeViewportFollowing();
|
|
1838
|
-
return this.#execute({
|
|
1839
|
-
type: this.view().lifecycle === "busy" ? "steer" : "prompt",
|
|
1840
|
-
correlationId: this.#correlation("prompt-command"),
|
|
1841
|
-
sessionId: this.backend.identity.sessionId,
|
|
1842
|
-
text: prompt,
|
|
1843
|
-
});
|
|
1306
|
+
return this.#submission.sendSlashPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
|
|
1844
1307
|
}
|
|
1845
1308
|
#treeSummary() {
|
|
1846
1309
|
return new Promise(resolve => {
|
|
@@ -1881,139 +1344,6 @@ export class OwnedUiSessionPresenter {
|
|
|
1881
1344
|
if (result.diagnostic === "Branch summarization cancelled")
|
|
1882
1345
|
await this.showTreeSelector(entryId);
|
|
1883
1346
|
}
|
|
1884
|
-
#startWorkflowLogin(request) {
|
|
1885
|
-
this.#finishWorkflowLogin();
|
|
1886
|
-
const dialog = createPiShellLoginDialog({
|
|
1887
|
-
getColumns: () => this.#terminal.viewport().columns,
|
|
1888
|
-
getRows: () => this.#terminal.viewport().rows,
|
|
1889
|
-
requestRender: () => this.#terminal.requestRender(),
|
|
1890
|
-
}, request.providerId, success => {
|
|
1891
|
-
if (!success)
|
|
1892
|
-
this.#finishWorkflowLogin();
|
|
1893
|
-
}, request.providerName);
|
|
1894
|
-
this.#activeLoginDialog = dialog;
|
|
1895
|
-
this.root.setInputSurface(dialog);
|
|
1896
|
-
this.#programStatus.setBlocked("authentication", {
|
|
1897
|
-
kind: "auth",
|
|
1898
|
-
message: `Log in to ${request.providerName}`,
|
|
1899
|
-
});
|
|
1900
|
-
this.#syncTerminalProgress(this.view());
|
|
1901
|
-
this.#terminal.requestRender();
|
|
1902
|
-
}
|
|
1903
|
-
#requestWorkflowInput(request) {
|
|
1904
|
-
const dialog = this.#activeLoginDialog;
|
|
1905
|
-
if (!dialog)
|
|
1906
|
-
return Promise.resolve(null);
|
|
1907
|
-
if (request.type === "select") {
|
|
1908
|
-
return new Promise(resolve => {
|
|
1909
|
-
const options = request.options ?? [];
|
|
1910
|
-
const labels = options.map(option => option.label);
|
|
1911
|
-
const restoreDialog = () => {
|
|
1912
|
-
if (this.#activeLoginDialog === dialog)
|
|
1913
|
-
this.root.setInputSurface(dialog);
|
|
1914
|
-
this.#terminal.requestRender();
|
|
1915
|
-
};
|
|
1916
|
-
const selector = createPiShellExtensionSelector(request.message, labels, label => {
|
|
1917
|
-
const id = options.find(option => option.label === label)?.id;
|
|
1918
|
-
restoreDialog();
|
|
1919
|
-
resolve(id ?? null);
|
|
1920
|
-
}, () => {
|
|
1921
|
-
restoreDialog();
|
|
1922
|
-
resolve(null);
|
|
1923
|
-
});
|
|
1924
|
-
this.root.setInputSurface(selector, false);
|
|
1925
|
-
this.#terminal.requestRender();
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
const response = request.type === "manual-code"
|
|
1929
|
-
? dialog.showManualInput(request.message)
|
|
1930
|
-
: dialog.showPrompt(request.message, request.placeholder);
|
|
1931
|
-
this.#terminal.requestRender();
|
|
1932
|
-
return response.then(value => value, () => null);
|
|
1933
|
-
}
|
|
1934
|
-
#notifyWorkflowLogin(event) {
|
|
1935
|
-
const dialog = this.#activeLoginDialog;
|
|
1936
|
-
if (!dialog)
|
|
1937
|
-
return;
|
|
1938
|
-
if (event.type === "auth_url")
|
|
1939
|
-
dialog.showAuth(event.url, event.instructions);
|
|
1940
|
-
else if (event.type === "device_code") {
|
|
1941
|
-
dialog.showDeviceCode(event);
|
|
1942
|
-
dialog.showWaiting("Waiting for authentication...");
|
|
1943
|
-
}
|
|
1944
|
-
else if (event.type === "info")
|
|
1945
|
-
dialog.showInfo(event.message, event.links);
|
|
1946
|
-
else if (event.type === "waiting")
|
|
1947
|
-
dialog.showWaiting(event.message);
|
|
1948
|
-
else
|
|
1949
|
-
dialog.showProgress(event.message);
|
|
1950
|
-
this.#terminal.requestRender();
|
|
1951
|
-
}
|
|
1952
|
-
#finishWorkflowLogin() {
|
|
1953
|
-
if (!this.#activeLoginDialog)
|
|
1954
|
-
return;
|
|
1955
|
-
this.#activeLoginDialog = undefined;
|
|
1956
|
-
this.root.setInputSurface(null);
|
|
1957
|
-
this.#programStatus.setBlocked("authentication", undefined);
|
|
1958
|
-
this.#syncTerminalProgress(this.view());
|
|
1959
|
-
this.#terminal.requestRender();
|
|
1960
|
-
}
|
|
1961
|
-
async #execute(command, draft) {
|
|
1962
|
-
if (draft === undefined)
|
|
1963
|
-
return this.backend.session.execute(command);
|
|
1964
|
-
const revision = this.#editorRevision;
|
|
1965
|
-
try {
|
|
1966
|
-
assertOwnedUiCommand(command);
|
|
1967
|
-
}
|
|
1968
|
-
catch (error) {
|
|
1969
|
-
return this.#recoverSubmission(draft, revision, error);
|
|
1970
|
-
}
|
|
1971
|
-
try {
|
|
1972
|
-
const result = await this.backend.session.execute(command);
|
|
1973
|
-
if (result.outcome === "rejected")
|
|
1974
|
-
return this.#recoverSubmission(draft, revision);
|
|
1975
|
-
return result;
|
|
1976
|
-
}
|
|
1977
|
-
catch {
|
|
1978
|
-
// Security: dispatch might already have reached the provider. Never retry automatically.
|
|
1979
|
-
this.root.editor.addToHistory(draft);
|
|
1980
|
-
this.#promptHistory?.rememberRecovery(draft);
|
|
1981
|
-
this.#reportSubmissionError(undefined, "Submission failed; delivery is uncertain. Check the conversation before retrying. Press Up to recover the draft.");
|
|
1982
|
-
return { outcome: "failed", diagnostic: "submission delivery is uncertain" };
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
async #guardSubmission(draft, action) {
|
|
1986
|
-
const revision = this.#editorRevision;
|
|
1987
|
-
try {
|
|
1988
|
-
return await action();
|
|
1989
|
-
}
|
|
1990
|
-
catch (error) {
|
|
1991
|
-
if (error instanceof ImageAttachmentError && error.code === "image-canceled")
|
|
1992
|
-
return rejected("image submission canceled");
|
|
1993
|
-
return this.#recoverSubmission(draft, revision, error);
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
#recoverSubmission(draft, revision, error) {
|
|
1997
|
-
this.root.editor.addToHistory(draft);
|
|
1998
|
-
this.#promptHistory?.rememberRecovery(draft);
|
|
1999
|
-
// Concurrency: never overwrite input typed (even typed and cleared) after this submission.
|
|
2000
|
-
if (revision === this.#editorRevision && this.root.editor.getText().length === 0)
|
|
2001
|
-
this.root.editor.setText(draft);
|
|
2002
|
-
const message = error instanceof ImageAttachmentError ? error.message : "Submission rejected. Check the prompt and attachments.";
|
|
2003
|
-
this.#reportSubmissionError(error, error instanceof ImageAttachmentError && error.code === "image-count"
|
|
2004
|
-
? message
|
|
2005
|
-
: `${message} Press Up to recover the draft.`);
|
|
2006
|
-
return rejected(message);
|
|
2007
|
-
}
|
|
2008
|
-
#reportSubmissionError(error, message) {
|
|
2009
|
-
// Security: arbitrary provider/extension error messages can contain the entire request.
|
|
2010
|
-
try {
|
|
2011
|
-
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
|
|
2012
|
-
?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
|
|
2013
|
-
this.#terminal.requestRender();
|
|
2014
|
-
}
|
|
2015
|
-
catch { /* Security: error presentation cannot create another rejected submission callback. */ }
|
|
2016
|
-
}
|
|
2017
1347
|
#simple(type) {
|
|
2018
1348
|
return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
|
|
2019
1349
|
}
|
|
@@ -2058,27 +1388,9 @@ function copyCharacters(snapshot) {
|
|
|
2058
1388
|
}
|
|
2059
1389
|
return hasContent ? count : undefined;
|
|
2060
1390
|
}
|
|
2061
|
-
export function formatSessionResumeCommand(metadata) {
|
|
2062
|
-
const args = [PRODUCT_TEXT.commandName];
|
|
2063
|
-
if (!metadata.usesDefaultSessionDir)
|
|
2064
|
-
args.push("--session-dir", quoteCommandArgument(metadata.sessionDir));
|
|
2065
|
-
args.push("--session", metadata.sessionId);
|
|
2066
|
-
return args.join(" ");
|
|
2067
|
-
}
|
|
2068
|
-
export function quoteCommandArgument(value) {
|
|
2069
|
-
if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value))
|
|
2070
|
-
return value;
|
|
2071
|
-
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
2072
|
-
}
|
|
2073
|
-
function dim(value) {
|
|
2074
|
-
return `\u001b[2m${value}\u001b[22m`;
|
|
2075
|
-
}
|
|
2076
1391
|
function rejected(diagnostic) {
|
|
2077
1392
|
return { outcome: "rejected", diagnostic };
|
|
2078
1393
|
}
|
|
2079
|
-
function modelKey(view) {
|
|
2080
|
-
return view.activeModel === null ? "" : `${view.activeModel.providerId}/${view.activeModel.modelId}`;
|
|
2081
|
-
}
|
|
2082
1394
|
function workflowAdapterResult(result) {
|
|
2083
1395
|
if (result.outcome === "completed")
|
|
2084
1396
|
return { outcome: "completed", diagnostic: null };
|