@timurproko/a1 0.2.6-dev.727 → 0.2.6-dev.734
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/clipboard-services.d.ts +19 -0
- package/dist/app/session-shell/clipboard-services.js +7 -0
- package/dist/app/session-shell/exit-notice.d.ts +9 -5
- package/dist/app/session-shell/exit-notice.js +5 -5
- package/dist/app/session-shell/index.d.ts +3 -1
- package/dist/app/session-shell/index.js +1 -0
- package/dist/app/session-shell/paste-preparation-client.d.ts +3 -0
- package/dist/app/session-shell/paste-preparation-client.js +5 -2
- package/dist/app/session-shell/path-chip-presentation.d.ts +10 -3
- package/dist/app/session-shell/path-chip-presentation.js +48 -6
- package/dist/app/session-shell/prompt-chips.js +30 -4
- package/dist/app/session-shell/quit-outro.d.ts +3 -1
- package/dist/app/session-shell/quit-outro.js +23 -8
- package/dist/app/session-shell/response-copy-transport.d.ts +6 -1
- package/dist/app/session-shell/response-copy-transport.js +8 -2
- package/dist/app/session-shell/session-shell-root.d.ts +20 -10
- package/dist/app/session-shell/session-shell-root.js +6 -6
- package/dist/app/session-shell/session-shell.d.ts +19 -21
- package/dist/app/session-shell/session-shell.js +656 -860
- package/dist/composition/owned-ui.d.ts +20 -2
- package/dist/composition/owned-ui.js +66 -35
- package/dist/composition/session-info-reference.d.ts +2 -2
- package/dist/composition/session-info-reference.js +2 -2
- package/dist/contracts/agent-engine/capability-ports.d.ts +1 -85
- package/dist/contracts/agent-engine/capability-ports.js +1 -35
- package/dist/contracts/agent-engine/index.d.ts +1 -7
- package/dist/contracts/agent-engine/index.js +0 -4
- package/dist/contracts/owned-ui/index.d.ts +5 -1
- package/dist/contracts/owned-ui/index.js +1 -0
- package/dist/contracts/owned-ui/model.d.ts +6 -0
- package/dist/contracts/owned-ui/session-backend.d.ts +119 -0
- package/dist/contracts/owned-ui/session-presenters.d.ts +55 -0
- package/dist/contracts/owned-ui/session-presenters.js +1 -0
- package/dist/contracts/owned-ui/session-workflows.d.ts +342 -0
- package/dist/contracts/owned-ui/session-workflows.js +30 -0
- package/dist/features/owned-ui/settings-app.js +35 -15
- package/dist/foundation/release/bootstrap.js +1 -2
- package/dist/foundation/release/release-gc.js +1 -1
- package/dist/foundation/release/update.js +0 -4
- package/dist/foundation/storage/control-store.js +0 -12
- package/dist/integrations/pi/components/components.js +0 -16
- package/dist/integrations/pi/components/index.d.ts +3 -3
- package/dist/integrations/pi/components/index.js +2 -2
- package/dist/integrations/pi/components/models-dialog.js +1 -30
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +11 -9
- package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -3
- package/dist/integrations/pi/components/shell-extension-ui.js +6 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -4
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +21 -1
- package/dist/integrations/pi/components/shell-shared-facade.js +18 -2
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +5 -1
- package/dist/integrations/pi/components/upstream/theme/theme.js +20 -2
- package/dist/integrations/pi/engine/adapter.d.ts +44 -24
- package/dist/integrations/pi/engine/adapter.js +130 -22
- package/dist/integrations/pi/engine/command-dispatch.d.ts +2 -5
- package/dist/integrations/pi/engine/extension-ui-binding.d.ts +2 -11
- package/dist/integrations/pi/engine/host-ports.d.ts +20 -0
- package/dist/integrations/pi/engine/host-ports.js +13 -0
- package/dist/integrations/pi/engine/host.d.ts +70 -0
- package/dist/integrations/pi/engine/host.js +147 -0
- package/dist/integrations/pi/engine/index.d.ts +8 -4
- package/dist/integrations/pi/engine/index.js +3 -1
- package/dist/integrations/pi/engine/resource-catalog.d.ts +4 -23
- package/dist/integrations/pi/engine/runtime-integration.d.ts +8 -2
- package/dist/integrations/pi/engine/runtime-integration.js +12 -7
- package/dist/integrations/pi/engine/session-integration.d.ts +3 -8
- package/dist/integrations/pi/engine/session-integration.js +0 -49
- package/dist/integrations/pi/engine/session-runtime.d.ts +18 -3
- package/dist/integrations/pi/engine/session-runtime.js +18 -7
- package/dist/integrations/pi/engine/settings-bridge.d.ts +2 -2
- package/dist/integrations/pi/engine/settings-bridge.js +0 -2
- package/dist/integrations/pi/engine/settings-port.d.ts +2 -0
- package/dist/integrations/pi/engine/settings-port.js +3 -5
- package/dist/integrations/pi/engine/workflows.d.ts +33 -260
- package/dist/integrations/pi/engine/workflows.js +6 -28
- package/dist/integrations/pi/session-presenters/index.d.ts +2 -0
- package/dist/integrations/pi/session-presenters/index.js +1 -0
- package/dist/integrations/pi/session-presenters/presenters.d.ts +16 -0
- package/dist/integrations/pi/session-presenters/presenters.js +341 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +0 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +66 -1
- package/dist/integrations/pi/tui-runtime/index.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/index.js +1 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/settings/declarations.d.ts +13 -1
- package/dist/ui/settings/declarations.js +12 -1
- package/dist/ui/settings/migrations.js +7 -0
- package/docs/architecture/boundaries.md +9 -1
- package/docs/architecture/project-structure.md +3 -2
- package/package.json +1 -1
- package/dist/contracts/agent-engine/model.d.ts +0 -78
- package/dist/contracts/agent-engine/model.js +0 -1
- package/dist/contracts/agent-engine/ports.d.ts +0 -18
- package/dist/contracts/agent-engine/serialization.d.ts +0 -7
- package/dist/contracts/agent-engine/serialization.js +0 -58
- package/dist/contracts/agent-engine/validation.d.ts +0 -5
- package/dist/contracts/agent-engine/validation.js +0 -128
- package/dist/integrations/pi/components/upstream/theme/system-theme.d.ts +0 -61
- package/dist/integrations/pi/components/upstream/theme/system-theme.js +0 -709
- /package/dist/contracts/{agent-engine/ports.js → owned-ui/session-backend.js} +0 -0
|
@@ -4,21 +4,18 @@ import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
|
|
|
4
4
|
import { PromptHistoryController } from "./prompt-history-controller.js";
|
|
5
5
|
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
6
|
import { boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
|
|
7
|
-
import { assertOwnedUiCommand } from "../../contracts/owned-ui/index.js";
|
|
7
|
+
import { assertOwnedUiCommand, OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
|
|
8
8
|
import { assertPromptImages, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
|
|
9
9
|
import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
|
|
10
10
|
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui/components/mouse.js";
|
|
11
11
|
import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
|
|
12
|
-
import {
|
|
12
|
+
import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
|
|
13
13
|
import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
|
|
14
14
|
import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
|
|
15
|
-
import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
|
|
16
15
|
import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { createPiShellTranscriptComponent, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
|
|
21
|
-
import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
|
|
16
|
+
import { createPiShellAuthProviderSelector, createPiShellDialog, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSelector, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
17
|
+
import { renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
18
|
+
import { piCanvasBackgroundAnsi } from "../../integrations/pi/components/theme.js";
|
|
22
19
|
import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
|
|
23
20
|
import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
24
21
|
import { classifyPiTuiInput } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
@@ -26,10 +23,14 @@ import { runImageWorker } from "./image-preparation-client.js";
|
|
|
26
23
|
import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
|
|
27
24
|
import { writeSystemClipboardText, } from "./system-clipboard.js";
|
|
28
25
|
import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
|
|
26
|
+
// Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
|
|
27
|
+
const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
|
|
29
28
|
export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
|
|
30
29
|
/** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
|
|
31
30
|
export class OwnedUiSessionShell {
|
|
32
31
|
backend;
|
|
32
|
+
#presenters;
|
|
33
|
+
#dialogHost;
|
|
33
34
|
root;
|
|
34
35
|
runtime;
|
|
35
36
|
#cwd;
|
|
@@ -76,6 +77,7 @@ export class OwnedUiSessionShell {
|
|
|
76
77
|
#streamPresentation;
|
|
77
78
|
#removeViewportPreInput;
|
|
78
79
|
#unsubscribeSettings;
|
|
80
|
+
#unsubscribeBackgroundSettings;
|
|
79
81
|
#unbindPiSettings;
|
|
80
82
|
#unbindTerminalSettings;
|
|
81
83
|
#unbindShutdownSettings;
|
|
@@ -93,17 +95,21 @@ export class OwnedUiSessionShell {
|
|
|
93
95
|
#suggestionModelKey;
|
|
94
96
|
constructor(options) {
|
|
95
97
|
const { backend, cwd, routeHost, sessionLayout } = options.engine;
|
|
96
|
-
const { terminal, startup, viewportSettings, quitOutro, reload: reloadPresentation, stream: streamPresentationOptions, input: inputPresentation } = options.presentation ?? {};
|
|
98
|
+
const { terminal, startup, viewportSettings, backgroundSettings, quitOutro, reload: reloadPresentation, stream: streamPresentationOptions, input: inputPresentation, } = options.presentation ?? {};
|
|
97
99
|
const { clipboard, responseCopy, paste: pasteDiagnostics, pastePreparation } = options.diagnostics ?? {};
|
|
98
100
|
const promptHistory = options.history;
|
|
101
|
+
const { clipboard: sharedClipboard, keybindings: sharedKeybindings } = options.shared ?? {};
|
|
99
102
|
this.backend = backend;
|
|
100
|
-
this.#
|
|
101
|
-
this.#
|
|
102
|
-
this.#
|
|
103
|
+
this.#presenters = options.presenters;
|
|
104
|
+
this.#sessionGeneration = this.backend.identity.sessionGeneration;
|
|
105
|
+
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
106
|
+
this.#suggestionModelKey = modelKey(this.backend.session.view());
|
|
103
107
|
this.#cwd = cwd;
|
|
104
108
|
this.#routeHost = routeHost ?? null;
|
|
105
109
|
this.#startupRoute = options.engine.startupRoute;
|
|
106
110
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
111
|
+
// Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
|
|
112
|
+
this.#dialogHost = this.#createDialogHost();
|
|
107
113
|
this.#lazySelectors = options.diagnostics?.lazySelectors;
|
|
108
114
|
this.#stopped = new Promise(resolve => {
|
|
109
115
|
this.#resolveStopped = resolve;
|
|
@@ -116,514 +122,569 @@ export class OwnedUiSessionShell {
|
|
|
116
122
|
// Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
|
|
117
123
|
this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
|
|
118
124
|
this.#skills = this.#customViewport ? options.skills ?? null : null;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const text = await clipboard.readText(signal);
|
|
163
|
-
return text === null ? null : { kind: "text", text };
|
|
164
|
-
};
|
|
165
|
-
this.root = new OwnedUiSessionShellRoot(this.backend.view(), cwd, {
|
|
166
|
-
getColumns: () => runtime?.viewport().columns ?? terminal?.columns ?? 80,
|
|
167
|
-
getRows: () => runtime?.viewport().rows ?? terminal?.rows ?? 24,
|
|
168
|
-
requestRender: force => runtime?.requestRender(force),
|
|
169
|
-
requestHyperlinkCleanup: rows => damageTerminal?.requestHyperlinkCleanup(rows),
|
|
170
|
-
onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
|
|
171
|
-
overlayActive: runtime?.hasOverlay() ?? false,
|
|
172
|
-
selectionActive: this.root.hasActiveSelection(),
|
|
173
|
-
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
174
|
-
}),
|
|
175
|
-
enableDockInputReuse: inputPresentation?.viewportReuse !== false,
|
|
176
|
-
persistentHistory: this.#customViewport && promptHistory !== undefined,
|
|
177
|
-
...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
|
|
178
|
-
onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
|
|
179
|
-
onPasteRejected: error => this.#reportSubmissionError(error),
|
|
180
|
-
onInterrupt: () => { void this.interrupt(); },
|
|
181
|
-
onClear: () => { void this.clearOrExit(); },
|
|
182
|
-
onExit: () => { void this.shutdown(); },
|
|
183
|
-
onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
|
|
184
|
-
onModelCycle: direction => { void this.cycleModel(direction); },
|
|
185
|
-
onThinkingCycle: () => { void this.cycleThinkingLevel(); },
|
|
186
|
-
onThinkingToggle: () => {
|
|
187
|
-
this.root.toggleThinkingVisibility();
|
|
188
|
-
this.runtime.requestRender();
|
|
189
|
-
},
|
|
190
|
-
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
191
|
-
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
|
|
192
|
-
onDequeue: () => this.restoreQueuedInput(),
|
|
193
|
-
onEditorChange: text => {
|
|
194
|
-
this.#editorRevision++;
|
|
195
|
-
promptSuggestionController?.abortPending();
|
|
196
|
-
if (text.length === 0) {
|
|
197
|
-
// Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
|
|
198
|
-
// Reassert retained ghost text in the microtask before Pi emits its scheduled frame.
|
|
199
|
-
queueMicrotask(() => promptSuggestionController?.restoreAvailable());
|
|
125
|
+
// Invariant: construction is transactional. Each binding registers its release as it is made, and a
|
|
126
|
+
// throw releases them in reverse, so a shell that never finished constructing holds nothing.
|
|
127
|
+
const constructed = [];
|
|
128
|
+
try {
|
|
129
|
+
const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
|
|
130
|
+
this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
|
|
131
|
+
execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
|
|
132
|
+
...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.copy }),
|
|
133
|
+
...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
|
|
134
|
+
...(clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => clipboard.writeText(text, signal) }),
|
|
135
|
+
...(terminalCopy ? { terminal: { submit: async (control, signal) => {
|
|
136
|
+
if (signal.aborted || this.#disposed || !runtime?.active)
|
|
137
|
+
throw new Error("Copy canceled");
|
|
138
|
+
// Performance: never grow the real terminal's pending buffer with another clipboard payload.
|
|
139
|
+
if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
|
|
140
|
+
throw new Error("Clipboard terminal is busy");
|
|
141
|
+
}
|
|
142
|
+
runtime.writeControl(control);
|
|
143
|
+
} } } : {}),
|
|
144
|
+
})),
|
|
145
|
+
...(responseCopy?.onEvent === undefined ? {} : { onEvent: responseCopy.onEvent }),
|
|
146
|
+
onFailure: result => {
|
|
147
|
+
if (this.#disposed || !runtime?.active)
|
|
148
|
+
return;
|
|
149
|
+
this.root.appendWorkflowStatus(result.outcome === "timed-out" ? "Copy timed out; the clipboard did not respond."
|
|
150
|
+
: result.failure === "size" ? "Selection exceeds this clipboard route's size limit."
|
|
151
|
+
: "Could not copy the selection; the clipboard is unavailable.");
|
|
152
|
+
runtime.requestRender();
|
|
153
|
+
},
|
|
154
|
+
}) : null;
|
|
155
|
+
constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
|
|
156
|
+
const readClipboard = async (signal) => {
|
|
157
|
+
if (signal.aborted)
|
|
158
|
+
throw new ImageAttachmentError("image-canceled");
|
|
159
|
+
if (clipboard === undefined)
|
|
160
|
+
return runImageWorker({ kind: "clipboard" }, signal);
|
|
161
|
+
try {
|
|
162
|
+
const image = await clipboard.readImage?.(signal);
|
|
163
|
+
if (image !== null && image !== undefined) {
|
|
164
|
+
const canonical = await runImageWorker({ kind: "canonicalize", source: image }, signal);
|
|
165
|
+
if (canonical !== null)
|
|
166
|
+
return { kind: "image", ...canonical };
|
|
167
|
+
}
|
|
200
168
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
queueMicrotask(() => promptSuggestionController?.restoreAvailable());
|
|
206
|
-
},
|
|
207
|
-
onInputSurfaceChanged: () => {
|
|
208
|
-
promptSuggestionController?.invalidate();
|
|
209
|
-
this.#promptHistory?.synchronize();
|
|
210
|
-
},
|
|
211
|
-
onCopyText: text => {
|
|
212
|
-
if (this.#responseCopy !== null) {
|
|
213
|
-
void this.#responseCopy.submitText(text);
|
|
214
|
-
return;
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof ImageAttachmentError)
|
|
171
|
+
throw error;
|
|
172
|
+
// Compatibility: an unavailable image reader can still provide text.
|
|
215
173
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
174
|
+
const text = await clipboard.readText(signal);
|
|
175
|
+
return text === null ? null : { kind: "text", text };
|
|
176
|
+
};
|
|
177
|
+
this.root = new OwnedUiSessionShellRoot(this.backend.session.view(), cwd, {
|
|
178
|
+
getColumns: () => runtime?.viewport().columns ?? terminal?.columns ?? 80,
|
|
179
|
+
getRows: () => runtime?.viewport().rows ?? terminal?.rows ?? 24,
|
|
180
|
+
requestRender: force => runtime?.requestRender(force),
|
|
181
|
+
requestHyperlinkCleanup: rows => damageTerminal?.requestHyperlinkCleanup(rows),
|
|
182
|
+
onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
|
|
183
|
+
overlayActive: runtime?.hasOverlay() ?? false,
|
|
184
|
+
selectionActive: this.root.hasActiveSelection(),
|
|
185
|
+
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
186
|
+
}),
|
|
187
|
+
enableDockInputReuse: inputPresentation?.viewportReuse !== false,
|
|
188
|
+
persistentHistory: this.#customViewport && promptHistory !== undefined,
|
|
189
|
+
...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
|
|
190
|
+
onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
|
|
191
|
+
onPasteRejected: error => this.#reportSubmissionError(error),
|
|
192
|
+
onInterrupt: () => { void this.interrupt(); },
|
|
193
|
+
onClear: () => { void this.clearOrExit(); },
|
|
194
|
+
onExit: () => { void this.shutdown(); },
|
|
195
|
+
onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
|
|
196
|
+
onModelCycle: direction => { void this.cycleModel(direction); },
|
|
197
|
+
onThinkingCycle: () => { void this.cycleThinkingLevel(); },
|
|
198
|
+
onThinkingToggle: () => {
|
|
199
|
+
this.root.toggleThinkingVisibility();
|
|
200
|
+
this.runtime.requestRender();
|
|
201
|
+
},
|
|
202
|
+
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
203
|
+
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
|
|
204
|
+
onDequeue: () => this.restoreQueuedInput(),
|
|
205
|
+
onEditorChange: text => {
|
|
206
|
+
this.#editorRevision++;
|
|
207
|
+
promptSuggestionController?.abortPending();
|
|
208
|
+
if (text.length === 0) {
|
|
209
|
+
// Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
|
|
210
|
+
// Reassert retained ghost text in the microtask before Pi emits its scheduled frame.
|
|
211
|
+
queueMicrotask(() => promptSuggestionController?.restoreAvailable());
|
|
212
|
+
}
|
|
213
|
+
this.root.reconcilePromptImageLimitNotice(text);
|
|
214
|
+
},
|
|
215
|
+
onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
|
|
216
|
+
onPromptSuggestionEligibilityChanged: () => {
|
|
217
|
+
queueMicrotask(() => promptSuggestionController?.restoreAvailable());
|
|
218
|
+
},
|
|
219
|
+
onInputSurfaceChanged: () => {
|
|
220
|
+
promptSuggestionController?.invalidate();
|
|
221
|
+
this.#promptHistory?.synchronize();
|
|
222
|
+
},
|
|
223
|
+
onCopyText: text => {
|
|
224
|
+
if (this.#responseCopy !== null) {
|
|
225
|
+
void this.#responseCopy.submitText(text);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const write = () => {
|
|
229
|
+
if (this.#disposed)
|
|
230
|
+
return Promise.resolve();
|
|
231
|
+
runtime?.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
|
|
232
|
+
return clipboard === undefined ? writeSystemClipboardText(text)
|
|
233
|
+
: clipboard.writeText?.(text) ?? Promise.resolve();
|
|
237
234
|
};
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const initialPiSettings = this.backend.pinnedSettingsSnapshot();
|
|
255
|
-
// Invariant: bare A1 owns a bounded viewport and therefore always runs on the alternate
|
|
256
|
-
// fullscreen surface. The pinned comparison profiles still honor Pi's mode.
|
|
257
|
-
const tuiMode = this.#customViewport
|
|
258
|
-
? "fullscreen"
|
|
259
|
-
: this.backend.disposed ? "regular" : initialPiSettings.tuiMode;
|
|
260
|
-
const runtimeOptions = {
|
|
261
|
-
root: this.root,
|
|
262
|
-
mode: tuiMode,
|
|
263
|
-
...(this.#customViewport ? {
|
|
264
|
-
// Invariant: the owned shell enables and routes mouse reports itself. Pi's
|
|
265
|
-
// enclosing fullscreen renderer must never establish a competing white selection.
|
|
266
|
-
mouse: false,
|
|
267
|
-
consumeUnhandledMouse: true,
|
|
268
|
-
onOverlayGeometry: surfaces => this.root.setViewportOverlaySurfaces(surfaces),
|
|
269
|
-
decorateTerminal: (terminal) => {
|
|
270
|
-
damageTerminal = new DamageAwareTerminalAdapter(terminal, {
|
|
271
|
-
regionalScroll: process.env.TERM !== "dumb",
|
|
272
|
-
inspectHyperlinks: readVisibleHyperlinks,
|
|
273
|
-
onResize: () => streamPresentation?.noteImmediatePresentation(),
|
|
274
|
-
onHyperlinkCleanupRequired: () => runtime?.requestRender(true),
|
|
275
|
-
});
|
|
276
|
-
return damageTerminal;
|
|
235
|
+
// Compatibility: comparison profiles retain their existing clipboard path.
|
|
236
|
+
pendingClipboardWrite = write().catch(() => { });
|
|
237
|
+
},
|
|
238
|
+
readClipboardContent: async (signal = new AbortController().signal) => {
|
|
239
|
+
await pendingClipboardWrite;
|
|
240
|
+
return readClipboard(signal);
|
|
241
|
+
},
|
|
242
|
+
captureClipboardPaste: () => {
|
|
243
|
+
const before = this.#responseCopy?.capturePasteBarrier() ?? (async () => true);
|
|
244
|
+
if (clipboard !== undefined)
|
|
245
|
+
return { kind: "provided", read: readClipboard, before };
|
|
246
|
+
if (responseCopyDestination(process.env) === "terminal")
|
|
247
|
+
return {
|
|
248
|
+
kind: "provided", before, read: async () => { throw new ImageAttachmentError("paste-unavailable"); },
|
|
249
|
+
};
|
|
250
|
+
return { kind: "native", before };
|
|
277
251
|
},
|
|
278
|
-
...(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
252
|
+
...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
|
|
253
|
+
...(pastePreparation === undefined && sharedClipboard === undefined ? {} : {
|
|
254
|
+
pastePreparation: { ...pastePreparation, ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.paste }) },
|
|
255
|
+
}),
|
|
256
|
+
...(sharedKeybindings === undefined ? {} : { keybindings: sharedKeybindings }),
|
|
257
|
+
...(this.#promptImages === null ? {} : { promptImageLimit: this.#promptImages.limit }),
|
|
258
|
+
skillsPresentation: () => this.#skills?.presentation() ?? "expand",
|
|
259
|
+
}, {
|
|
260
|
+
...startup,
|
|
261
|
+
resources: startup?.resources ?? shellResourceEntries(this.backend),
|
|
262
|
+
}, this.backend.identity.agentDir, this.#presenters.transcriptRenderers(), sessionLayout, {
|
|
263
|
+
resolve: assetId => this.backend.extensions.resolveTranscriptImage(assetId),
|
|
264
|
+
});
|
|
265
|
+
// Invariant: the runtime owns the root (its theme subscription and helper clients) once constructed;
|
|
266
|
+
// stopping an idle runtime disposes it synchronously.
|
|
267
|
+
constructed.push(() => {
|
|
268
|
+
if (runtime === undefined)
|
|
269
|
+
this.root.dispose();
|
|
270
|
+
else
|
|
271
|
+
void runtime.dispose().catch(() => { });
|
|
272
|
+
});
|
|
273
|
+
const initialPiSettings = this.backend.settings.snapshot();
|
|
274
|
+
// Invariant: bare A1 owns a bounded viewport and therefore always runs on the alternate
|
|
275
|
+
// fullscreen surface. The pinned comparison profiles still honor Pi's mode.
|
|
276
|
+
const tuiMode = this.#customViewport
|
|
277
|
+
? "fullscreen"
|
|
278
|
+
: this.backend.identity.disposed ? "regular" : initialPiSettings.tuiMode;
|
|
279
|
+
const runtimeOptions = {
|
|
280
|
+
root: this.root,
|
|
281
|
+
mode: tuiMode,
|
|
282
|
+
...(this.#customViewport ? {
|
|
283
|
+
// Invariant: the owned shell enables and routes mouse reports itself. Pi's
|
|
284
|
+
// enclosing fullscreen renderer must never establish a competing white selection.
|
|
285
|
+
mouse: false,
|
|
286
|
+
consumeUnhandledMouse: true,
|
|
287
|
+
onOverlayGeometry: surfaces => this.root.setViewportOverlaySurfaces(surfaces),
|
|
288
|
+
decorateTerminal: (terminal) => {
|
|
289
|
+
damageTerminal = new DamageAwareTerminalAdapter(terminal, {
|
|
290
|
+
regionalScroll: process.env.TERM !== "dumb",
|
|
291
|
+
inspectHyperlinks: readVisibleHyperlinks,
|
|
292
|
+
onResize: () => streamPresentation?.noteImmediatePresentation(),
|
|
293
|
+
onHyperlinkCleanupRequired: () => runtime?.requestRender(true),
|
|
294
|
+
});
|
|
295
|
+
return damageTerminal;
|
|
296
|
+
},
|
|
297
|
+
...(inputPresentation?.coordination === false ? {} : {
|
|
298
|
+
inputCoordination: {
|
|
299
|
+
classify: (data, focusedOverlay) => classifyPiTuiInput(data, focusedOverlay ?? this.root.inputCoordinationSurface()),
|
|
300
|
+
onReceipt: () => streamPresentation?.noteImmediatePresentation(),
|
|
301
|
+
...(inputPresentation?.scheduler === undefined
|
|
302
|
+
? {}
|
|
303
|
+
: { scheduler: inputPresentation.scheduler }),
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
} : { layoutRoot: this.root.layoutRoot() }),
|
|
307
|
+
...(inputPresentation?.onEvent === undefined ? {} : {
|
|
308
|
+
inputDiagnostics: {
|
|
309
|
+
onEvent: inputPresentation.onEvent,
|
|
310
|
+
...(inputPresentation.now === undefined ? {} : { now: inputPresentation.now }),
|
|
285
311
|
},
|
|
286
312
|
}),
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.root.setPromptSuggestion(
|
|
322
|
-
|
|
313
|
+
...(terminal === undefined ? {} : { terminal: terminal }),
|
|
314
|
+
hardwareCursor: this.backend.session.view().terminal.hardwareCursor,
|
|
315
|
+
...(this.#customViewport ? {} : { wheelScrollLines: initialPiSettings.fullscreenWheelScrollLines }),
|
|
316
|
+
};
|
|
317
|
+
runtime = new PiTuiRuntimeAdapter(runtimeOptions);
|
|
318
|
+
this.runtime = runtime;
|
|
319
|
+
this.#programStatus = new ProgramStatusReporter(status => { if (this.runtime.active)
|
|
320
|
+
this.runtime.setProgramStatus(status); }, () => this.view().status.footer?.sessionName ?? undefined);
|
|
321
|
+
this.#damageTerminal = damageTerminal ?? null;
|
|
322
|
+
if (this.#customViewport && backgroundSettings !== undefined && damageTerminal !== undefined) {
|
|
323
|
+
damageTerminal.setCanvasBackground(piCanvasBackgroundAnsi(backgroundSettings.snapshot()));
|
|
324
|
+
}
|
|
325
|
+
this.#quitOutro = quitOutro;
|
|
326
|
+
this.#reloadPresentation = reloadPresentation;
|
|
327
|
+
const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
|
|
328
|
+
streamPresentation = streamPresentationOptions?.scheduler === undefined
|
|
329
|
+
? new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval)
|
|
330
|
+
: new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval, streamPresentationOptions.scheduler);
|
|
331
|
+
this.#streamPresentation = streamPresentation;
|
|
332
|
+
constructed.push(() => this.#streamPresentation.dispose());
|
|
333
|
+
const promptSuggestionOptions = this.#customViewport ? options.suggestions : undefined;
|
|
334
|
+
promptSuggestionController = promptSuggestionOptions === undefined ? null : new ContextualPromptSuggestionController({
|
|
335
|
+
generator: promptSuggestionOptions.generator,
|
|
336
|
+
enabled: promptSuggestionOptions.enabled(),
|
|
337
|
+
...(promptSuggestionOptions.diagnostics === undefined ? {} : { diagnostics: promptSuggestionOptions.diagnostics }),
|
|
338
|
+
surface: {
|
|
339
|
+
canPresent: identity => this.#promptSuggestionPresentationBlockReason(identity) === null,
|
|
340
|
+
presentationBlockReason: identity => this.#promptSuggestionPresentationBlockReason(identity),
|
|
341
|
+
present: text => {
|
|
342
|
+
if (!this.root.canPresentPromptSuggestion())
|
|
343
|
+
return false;
|
|
344
|
+
this.root.setPromptSuggestion(text);
|
|
345
|
+
return true;
|
|
346
|
+
},
|
|
347
|
+
clear: () => this.root.setPromptSuggestion(null),
|
|
348
|
+
requestRender: () => this.runtime.requestRender(),
|
|
323
349
|
},
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
350
|
+
});
|
|
351
|
+
this.#promptSuggestions = promptSuggestionController;
|
|
352
|
+
constructed.push(() => this.#promptSuggestions?.dispose());
|
|
353
|
+
this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
|
|
354
|
+
? () => { }
|
|
355
|
+
: promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
|
|
356
|
+
constructed.push(this.#unsubscribePromptSuggestions);
|
|
357
|
+
this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
|
|
358
|
+
if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
|
|
359
|
+
this.runtime.requestRender();
|
|
360
|
+
});
|
|
361
|
+
constructed.push(this.#unsubscribePromptImages);
|
|
362
|
+
// Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
|
|
363
|
+
// skill-command registration, both of which the owned settings manager reports through one change.
|
|
364
|
+
this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
|
|
365
|
+
// Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
|
|
366
|
+
if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
|
|
367
|
+
return;
|
|
368
|
+
this.#installAutocompleteCommands();
|
|
334
369
|
this.runtime.requestRender();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
&& (this.root.editor.matchesTerminalKey(data, "
|
|
380
|
-
|| this.root.editor.matchesTerminalKey(data, "
|
|
381
|
-
|
|
370
|
+
});
|
|
371
|
+
constructed.push(this.#unsubscribeSkills);
|
|
372
|
+
this.runtime.setHardwareCursor(initialPiSettings.showHardwareCursor);
|
|
373
|
+
this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
|
|
374
|
+
this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
|
|
375
|
+
this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
|
|
376
|
+
// Invariant: bare A1 prints only the resume hint at exit, so the pinned exit-output
|
|
377
|
+
// choice is hidden there and cannot be bound; the comparison profile binds and honors it.
|
|
378
|
+
this.#unbindShutdownSettings = this.backend.settings.productMode === "bare" ? () => { } : this.backend.settings.bindOwner("shutdown", {
|
|
379
|
+
fullscreenExitOutput: { apply() { } },
|
|
380
|
+
});
|
|
381
|
+
constructed.push(this.#unbindShutdownSettings);
|
|
382
|
+
this.#unbindTerminalSettings = this.backend.settings.bindOwner("terminal", {
|
|
383
|
+
showHardwareCursor: { apply: value => {
|
|
384
|
+
if (typeof value !== "boolean")
|
|
385
|
+
throw new TypeError("Hardware cursor setting is invalid");
|
|
386
|
+
this.runtime.setHardwareCursor(value);
|
|
387
|
+
} },
|
|
388
|
+
clearOnShrink: { apply: value => {
|
|
389
|
+
if (typeof value !== "boolean")
|
|
390
|
+
throw new TypeError("Clear-on-shrink setting is invalid");
|
|
391
|
+
this.runtime.setClearOnShrink(value);
|
|
392
|
+
} },
|
|
393
|
+
showTerminalProgress: { apply: value => {
|
|
394
|
+
if (typeof value !== "boolean")
|
|
395
|
+
throw new TypeError("Terminal progress setting is invalid");
|
|
396
|
+
this.#terminalProgressEnabled = value;
|
|
397
|
+
this.#syncTerminalProgress(this.view());
|
|
398
|
+
} },
|
|
399
|
+
});
|
|
400
|
+
constructed.push(this.#unbindTerminalSettings);
|
|
401
|
+
this.#removeViewportPreInput = this.#customViewport
|
|
402
|
+
? this.runtime.addPreInputListener(data => {
|
|
403
|
+
if (inputPresentation?.coordination === false)
|
|
404
|
+
this.#streamPresentation.noteImmediatePresentation();
|
|
405
|
+
// Compatibility: route modal paging and boundary keys before Pi's fullscreen handlers.
|
|
406
|
+
const isDefault = this.root.usesDefaultInputSurface();
|
|
407
|
+
if (!this.runtime.hasOverlay() && !isDefault
|
|
408
|
+
&& (this.root.editor.matchesTerminalKey(data, "pageUp")
|
|
409
|
+
|| this.root.editor.matchesTerminalKey(data, "pageDown"))) {
|
|
410
|
+
this.root.handleInput(data);
|
|
411
|
+
this.runtime.requestRender();
|
|
412
|
+
return { consume: true };
|
|
413
|
+
}
|
|
414
|
+
if (!this.runtime.hasOverlay() && (this.root.editor.matchesTerminalKey(data, "home")
|
|
415
|
+
|| this.root.editor.matchesTerminalKey(data, "end")
|
|
416
|
+
|| !isDefault && (this.root.editor.matchesTerminalKey(data, "ctrl+home")
|
|
417
|
+
|| this.root.editor.matchesTerminalKey(data, "ctrl+end")))) {
|
|
418
|
+
if (isDefault)
|
|
419
|
+
this.root.handleViewportPreInput(data, true);
|
|
420
|
+
this.root.handleInput(data);
|
|
421
|
+
this.runtime.requestRender();
|
|
422
|
+
return { consume: true };
|
|
423
|
+
}
|
|
424
|
+
// Invariant: geometry must belong to the painted frame, including newly opened/nested
|
|
425
|
+
// surfaces. Steady pointer input does not trigger a synchronous composition.
|
|
426
|
+
const viewport = this.runtime.viewport();
|
|
427
|
+
if (data.includes("\u001b[<") && !this.root.viewportInputGeometryReady(viewport.columns, viewport.rows)) {
|
|
428
|
+
this.runtime.renderNow();
|
|
429
|
+
}
|
|
430
|
+
const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
|
|
431
|
+
if (routed.copySelection !== undefined) {
|
|
432
|
+
const snapshot = routed.copySelection;
|
|
433
|
+
const acknowledgement = copyAcknowledgement(snapshot);
|
|
434
|
+
const intent = ++this.#copyIntentSequence;
|
|
435
|
+
void this.#responseCopy?.submit(snapshot, pendingClipboardWrite).then(result => {
|
|
436
|
+
if (this.#disposed || !this.runtime.active || intent !== this.#copyIntentSequence
|
|
437
|
+
|| result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
|
|
438
|
+
return;
|
|
439
|
+
if (acknowledgement.hasNonWhitespace) {
|
|
440
|
+
this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (!routed.consumed)
|
|
445
|
+
return routed.data === data ? undefined : { data: routed.data };
|
|
446
|
+
return routed.data.length === 0 ? { consume: true } : { data: routed.data };
|
|
447
|
+
})
|
|
448
|
+
: () => { };
|
|
449
|
+
constructed.push(this.#removeViewportPreInput);
|
|
450
|
+
const applyViewportSettings = () => {
|
|
451
|
+
const snapshot = viewportSettings?.snapshot();
|
|
452
|
+
this.root.setViewportConfig(snapshot ?? {
|
|
453
|
+
scrollbarAppearance: "auto",
|
|
454
|
+
scrollbarStyle: "thin",
|
|
455
|
+
scrollbarSpeed: "normal",
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
applyViewportSettings();
|
|
459
|
+
this.#unsubscribeSettings = this.#customViewport && viewportSettings
|
|
460
|
+
? viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
|
|
461
|
+
: () => { };
|
|
462
|
+
constructed.push(this.#unsubscribeSettings);
|
|
463
|
+
this.#unsubscribeBackgroundSettings = this.#customViewport && backgroundSettings
|
|
464
|
+
? backgroundSettings.onChange(style => {
|
|
465
|
+
if (this.#damageTerminal?.setCanvasBackground(piCanvasBackgroundAnsi(style))) {
|
|
466
|
+
this.runtime.requestRender(true);
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
: () => { };
|
|
470
|
+
constructed.push(this.#unsubscribeBackgroundSettings);
|
|
471
|
+
this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
|
|
472
|
+
this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
|
|
473
|
+
this.root.setOutputPad(initialPiSettings.outputPad);
|
|
474
|
+
this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
|
|
475
|
+
this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
|
|
476
|
+
this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
|
|
477
|
+
this.#showImages = initialPiSettings.showImages;
|
|
478
|
+
this.#imageWidthCells = initialPiSettings.imageWidthCells;
|
|
479
|
+
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
480
|
+
this.#unbindPiSettings = this.backend.settings.bindOwner("shell", {
|
|
481
|
+
editorPaddingX: { apply: value => {
|
|
482
|
+
if (typeof value !== "number")
|
|
483
|
+
throw new TypeError("Editor padding is invalid");
|
|
484
|
+
this.root.setEditorPaddingX(value);
|
|
485
|
+
} },
|
|
486
|
+
autocompleteMaxVisible: { apply: value => {
|
|
487
|
+
if (typeof value !== "number")
|
|
488
|
+
throw new TypeError("Autocomplete maximum is invalid");
|
|
489
|
+
this.root.setAutocompleteMaxVisible(value);
|
|
490
|
+
} },
|
|
491
|
+
outputPad: { apply: value => {
|
|
492
|
+
if (value !== 0 && value !== 1)
|
|
493
|
+
throw new TypeError("Output padding is invalid");
|
|
494
|
+
this.root.setOutputPad(value);
|
|
495
|
+
} },
|
|
496
|
+
hideThinkingBlock: { apply: value => {
|
|
497
|
+
if (typeof value !== "boolean")
|
|
498
|
+
throw new TypeError("Thinking-block visibility is invalid");
|
|
499
|
+
this.root.setHideThinkingBlock(value);
|
|
500
|
+
} },
|
|
501
|
+
mermaidRenderingMode: { apply: value => {
|
|
502
|
+
if (value !== "off" && value !== "final" && value !== "streaming")
|
|
503
|
+
throw new TypeError("Mermaid mode is invalid");
|
|
504
|
+
this.root.setMermaidRenderingMode(value);
|
|
505
|
+
} },
|
|
506
|
+
showImages: { apply: value => {
|
|
507
|
+
if (typeof value !== "boolean")
|
|
508
|
+
throw new TypeError("Image visibility is invalid");
|
|
509
|
+
this.#showImages = value;
|
|
510
|
+
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
511
|
+
} },
|
|
512
|
+
imageWidthCells: { apply: value => {
|
|
513
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
514
|
+
throw new TypeError("Image width is invalid");
|
|
515
|
+
this.#imageWidthCells = value;
|
|
516
|
+
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
517
|
+
} },
|
|
518
|
+
...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
|
|
519
|
+
if (typeof value !== "boolean")
|
|
520
|
+
throw new TypeError("Fullscreen copy-on-select setting is invalid");
|
|
521
|
+
this.root.setFullscreenCopyOnSelect(value);
|
|
522
|
+
} } } : {
|
|
523
|
+
fullscreenWheelScrollLines: { apply: (value) => {
|
|
524
|
+
if (value !== "auto" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100)) {
|
|
525
|
+
throw new TypeError("Fullscreen wheel-scroll distance is invalid");
|
|
526
|
+
}
|
|
527
|
+
this.runtime.setWheelScrollLines(value);
|
|
528
|
+
} },
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
constructed.push(this.#unbindPiSettings);
|
|
532
|
+
if (this.#customViewport && promptHistory !== undefined) {
|
|
533
|
+
this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
|
|
534
|
+
const sidecar = this.#promptHistoryImageSidecar;
|
|
535
|
+
this.#promptHistory = new PromptHistoryController({
|
|
536
|
+
editor: this.root.editor,
|
|
537
|
+
store: promptHistory.store,
|
|
538
|
+
limit: promptHistory.limit,
|
|
539
|
+
fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
|
|
540
|
+
active: () => this.root.usesDefaultInputSurface(),
|
|
541
|
+
render: () => this.runtime.requestRender(),
|
|
542
|
+
rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
|
|
543
|
+
});
|
|
544
|
+
constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
|
|
545
|
+
}
|
|
546
|
+
this.#extensionBridge = createPiExtensionUiBridge({
|
|
547
|
+
runtime: {
|
|
548
|
+
getColumns: () => this.runtime.viewport().columns,
|
|
549
|
+
getRows: () => this.runtime.viewport().rows,
|
|
550
|
+
requestRender: () => this.runtime.requestRender(),
|
|
551
|
+
},
|
|
552
|
+
agentDir: this.backend.identity.agentDir,
|
|
553
|
+
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
554
|
+
setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
|
|
555
|
+
showOverlay: (component, overlayOptions) => this.runtime.showOverlay(component, overlayOptions),
|
|
556
|
+
listenInput: handler => this.runtime.addInputListener(handler),
|
|
557
|
+
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
558
|
+
replaceHeader: component => this.root.setExtensionHeader(component),
|
|
559
|
+
replaceFooter: component => this.root.setExtensionFooter(component),
|
|
560
|
+
setStatus: (key, text) => this.root.setExtensionStatus(key, text),
|
|
561
|
+
setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
|
|
562
|
+
notify: (message, type) => this.root.addExtensionNotification(message, type),
|
|
563
|
+
setTitle: title => this.runtime.setTitle(title),
|
|
564
|
+
getEditorText: () => this.root.editor.getText(),
|
|
565
|
+
setEditorText: text => this.root.editor.setText(text),
|
|
566
|
+
pasteToEditor: text => this.root.editor.insertText(text),
|
|
567
|
+
addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
|
|
568
|
+
setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
|
|
569
|
+
getFooterData: () => this.root.extensionFooterData(),
|
|
570
|
+
getToolsExpanded: () => this.root.toolsExpanded,
|
|
571
|
+
setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
|
|
572
|
+
});
|
|
573
|
+
constructed.push(() => this.#extensionBridge.dispose());
|
|
574
|
+
constructed.push(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
|
|
575
|
+
this.backend.workflows.setWorkflowInteractionHost({
|
|
576
|
+
startLogin: request => this.#startWorkflowLogin(request),
|
|
577
|
+
prompt: request => this.#requestWorkflowInput(request),
|
|
578
|
+
notify: event => this.#notifyWorkflowLogin(event),
|
|
579
|
+
publish: message => {
|
|
580
|
+
this.root.appendWorkflowMessage(message);
|
|
382
581
|
this.runtime.requestRender();
|
|
383
|
-
|
|
582
|
+
},
|
|
583
|
+
finishLogin: () => this.#finishWorkflowLogin(),
|
|
584
|
+
});
|
|
585
|
+
this.#installAutocompleteCommands();
|
|
586
|
+
this.#unsubscribe = this.backend.session.onEvent(event => {
|
|
587
|
+
// Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
|
|
588
|
+
if (this.#disposed) {
|
|
589
|
+
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
590
|
+
this.#settleStoppedLifecycle();
|
|
591
|
+
return;
|
|
384
592
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
593
|
+
// Performance: a streamed chunk names one block, and touching only that block is what keeps the
|
|
594
|
+
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
595
|
+
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
596
|
+
if (event.type === "agent-run-started") {
|
|
597
|
+
this.#promptSuggestions?.invalidate();
|
|
598
|
+
this.root.resumeViewportFollowing();
|
|
599
|
+
}
|
|
600
|
+
if (event.type === "assistant-message-completed" && !event.successful) {
|
|
601
|
+
this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
|
|
602
|
+
}
|
|
603
|
+
if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
|
|
604
|
+
this.#programStatus.handleEvent(event);
|
|
605
|
+
}
|
|
606
|
+
if (event.type === "assistant-message-completed") {
|
|
607
|
+
this.root.noteCompletedAssistantMessage();
|
|
608
|
+
const identity = {
|
|
609
|
+
sessionId: event.sessionId,
|
|
610
|
+
sessionGeneration: event.sessionGeneration,
|
|
611
|
+
runSequence: event.runSequence,
|
|
612
|
+
responseSequence: event.responseSequence,
|
|
613
|
+
model: event.model,
|
|
614
|
+
};
|
|
615
|
+
if (event.model === null)
|
|
616
|
+
this.#promptSuggestions?.skip(identity, "no-model");
|
|
617
|
+
else
|
|
618
|
+
this.#promptSuggestions?.consider({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
619
|
+
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
620
|
+
: event.stopReason !== "stop" ? "incomplete-response"
|
|
621
|
+
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
622
|
+
: this.root.promptSuggestionPrepareBlockReason());
|
|
394
623
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
624
|
+
const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
|
|
625
|
+
const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
|
|
626
|
+
? this.#syncBlock(event.block)
|
|
627
|
+
: semanticOnly ? this.view() : this.#syncView();
|
|
628
|
+
this.#syncTerminalProgress(view);
|
|
629
|
+
const currentModelKey = modelKey(view);
|
|
630
|
+
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
|
|
631
|
+
this.#promptSuggestions?.invalidate();
|
|
632
|
+
this.#suggestionModelKey = currentModelKey;
|
|
400
633
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const snapshot = routed.copySelection;
|
|
404
|
-
const acknowledgement = copyAcknowledgement(snapshot);
|
|
405
|
-
const intent = ++this.#copyIntentSequence;
|
|
406
|
-
void this.#responseCopy?.submit(snapshot, pendingClipboardWrite).then(result => {
|
|
407
|
-
if (this.#disposed || !runtime.active || intent !== this.#copyIntentSequence
|
|
408
|
-
|| result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
|
|
409
|
-
return;
|
|
410
|
-
if (acknowledgement.hasNonWhitespace) {
|
|
411
|
-
this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
|
|
412
|
-
}
|
|
413
|
-
});
|
|
634
|
+
if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
|
|
635
|
+
this.#promptSuggestions?.invalidate();
|
|
414
636
|
}
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
637
|
+
if (event.type === "agent-run-settled") {
|
|
638
|
+
const identity = {
|
|
639
|
+
sessionId: event.sessionId,
|
|
640
|
+
sessionGeneration: event.sessionGeneration,
|
|
641
|
+
runSequence: event.runSequence,
|
|
642
|
+
responseSequence: event.responseSequence,
|
|
643
|
+
model: event.model,
|
|
644
|
+
};
|
|
645
|
+
if (event.model === null)
|
|
646
|
+
this.#promptSuggestions?.skip(identity, "no-model");
|
|
647
|
+
else
|
|
648
|
+
this.#promptSuggestions?.settle({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
649
|
+
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
650
|
+
: event.stopReason !== "stop" ? "incomplete-response"
|
|
651
|
+
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
652
|
+
: this.root.promptSuggestionPrepareBlockReason());
|
|
653
|
+
}
|
|
654
|
+
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
655
|
+
this.#settleStoppedLifecycle();
|
|
426
656
|
});
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
|
|
436
|
-
this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
|
|
437
|
-
this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
|
|
438
|
-
this.#showImages = initialPiSettings.showImages;
|
|
439
|
-
this.#imageWidthCells = initialPiSettings.imageWidthCells;
|
|
440
|
-
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
441
|
-
this.#unbindPiSettings = this.backend.bindSettingsOwner("shell", {
|
|
442
|
-
editorPaddingX: { apply: value => {
|
|
443
|
-
if (typeof value !== "number")
|
|
444
|
-
throw new TypeError("Editor padding is invalid");
|
|
445
|
-
this.root.setEditorPaddingX(value);
|
|
446
|
-
} },
|
|
447
|
-
autocompleteMaxVisible: { apply: value => {
|
|
448
|
-
if (typeof value !== "number")
|
|
449
|
-
throw new TypeError("Autocomplete maximum is invalid");
|
|
450
|
-
this.root.setAutocompleteMaxVisible(value);
|
|
451
|
-
} },
|
|
452
|
-
outputPad: { apply: value => {
|
|
453
|
-
if (value !== 0 && value !== 1)
|
|
454
|
-
throw new TypeError("Output padding is invalid");
|
|
455
|
-
this.root.setOutputPad(value);
|
|
456
|
-
} },
|
|
457
|
-
hideThinkingBlock: { apply: value => {
|
|
458
|
-
if (typeof value !== "boolean")
|
|
459
|
-
throw new TypeError("Thinking-block visibility is invalid");
|
|
460
|
-
this.root.setHideThinkingBlock(value);
|
|
461
|
-
} },
|
|
462
|
-
mermaidRenderingMode: { apply: value => {
|
|
463
|
-
if (value !== "off" && value !== "final" && value !== "streaming")
|
|
464
|
-
throw new TypeError("Mermaid mode is invalid");
|
|
465
|
-
this.root.setMermaidRenderingMode(value);
|
|
466
|
-
} },
|
|
467
|
-
showImages: { apply: value => {
|
|
468
|
-
if (typeof value !== "boolean")
|
|
469
|
-
throw new TypeError("Image visibility is invalid");
|
|
470
|
-
this.#showImages = value;
|
|
471
|
-
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
472
|
-
} },
|
|
473
|
-
imageWidthCells: { apply: value => {
|
|
474
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
475
|
-
throw new TypeError("Image width is invalid");
|
|
476
|
-
this.#imageWidthCells = value;
|
|
477
|
-
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
478
|
-
} },
|
|
479
|
-
...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
|
|
480
|
-
if (typeof value !== "boolean")
|
|
481
|
-
throw new TypeError("Fullscreen copy-on-select setting is invalid");
|
|
482
|
-
this.root.setFullscreenCopyOnSelect(value);
|
|
483
|
-
} } } : {
|
|
484
|
-
fullscreenWheelScrollLines: { apply: (value) => {
|
|
485
|
-
if (value !== "auto" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100)) {
|
|
486
|
-
throw new TypeError("Fullscreen wheel-scroll distance is invalid");
|
|
487
|
-
}
|
|
488
|
-
this.runtime.setWheelScrollLines(value);
|
|
489
|
-
} },
|
|
490
|
-
}),
|
|
491
|
-
});
|
|
492
|
-
if (this.#customViewport && promptHistory !== undefined) {
|
|
493
|
-
this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
|
|
494
|
-
const sidecar = this.#promptHistoryImageSidecar;
|
|
495
|
-
this.#promptHistory = new PromptHistoryController({
|
|
496
|
-
editor: this.root.editor,
|
|
497
|
-
store: promptHistory.store,
|
|
498
|
-
limit: promptHistory.limit,
|
|
499
|
-
fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
|
|
500
|
-
active: () => this.root.usesDefaultInputSurface(),
|
|
501
|
-
render: () => this.runtime.requestRender(),
|
|
502
|
-
rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
|
|
657
|
+
constructed.push(this.#unsubscribe);
|
|
658
|
+
this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.extensions.bindClipboardWriter(async (text) => {
|
|
659
|
+
const result = await this.#responseCopy.submitText(text);
|
|
660
|
+
if (result.outcome === "delivered")
|
|
661
|
+
return true;
|
|
662
|
+
if (result.outcome === "submitted-unverified")
|
|
663
|
+
return false;
|
|
664
|
+
throw new Error(result.outcome === "timed-out" ? "Clipboard delivery timed out" : "Clipboard delivery could not be completed");
|
|
503
665
|
});
|
|
666
|
+
constructed.push(this.#unbindClipboardWriter);
|
|
667
|
+
if (this.backend.session.view().lifecycle === "stopped")
|
|
668
|
+
this.#resolveStopped?.();
|
|
504
669
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
agentDir: this.backend.agentDir,
|
|
512
|
-
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
513
|
-
setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
|
|
514
|
-
showOverlay: (component, overlayOptions) => this.runtime.showOverlay(component, overlayOptions),
|
|
515
|
-
listenInput: handler => this.runtime.addInputListener(handler),
|
|
516
|
-
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
517
|
-
replaceHeader: component => this.root.setExtensionHeader(component),
|
|
518
|
-
replaceFooter: component => this.root.setExtensionFooter(component),
|
|
519
|
-
setStatus: (key, text) => this.root.setExtensionStatus(key, text),
|
|
520
|
-
setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
|
|
521
|
-
notify: (message, type) => this.root.addExtensionNotification(message, type),
|
|
522
|
-
setTitle: title => this.runtime.setTitle(title),
|
|
523
|
-
getEditorText: () => this.root.editor.getText(),
|
|
524
|
-
setEditorText: text => this.root.editor.setText(text),
|
|
525
|
-
pasteToEditor: text => this.root.editor.insertText(text),
|
|
526
|
-
addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
|
|
527
|
-
setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
|
|
528
|
-
getFooterData: () => this.root.extensionFooterData(),
|
|
529
|
-
getToolsExpanded: () => this.root.toolsExpanded,
|
|
530
|
-
setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
|
|
531
|
-
});
|
|
532
|
-
this.backend.setWorkflowInteractionHost({
|
|
533
|
-
startLogin: request => this.#startWorkflowLogin(request),
|
|
534
|
-
prompt: request => this.#requestWorkflowInput(request),
|
|
535
|
-
notify: event => this.#notifyWorkflowLogin(event),
|
|
536
|
-
publish: message => {
|
|
537
|
-
this.root.appendWorkflowMessage(message);
|
|
538
|
-
this.runtime.requestRender();
|
|
539
|
-
},
|
|
540
|
-
finishLogin: () => this.#finishWorkflowLogin(),
|
|
541
|
-
});
|
|
542
|
-
this.#installAutocompleteCommands();
|
|
543
|
-
this.#unsubscribe = this.backend.onEvent(event => {
|
|
544
|
-
// Performance: a streamed chunk names one block, and touching only that block is what keeps the
|
|
545
|
-
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
546
|
-
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
547
|
-
if (event.type === "agent-run-started") {
|
|
548
|
-
this.#promptSuggestions?.invalidate();
|
|
549
|
-
this.root.resumeViewportFollowing();
|
|
550
|
-
}
|
|
551
|
-
if (event.type === "assistant-message-completed" && !event.successful) {
|
|
552
|
-
this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
|
|
553
|
-
}
|
|
554
|
-
if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
|
|
555
|
-
this.#programStatus.handleEvent(event);
|
|
556
|
-
}
|
|
557
|
-
if (event.type === "assistant-message-completed") {
|
|
558
|
-
this.root.noteCompletedAssistantMessage();
|
|
559
|
-
const identity = {
|
|
560
|
-
sessionId: event.sessionId,
|
|
561
|
-
sessionGeneration: event.sessionGeneration,
|
|
562
|
-
runSequence: event.runSequence,
|
|
563
|
-
responseSequence: event.responseSequence,
|
|
564
|
-
model: event.model,
|
|
565
|
-
};
|
|
566
|
-
if (event.model === null)
|
|
567
|
-
this.#promptSuggestions?.skip(identity, "no-model");
|
|
568
|
-
else
|
|
569
|
-
this.#promptSuggestions?.consider({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
570
|
-
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
571
|
-
: event.stopReason !== "stop" ? "incomplete-response"
|
|
572
|
-
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
573
|
-
: this.root.promptSuggestionPrepareBlockReason());
|
|
574
|
-
}
|
|
575
|
-
const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
|
|
576
|
-
const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
|
|
577
|
-
? this.#syncBlock(event.block)
|
|
578
|
-
: semanticOnly ? this.view() : this.#syncView();
|
|
579
|
-
this.#syncTerminalProgress(view);
|
|
580
|
-
const currentModelKey = modelKey(view);
|
|
581
|
-
if (this.backend.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
|
|
582
|
-
this.#promptSuggestions?.invalidate();
|
|
583
|
-
this.#suggestionModelKey = currentModelKey;
|
|
584
|
-
}
|
|
585
|
-
if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
|
|
586
|
-
this.#promptSuggestions?.invalidate();
|
|
587
|
-
}
|
|
588
|
-
if (event.type === "agent-run-settled") {
|
|
589
|
-
const identity = {
|
|
590
|
-
sessionId: event.sessionId,
|
|
591
|
-
sessionGeneration: event.sessionGeneration,
|
|
592
|
-
runSequence: event.runSequence,
|
|
593
|
-
responseSequence: event.responseSequence,
|
|
594
|
-
model: event.model,
|
|
595
|
-
};
|
|
596
|
-
if (event.model === null)
|
|
597
|
-
this.#promptSuggestions?.skip(identity, "no-model");
|
|
598
|
-
else
|
|
599
|
-
this.#promptSuggestions?.settle({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
600
|
-
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
601
|
-
: event.stopReason !== "stop" ? "incomplete-response"
|
|
602
|
-
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
603
|
-
: this.root.promptSuggestionPrepareBlockReason());
|
|
670
|
+
catch (error) {
|
|
671
|
+
for (const release of constructed.reverse()) {
|
|
672
|
+
try {
|
|
673
|
+
release();
|
|
674
|
+
}
|
|
675
|
+
catch { /* Rationale: the construction failure is the error worth reporting. */ }
|
|
604
676
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
});
|
|
608
|
-
this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.bindClipboardWriter(async (text) => {
|
|
609
|
-
const result = await this.#responseCopy.submitText(text);
|
|
610
|
-
if (result.outcome === "delivered")
|
|
611
|
-
return true;
|
|
612
|
-
if (result.outcome === "submitted-unverified")
|
|
613
|
-
return false;
|
|
614
|
-
throw new Error(result.outcome === "timed-out" ? "Clipboard delivery timed out" : "Clipboard delivery could not be completed");
|
|
615
|
-
});
|
|
616
|
-
if (this.backend.view().lifecycle === "stopped")
|
|
617
|
-
this.#resolveStopped?.();
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
618
679
|
}
|
|
619
680
|
view() {
|
|
620
|
-
return this.backend.view();
|
|
681
|
+
return this.backend.session.view();
|
|
621
682
|
}
|
|
622
683
|
#promptSuggestionPresentationBlockReason(identity) {
|
|
623
684
|
const view = this.view();
|
|
624
685
|
if (this.#disposed)
|
|
625
686
|
return "disposed";
|
|
626
|
-
if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.sessionGeneration
|
|
687
|
+
if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.identity.sessionGeneration
|
|
627
688
|
|| modelKey(view) !== `${identity.model.providerId}/${identity.model.modelId}`)
|
|
628
689
|
return "stale-identity";
|
|
629
690
|
return this.root.promptSuggestionPresentationBlockReason();
|
|
@@ -640,7 +701,7 @@ export class OwnedUiSessionShell {
|
|
|
640
701
|
this.#syncTerminalProgress(this.view());
|
|
641
702
|
if (this.#customViewport)
|
|
642
703
|
this.#setPointerReporting(true);
|
|
643
|
-
void this.backend.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
|
|
704
|
+
void this.backend.extensions.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
|
|
644
705
|
this.#syncView();
|
|
645
706
|
// Performance: the spare clipboard helpers fork after the first frame is out, so startup never waits on them.
|
|
646
707
|
setImmediate(() => {
|
|
@@ -670,10 +731,10 @@ export class OwnedUiSessionShell {
|
|
|
670
731
|
if (!this.root.hasPendingPastes(draft))
|
|
671
732
|
return this.#guardSubmission(draft, action);
|
|
672
733
|
const controller = new AbortController();
|
|
673
|
-
const generation = this.backend.sessionGeneration;
|
|
734
|
+
const generation = this.backend.identity.sessionGeneration;
|
|
674
735
|
const result = this.#guardSubmission(draft, async () => {
|
|
675
736
|
await this.root.waitForPromptPastes(draft, controller.signal);
|
|
676
|
-
if (controller.signal.aborted || this.#disposed || this.backend.sessionGeneration !== generation)
|
|
737
|
+
if (controller.signal.aborted || this.#disposed || this.backend.identity.sessionGeneration !== generation)
|
|
677
738
|
return rejected("image submission canceled");
|
|
678
739
|
return action();
|
|
679
740
|
}).finally(() => {
|
|
@@ -707,7 +768,7 @@ export class OwnedUiSessionShell {
|
|
|
707
768
|
this.#promptHistory.rememberRecovery(text);
|
|
708
769
|
else {
|
|
709
770
|
this.#persistImageSidecars(text);
|
|
710
|
-
this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.sessionId);
|
|
771
|
+
this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.identity.sessionId);
|
|
711
772
|
}
|
|
712
773
|
}
|
|
713
774
|
}
|
|
@@ -738,7 +799,7 @@ export class OwnedUiSessionShell {
|
|
|
738
799
|
if (command) {
|
|
739
800
|
this.#rememberInput(displayInput, "bash");
|
|
740
801
|
try {
|
|
741
|
-
const result = await this.backend.executeBashWorkflow(command, excludeFromContext);
|
|
802
|
+
const result = await this.backend.workflows.executeBashWorkflow(command, excludeFromContext);
|
|
742
803
|
const workflow = {
|
|
743
804
|
command: "debug",
|
|
744
805
|
outcome: result.cancelled ? "cancelled" : result.exitCode === 0 || result.exitCode === undefined ? "completed" : "failed",
|
|
@@ -767,7 +828,7 @@ export class OwnedUiSessionShell {
|
|
|
767
828
|
return await this.#execute({
|
|
768
829
|
type,
|
|
769
830
|
correlationId: this.#correlation(type),
|
|
770
|
-
sessionId: this.backend.sessionId,
|
|
831
|
+
sessionId: this.backend.identity.sessionId,
|
|
771
832
|
text: input,
|
|
772
833
|
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
773
834
|
}, displayInput);
|
|
@@ -800,7 +861,7 @@ export class OwnedUiSessionShell {
|
|
|
800
861
|
// Compatibility: match Pi: with an empty editor, two escapes inside 500 ms open the
|
|
801
862
|
// configured session navigator. The first escape intentionally does not
|
|
802
863
|
// interrupt or mutate the prompt.
|
|
803
|
-
const action = this.backend.
|
|
864
|
+
const action = this.backend.settings.snapshot().doubleEscapeAction;
|
|
804
865
|
if (action === "none")
|
|
805
866
|
return { outcome: "rejected", diagnostic: "nothing to interrupt" };
|
|
806
867
|
if (now - this.#lastEscapeTime < 500) {
|
|
@@ -834,7 +895,7 @@ export class OwnedUiSessionShell {
|
|
|
834
895
|
return this.#execute({
|
|
835
896
|
type: "resume-session",
|
|
836
897
|
correlationId: this.#correlation("resume"),
|
|
837
|
-
sessionId: this.backend.sessionId,
|
|
898
|
+
sessionId: this.backend.identity.sessionId,
|
|
838
899
|
sessionPath,
|
|
839
900
|
});
|
|
840
901
|
}
|
|
@@ -843,7 +904,7 @@ export class OwnedUiSessionShell {
|
|
|
843
904
|
return this.#execute({
|
|
844
905
|
type: "set-model",
|
|
845
906
|
correlationId: this.#correlation("model"),
|
|
846
|
-
sessionId: this.backend.sessionId,
|
|
907
|
+
sessionId: this.backend.identity.sessionId,
|
|
847
908
|
model: { providerId, modelId, displayName: modelId },
|
|
848
909
|
});
|
|
849
910
|
}
|
|
@@ -851,7 +912,7 @@ export class OwnedUiSessionShell {
|
|
|
851
912
|
return this.#execute({
|
|
852
913
|
type: "set-thinking-level",
|
|
853
914
|
correlationId: this.#correlation("thinking"),
|
|
854
|
-
sessionId: this.backend.sessionId,
|
|
915
|
+
sessionId: this.backend.identity.sessionId,
|
|
855
916
|
thinkingLevel,
|
|
856
917
|
});
|
|
857
918
|
}
|
|
@@ -861,7 +922,7 @@ export class OwnedUiSessionShell {
|
|
|
861
922
|
return this.setThinkingLevel(levels[(current + 1) % levels.length] ?? "off");
|
|
862
923
|
}
|
|
863
924
|
async cycleModel(direction) {
|
|
864
|
-
const result = await this.backend.cycleModelWorkflow(direction);
|
|
925
|
+
const result = await this.backend.workflows.cycleModelWorkflow(direction);
|
|
865
926
|
this.root.appendWorkflowResult(result);
|
|
866
927
|
this.runtime.requestRender();
|
|
867
928
|
return workflowAdapterResult(result);
|
|
@@ -884,13 +945,13 @@ export class OwnedUiSessionShell {
|
|
|
884
945
|
return this.#execute({
|
|
885
946
|
type: "follow-up",
|
|
886
947
|
correlationId: this.#correlation("follow-up"),
|
|
887
|
-
sessionId: this.backend.sessionId,
|
|
948
|
+
sessionId: this.backend.identity.sessionId,
|
|
888
949
|
text,
|
|
889
950
|
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
890
951
|
}, displayInput);
|
|
891
952
|
}
|
|
892
953
|
restoreQueuedInput() {
|
|
893
|
-
const queued = [...this.#waitingImages.keys(), ...this.backend.clearQueuedWorkflows()];
|
|
954
|
+
const queued = [...this.#waitingImages.keys(), ...this.backend.workflows.clearQueuedWorkflows()];
|
|
894
955
|
this.#cancelWaitingImages();
|
|
895
956
|
if (queued.length === 0)
|
|
896
957
|
return;
|
|
@@ -926,6 +987,24 @@ export class OwnedUiSessionShell {
|
|
|
926
987
|
this.#dialogHandle = handle;
|
|
927
988
|
return handle;
|
|
928
989
|
}
|
|
990
|
+
// Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
|
|
991
|
+
#createDialogHost() {
|
|
992
|
+
const isDisposed = () => this.#disposed;
|
|
993
|
+
return {
|
|
994
|
+
get disposed() { return isDisposed(); },
|
|
995
|
+
setInputSurface: surface => this.root.setInputSurface(surface),
|
|
996
|
+
requestRender: () => this.runtime.requestRender(),
|
|
997
|
+
viewport: () => this.runtime.viewport(),
|
|
998
|
+
appendWorkflowStatus: text => this.root.appendWorkflowStatus(text),
|
|
999
|
+
appendWorkflowResult: result => this.root.appendWorkflowResult(result),
|
|
1000
|
+
runWorkflow: request => this.runWorkflow(request),
|
|
1001
|
+
// Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
|
|
1002
|
+
// fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
|
|
1003
|
+
...(this.#customViewport
|
|
1004
|
+
? { beginPresentationHold: () => this.runtime.beginPresentationHold() }
|
|
1005
|
+
: { switchTuiMode: mode => this.runtime.switchMode(mode) }),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
929
1008
|
#loadLazySelectors() {
|
|
930
1009
|
if (this.#lazySelectors !== undefined)
|
|
931
1010
|
return Promise.resolve(this.#lazySelectors);
|
|
@@ -935,7 +1014,7 @@ export class OwnedUiSessionShell {
|
|
|
935
1014
|
});
|
|
936
1015
|
}
|
|
937
1016
|
async showThinkingSelector() {
|
|
938
|
-
const snapshot = this.backend.
|
|
1017
|
+
const snapshot = this.backend.settings.snapshot();
|
|
939
1018
|
const close = () => {
|
|
940
1019
|
this.root.setFooterLevel(true);
|
|
941
1020
|
this.root.setInputSurface(null);
|
|
@@ -953,7 +1032,7 @@ export class OwnedUiSessionShell {
|
|
|
953
1032
|
availableLevels: snapshot.availableThinkingLevels,
|
|
954
1033
|
onSelect: select,
|
|
955
1034
|
onCancel: close,
|
|
956
|
-
onSelectAsDefault: level => this.backend.setDefaultThinkingLevel(level),
|
|
1035
|
+
onSelectAsDefault: level => this.backend.settings.setDefaultThinkingLevel(level),
|
|
957
1036
|
defaultLevel: snapshot.defaultThinkingLevel,
|
|
958
1037
|
...(this.#customViewport ? {
|
|
959
1038
|
presentation: {
|
|
@@ -990,34 +1069,10 @@ export class OwnedUiSessionShell {
|
|
|
990
1069
|
this.runtime.requestRender();
|
|
991
1070
|
}
|
|
992
1071
|
showModelSelector(initialSearchInput) {
|
|
993
|
-
|
|
994
|
-
const close = () => {
|
|
995
|
-
this.root.setInputSurface(null);
|
|
996
|
-
this.runtime.requestRender();
|
|
997
|
-
};
|
|
998
|
-
const component = createPiShellModelSelector({
|
|
999
|
-
...context,
|
|
1000
|
-
runtime: {
|
|
1001
|
-
getColumns: () => this.runtime.viewport().columns,
|
|
1002
|
-
getRows: () => this.runtime.viewport().rows,
|
|
1003
|
-
requestRender: () => this.runtime.requestRender(),
|
|
1004
|
-
},
|
|
1005
|
-
...(initialSearchInput === undefined ? {} : { initialSearchInput }),
|
|
1006
|
-
onSelect: model => {
|
|
1007
|
-
close();
|
|
1008
|
-
void this.runWorkflow({ command: "model", argument: "", selection: modelReference(model) });
|
|
1009
|
-
},
|
|
1010
|
-
onSelectAsDefault: model => {
|
|
1011
|
-
close();
|
|
1012
|
-
void this.runWorkflow({ command: "model", argument: "", selection: modelReference(model), persist: true });
|
|
1013
|
-
},
|
|
1014
|
-
onCancel: close,
|
|
1015
|
-
});
|
|
1016
|
-
this.root.setInputSurface(component);
|
|
1017
|
-
this.runtime.requestRender();
|
|
1072
|
+
this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
|
|
1018
1073
|
}
|
|
1019
1074
|
showForkSelector() {
|
|
1020
|
-
const options = this.backend.pinnedForkOptions();
|
|
1075
|
+
const options = this.backend.catalog.pinnedForkOptions();
|
|
1021
1076
|
if (options.length === 0) {
|
|
1022
1077
|
this.root.appendWorkflowResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
|
|
1023
1078
|
this.runtime.requestRender();
|
|
@@ -1037,7 +1092,7 @@ export class OwnedUiSessionShell {
|
|
|
1037
1092
|
async showLogoutSelector() {
|
|
1038
1093
|
let options;
|
|
1039
1094
|
try {
|
|
1040
|
-
options = await this.backend.pinnedLogoutOptions();
|
|
1095
|
+
options = await this.backend.catalog.pinnedLogoutOptions();
|
|
1041
1096
|
}
|
|
1042
1097
|
catch (error) {
|
|
1043
1098
|
this.root.appendWorkflowResult({
|
|
@@ -1065,7 +1120,7 @@ export class OwnedUiSessionShell {
|
|
|
1065
1120
|
this.runtime.requestRender();
|
|
1066
1121
|
}
|
|
1067
1122
|
showLoginMethodSelector(providerReference) {
|
|
1068
|
-
const method = this.backend.pinnedLoginMethodOptions(providerReference);
|
|
1123
|
+
const method = this.backend.catalog.pinnedLoginMethodOptions(providerReference);
|
|
1069
1124
|
if (method.options.length === 0) {
|
|
1070
1125
|
this.showLoginProviderSelector(undefined, providerReference);
|
|
1071
1126
|
return;
|
|
@@ -1091,67 +1146,10 @@ export class OwnedUiSessionShell {
|
|
|
1091
1146
|
this.root.setInputSurface(component);
|
|
1092
1147
|
this.runtime.requestRender();
|
|
1093
1148
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
this.runtime.requestRender();
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const close = () => {
|
|
1102
|
-
this.root.setInputSurface(null);
|
|
1103
|
-
this.runtime.requestRender();
|
|
1104
|
-
};
|
|
1105
|
-
const releasePresentation = this.#customViewport ? this.runtime.beginPresentationHold() : undefined;
|
|
1106
|
-
try {
|
|
1107
|
-
const selectors = await this.#loadLazySelectors();
|
|
1108
|
-
const component = await selectors.createTree({
|
|
1109
|
-
tree: context.tree,
|
|
1110
|
-
currentLeafId: context.currentLeafId,
|
|
1111
|
-
terminalHeight: this.runtime.viewport().rows,
|
|
1112
|
-
initialFilterMode: context.filterMode,
|
|
1113
|
-
...(initialSelectedId === undefined ? {} : { initialSelectedId }),
|
|
1114
|
-
onLabelChange: context.appendLabelChange,
|
|
1115
|
-
onCopy: text => {
|
|
1116
|
-
if (!text) {
|
|
1117
|
-
this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: "Selected entry has no text to copy" });
|
|
1118
|
-
this.runtime.requestRender();
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
const generation = this.backend.sessionBindingGeneration;
|
|
1122
|
-
void this.backend.copyWorkflowText(text).then(acknowledged => {
|
|
1123
|
-
if (this.#disposed || generation !== this.backend.sessionBindingGeneration)
|
|
1124
|
-
return;
|
|
1125
|
-
this.root.appendWorkflowStatus(acknowledged ? "Copied selected message to clipboard" : "Submitted selected message to clipboard");
|
|
1126
|
-
this.runtime.requestRender();
|
|
1127
|
-
}).catch(error => {
|
|
1128
|
-
if (this.#disposed || generation !== this.backend.sessionBindingGeneration)
|
|
1129
|
-
return;
|
|
1130
|
-
this.root.appendWorkflowResult({ command: "tree", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
1131
|
-
this.runtime.requestRender();
|
|
1132
|
-
});
|
|
1133
|
-
},
|
|
1134
|
-
onCancel: close,
|
|
1135
|
-
onSelect: entryId => {
|
|
1136
|
-
if (entryId === context.currentLeafId) {
|
|
1137
|
-
close();
|
|
1138
|
-
this.root.appendWorkflowStatus("Already at this point");
|
|
1139
|
-
this.runtime.requestRender();
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
if (context.skipSummaryPrompt)
|
|
1143
|
-
close();
|
|
1144
|
-
void this.#completeTreeSelection(entryId, context.skipSummaryPrompt);
|
|
1145
|
-
},
|
|
1146
|
-
});
|
|
1147
|
-
if (this.#disposed)
|
|
1148
|
-
return;
|
|
1149
|
-
this.root.setInputSurface(component);
|
|
1150
|
-
this.runtime.requestRender();
|
|
1151
|
-
}
|
|
1152
|
-
finally {
|
|
1153
|
-
releasePresentation?.();
|
|
1154
|
-
}
|
|
1149
|
+
showTreeSelector(initialSelectedId) {
|
|
1150
|
+
return this.#presenters.openTreeSelector(this.#dialogHost, (entryId, skipSummaryPrompt) => {
|
|
1151
|
+
void this.#completeTreeSelection(entryId, skipSummaryPrompt);
|
|
1152
|
+
}, initialSelectedId);
|
|
1155
1153
|
}
|
|
1156
1154
|
showLoginAuthTypeSelector() {
|
|
1157
1155
|
const close = () => {
|
|
@@ -1167,7 +1165,7 @@ export class OwnedUiSessionShell {
|
|
|
1167
1165
|
this.runtime.requestRender();
|
|
1168
1166
|
}
|
|
1169
1167
|
showLoginProviderSelector(authType, initialSearchInput) {
|
|
1170
|
-
const options = this.backend.pinnedLoginOptions(authType);
|
|
1168
|
+
const options = this.backend.catalog.pinnedLoginOptions(authType);
|
|
1171
1169
|
if (options.length === 0) {
|
|
1172
1170
|
this.root.appendWorkflowStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
|
|
1173
1171
|
this.runtime.requestRender();
|
|
@@ -1188,82 +1186,11 @@ export class OwnedUiSessionShell {
|
|
|
1188
1186
|
this.root.setInputSurface(component);
|
|
1189
1187
|
this.runtime.requestRender();
|
|
1190
1188
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const close = () => {
|
|
1194
|
-
this.root.setInputSurface(null);
|
|
1195
|
-
this.runtime.requestRender();
|
|
1196
|
-
};
|
|
1197
|
-
const component = await createPiShellSessionSelector({
|
|
1198
|
-
currentSessionsLoader: context.loadCurrentSessions,
|
|
1199
|
-
allSessionsLoader: context.loadAllSessions,
|
|
1200
|
-
currentSessionFilePath: context.currentSessionFilePath,
|
|
1201
|
-
renameSession: context.renameSession,
|
|
1202
|
-
requestRender: () => this.runtime.requestRender(),
|
|
1203
|
-
onSelect: sessionPath => {
|
|
1204
|
-
close();
|
|
1205
|
-
void this.runWorkflow({ command: "resume", argument: sessionPath });
|
|
1206
|
-
},
|
|
1207
|
-
onCancel: close,
|
|
1208
|
-
onExit: () => {
|
|
1209
|
-
close();
|
|
1210
|
-
void this.shutdown();
|
|
1211
|
-
},
|
|
1212
|
-
});
|
|
1213
|
-
this.root.setInputSurface(component);
|
|
1214
|
-
this.runtime.requestRender();
|
|
1189
|
+
showSessionSelector() {
|
|
1190
|
+
return this.#presenters.openSessionSelector(this.#dialogHost, () => { void this.shutdown(); });
|
|
1215
1191
|
}
|
|
1216
1192
|
showSettingsSelector() {
|
|
1217
|
-
|
|
1218
|
-
const close = () => {
|
|
1219
|
-
this.root.setInputSurface(null);
|
|
1220
|
-
this.runtime.requestRender();
|
|
1221
|
-
};
|
|
1222
|
-
const { currentModel, ...settingsSnapshot } = snapshot;
|
|
1223
|
-
const component = createPiShellSettingsSelector({
|
|
1224
|
-
config: {
|
|
1225
|
-
...settingsSnapshot,
|
|
1226
|
-
availableThinkingLevels: [...snapshot.availableThinkingLevels],
|
|
1227
|
-
availableThemes: [...snapshot.availableThemes],
|
|
1228
|
-
warnings: { ...snapshot.warnings },
|
|
1229
|
-
modelThinkingLevels: { ...snapshot.modelThinkingLevels },
|
|
1230
|
-
...(currentModel === undefined ? {} : { currentModel: currentModel }),
|
|
1231
|
-
availableDefaultModels: snapshot.availableDefaultModels,
|
|
1232
|
-
},
|
|
1233
|
-
onChange: (callback, value) => {
|
|
1234
|
-
if (callback === "onCancel") {
|
|
1235
|
-
close();
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
if (callback === "onTuiModeChange") {
|
|
1239
|
-
// Invariant: the custom bare-A1 surface is permanently fullscreen; this callback
|
|
1240
|
-
// remains available only to pinned comparison profiles.
|
|
1241
|
-
if (this.#customViewport)
|
|
1242
|
-
return;
|
|
1243
|
-
if (value !== "regular" && value !== "fullscreen")
|
|
1244
|
-
return;
|
|
1245
|
-
if (!this.runtime.switchMode(value)) {
|
|
1246
|
-
this.root.appendWorkflowStatus("Close active overlays before changing TUI mode");
|
|
1247
|
-
this.runtime.requestRender();
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
if (this.#customViewport) {
|
|
1251
|
-
this.#pointerReporting = false;
|
|
1252
|
-
this.#setPointerReporting(true);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
void this.backend.applyPinnedSettingValue(callback, value).then(result => {
|
|
1256
|
-
if (result.outcome === "failed")
|
|
1257
|
-
this.root.appendWorkflowResult(result);
|
|
1258
|
-
else if (callback === "onTuiModeChange")
|
|
1259
|
-
this.root.appendWorkflowStatus(`TUI mode: ${value}`);
|
|
1260
|
-
this.runtime.requestRender();
|
|
1261
|
-
});
|
|
1262
|
-
},
|
|
1263
|
-
onCancel: close,
|
|
1264
|
-
});
|
|
1265
|
-
this.root.setInputSurface(component);
|
|
1266
|
-
this.runtime.requestRender();
|
|
1193
|
+
this.#presenters.openSettingsSelector(this.#dialogHost);
|
|
1267
1194
|
}
|
|
1268
1195
|
shutdown() {
|
|
1269
1196
|
this.#shutdownPromise ??= this.#shutdown();
|
|
@@ -1271,7 +1198,13 @@ export class OwnedUiSessionShell {
|
|
|
1271
1198
|
}
|
|
1272
1199
|
async #shutdown() {
|
|
1273
1200
|
try {
|
|
1274
|
-
const
|
|
1201
|
+
const quit = this.backend.workflows.executeWorkflow({ command: "quit", argument: "" });
|
|
1202
|
+
// Invariant: a slow or hung engine quit never holds the terminal past the cleanup deadline. The
|
|
1203
|
+
// shell restores it then, and the result still waits for the engine to finish quitting.
|
|
1204
|
+
const timedOut = await boundedCleanup(() => quit.catch(() => { })).then(() => false, () => true);
|
|
1205
|
+
if (timedOut)
|
|
1206
|
+
await this.dispose();
|
|
1207
|
+
const result = await quit;
|
|
1275
1208
|
await this.dispose();
|
|
1276
1209
|
return workflowAdapterResult(result);
|
|
1277
1210
|
}
|
|
@@ -1295,9 +1228,9 @@ export class OwnedUiSessionShell {
|
|
|
1295
1228
|
});
|
|
1296
1229
|
return { outcome: "completed", diagnostic: null };
|
|
1297
1230
|
}
|
|
1298
|
-
const copyGeneration = request.command === "copy" ? this.backend.sessionBindingGeneration : undefined;
|
|
1231
|
+
const copyGeneration = request.command === "copy" ? this.backend.identity.sessionBindingGeneration : undefined;
|
|
1299
1232
|
if (request.command === "login" && request.selection !== undefined) {
|
|
1300
|
-
const setup = this.backend.pinnedAmbientAuthentication(request.selection);
|
|
1233
|
+
const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
|
|
1301
1234
|
if (setup) {
|
|
1302
1235
|
const close = () => {
|
|
1303
1236
|
this.root.setInputSurface(null);
|
|
@@ -1362,7 +1295,7 @@ export class OwnedUiSessionShell {
|
|
|
1362
1295
|
return { outcome: "completed", diagnostic: null };
|
|
1363
1296
|
}
|
|
1364
1297
|
if (request.command === "reload") {
|
|
1365
|
-
const blocked = this.backend.reloadBlockedResult();
|
|
1298
|
+
const blocked = this.backend.workflows.reloadBlockedResult();
|
|
1366
1299
|
if (blocked) {
|
|
1367
1300
|
this.root.appendWorkflowResult(blocked);
|
|
1368
1301
|
this.runtime.requestRender();
|
|
@@ -1388,7 +1321,7 @@ export class OwnedUiSessionShell {
|
|
|
1388
1321
|
}
|
|
1389
1322
|
let result;
|
|
1390
1323
|
try {
|
|
1391
|
-
result = await this.backend.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
|
|
1324
|
+
result = await this.backend.workflows.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
|
|
1392
1325
|
}
|
|
1393
1326
|
finally {
|
|
1394
1327
|
if (operationSurface) {
|
|
@@ -1400,7 +1333,7 @@ export class OwnedUiSessionShell {
|
|
|
1400
1333
|
this.runtime.requestRender();
|
|
1401
1334
|
}
|
|
1402
1335
|
}
|
|
1403
|
-
if (copyGeneration !== undefined && (this.#disposed || copyGeneration !== this.backend.sessionBindingGeneration)) {
|
|
1336
|
+
if (copyGeneration !== undefined && (this.#disposed || copyGeneration !== this.backend.identity.sessionBindingGeneration)) {
|
|
1404
1337
|
return workflowAdapterResult({ command: "copy", outcome: "cancelled", message: "" });
|
|
1405
1338
|
}
|
|
1406
1339
|
if (result.outcome === "requires-selection" && request.command === "model") {
|
|
@@ -1457,7 +1390,7 @@ export class OwnedUiSessionShell {
|
|
|
1457
1390
|
await sleep(remaining);
|
|
1458
1391
|
}
|
|
1459
1392
|
showTrustSelector() {
|
|
1460
|
-
const context = this.backend.pinnedProjectTrustContext();
|
|
1393
|
+
const context = this.backend.catalog.pinnedProjectTrustContext();
|
|
1461
1394
|
const close = () => {
|
|
1462
1395
|
this.root.setInputSurface(null);
|
|
1463
1396
|
this.runtime.requestRender();
|
|
@@ -1466,7 +1399,7 @@ export class OwnedUiSessionShell {
|
|
|
1466
1399
|
...context,
|
|
1467
1400
|
onSelect: selection => {
|
|
1468
1401
|
try {
|
|
1469
|
-
this.backend.persistProjectTrust(selection.updates);
|
|
1402
|
+
this.backend.catalog.persistProjectTrust(selection.updates);
|
|
1470
1403
|
close();
|
|
1471
1404
|
this.root.appendWorkflowStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
|
|
1472
1405
|
}
|
|
@@ -1482,157 +1415,16 @@ export class OwnedUiSessionShell {
|
|
|
1482
1415
|
this.runtime.requestRender();
|
|
1483
1416
|
}
|
|
1484
1417
|
showScopedModelsSelector() {
|
|
1485
|
-
|
|
1486
|
-
let currentEnabledIds = initial.enabledModelIds === null ? null : [...initial.enabledModelIds];
|
|
1487
|
-
let selectionChanged = false;
|
|
1488
|
-
let disposed = false;
|
|
1489
|
-
let timedOut = false;
|
|
1490
|
-
const controller = new AbortController();
|
|
1491
|
-
const timeout = setTimeout(() => {
|
|
1492
|
-
timedOut = true;
|
|
1493
|
-
controller.abort();
|
|
1494
|
-
}, 15_000);
|
|
1495
|
-
const close = () => {
|
|
1496
|
-
disposed = true;
|
|
1497
|
-
clearTimeout(timeout);
|
|
1498
|
-
controller.abort();
|
|
1499
|
-
this.root.setInputSurface(null);
|
|
1500
|
-
this.runtime.requestRender();
|
|
1501
|
-
};
|
|
1502
|
-
const selector = createPiShellScopedModelsSelector({
|
|
1503
|
-
models: initial.models,
|
|
1504
|
-
enabledModelIds: currentEnabledIds,
|
|
1505
|
-
refreshStatus: "Refreshing model catalogs…",
|
|
1506
|
-
onChange: enabledIds => {
|
|
1507
|
-
selectionChanged = true;
|
|
1508
|
-
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
|
1509
|
-
this.backend.updateScopedModels(currentEnabledIds);
|
|
1510
|
-
this.runtime.requestRender();
|
|
1511
|
-
},
|
|
1512
|
-
onPersist: enabledIds => {
|
|
1513
|
-
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
|
1514
|
-
this.backend.persistScopedModels(currentEnabledIds);
|
|
1515
|
-
this.root.appendWorkflowStatus("Model selection saved to settings");
|
|
1516
|
-
this.runtime.requestRender();
|
|
1517
|
-
},
|
|
1518
|
-
onCancel: close,
|
|
1519
|
-
});
|
|
1520
|
-
const component = {
|
|
1521
|
-
...selector,
|
|
1522
|
-
dispose: () => {
|
|
1523
|
-
disposed = true;
|
|
1524
|
-
clearTimeout(timeout);
|
|
1525
|
-
controller.abort();
|
|
1526
|
-
selector.dispose?.();
|
|
1527
|
-
},
|
|
1528
|
-
};
|
|
1529
|
-
this.root.setInputSurface(component);
|
|
1530
|
-
this.runtime.requestRender();
|
|
1531
|
-
void this.backend.refreshScopedModels(controller.signal).then(refreshed => {
|
|
1532
|
-
if (disposed)
|
|
1533
|
-
return;
|
|
1534
|
-
if (!selectionChanged) {
|
|
1535
|
-
currentEnabledIds = refreshed.enabledModelIds === null ? null : [...refreshed.enabledModelIds];
|
|
1536
|
-
component.updateModels(refreshed.models, currentEnabledIds);
|
|
1537
|
-
}
|
|
1538
|
-
else {
|
|
1539
|
-
component.updateModels(refreshed.models);
|
|
1540
|
-
this.backend.updateScopedModels(currentEnabledIds);
|
|
1541
|
-
}
|
|
1542
|
-
component.setRefreshStatus(timedOut ? "Model refresh timed out; showing cached models." : refreshed.status, timedOut ? "warning" : refreshed.statusKind);
|
|
1543
|
-
this.runtime.requestRender();
|
|
1544
|
-
}).catch(error => {
|
|
1545
|
-
if (disposed)
|
|
1546
|
-
return;
|
|
1547
|
-
component.setRefreshStatus(timedOut
|
|
1548
|
-
? "Model refresh timed out; showing cached models."
|
|
1549
|
-
: `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
1550
|
-
this.runtime.requestRender();
|
|
1551
|
-
}).finally(() => clearTimeout(timeout));
|
|
1418
|
+
this.#presenters.openScopedModelsSelector(this.#dialogHost);
|
|
1552
1419
|
}
|
|
1553
1420
|
// Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
|
|
1554
1421
|
#isWorkflowRoute(value) {
|
|
1555
1422
|
return workflowCommandNames(this.#customViewport ? "bare" : "comparison").includes(value)
|
|
1556
|
-
||
|
|
1423
|
+
|| OWNED_UI_HIDDEN_COMMAND_NAMES.includes(value);
|
|
1557
1424
|
}
|
|
1558
1425
|
/** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
|
|
1559
1426
|
showModelsDialog(initialQuery) {
|
|
1560
|
-
|
|
1561
|
-
const available = new Set(context.models.map(model => `${model.provider}/${model.id}`));
|
|
1562
|
-
const savedScopeIds = context.persistedScopeIds.filter(id => available.has(id));
|
|
1563
|
-
// Invariant: an explicit session scope wins; otherwise the dialog starts from what is persisted, never from "all rows scoped".
|
|
1564
|
-
const scopeIds = context.sessionScopeIds.length > 0 ? context.sessionScopeIds : savedScopeIds;
|
|
1565
|
-
let disposed = false;
|
|
1566
|
-
let timedOut = false;
|
|
1567
|
-
const controller = new AbortController();
|
|
1568
|
-
const timeout = setTimeout(() => {
|
|
1569
|
-
timedOut = true;
|
|
1570
|
-
controller.abort();
|
|
1571
|
-
}, 15_000);
|
|
1572
|
-
const close = () => {
|
|
1573
|
-
disposed = true;
|
|
1574
|
-
clearTimeout(timeout);
|
|
1575
|
-
controller.abort();
|
|
1576
|
-
this.root.setInputSurface(null);
|
|
1577
|
-
this.runtime.requestRender();
|
|
1578
|
-
};
|
|
1579
|
-
const dialog = createPiShellModelsDialog({
|
|
1580
|
-
models: context.models,
|
|
1581
|
-
activeModelId: context.activeModelId,
|
|
1582
|
-
scopeIds,
|
|
1583
|
-
savedScopeIds,
|
|
1584
|
-
...(initialQuery === undefined ? {} : { initialQuery }),
|
|
1585
|
-
refreshStatus: "Refreshing model catalogs…",
|
|
1586
|
-
requestRender: () => this.runtime.requestRender(),
|
|
1587
|
-
onSelect: modelId => {
|
|
1588
|
-
void this.runWorkflow({ command: "models", argument: "", selection: modelId }).then(result => {
|
|
1589
|
-
if (!disposed && result.outcome === "completed")
|
|
1590
|
-
close();
|
|
1591
|
-
});
|
|
1592
|
-
},
|
|
1593
|
-
onScopeChange: ids => {
|
|
1594
|
-
this.backend.setSessionModelScope(ids);
|
|
1595
|
-
this.runtime.requestRender();
|
|
1596
|
-
},
|
|
1597
|
-
onSave: ids => {
|
|
1598
|
-
try {
|
|
1599
|
-
this.backend.persistModelScope(ids);
|
|
1600
|
-
}
|
|
1601
|
-
catch (error) {
|
|
1602
|
-
this.root.appendWorkflowResult({ command: "models", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
1603
|
-
this.runtime.requestRender();
|
|
1604
|
-
throw error;
|
|
1605
|
-
}
|
|
1606
|
-
this.root.appendWorkflowStatus("Model selection saved to settings");
|
|
1607
|
-
this.runtime.requestRender();
|
|
1608
|
-
},
|
|
1609
|
-
onCancel: close,
|
|
1610
|
-
});
|
|
1611
|
-
const component = {
|
|
1612
|
-
...dialog,
|
|
1613
|
-
dispose: () => {
|
|
1614
|
-
disposed = true;
|
|
1615
|
-
clearTimeout(timeout);
|
|
1616
|
-
controller.abort();
|
|
1617
|
-
dialog.dispose?.();
|
|
1618
|
-
},
|
|
1619
|
-
};
|
|
1620
|
-
this.root.setInputSurface(component);
|
|
1621
|
-
this.runtime.requestRender();
|
|
1622
|
-
void this.backend.refreshModels(controller.signal).then(refreshed => {
|
|
1623
|
-
if (disposed)
|
|
1624
|
-
return;
|
|
1625
|
-
component.updateModels(refreshed.models);
|
|
1626
|
-
component.setRefreshStatus(timedOut ? "Model refresh timed out; showing cached models." : refreshed.status, timedOut ? "warning" : refreshed.statusKind);
|
|
1627
|
-
this.runtime.requestRender();
|
|
1628
|
-
}).catch(error => {
|
|
1629
|
-
if (disposed)
|
|
1630
|
-
return;
|
|
1631
|
-
component.setRefreshStatus(timedOut
|
|
1632
|
-
? "Model refresh timed out; showing cached models."
|
|
1633
|
-
: `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
1634
|
-
this.runtime.requestRender();
|
|
1635
|
-
}).finally(() => clearTimeout(timeout));
|
|
1427
|
+
this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
|
|
1636
1428
|
}
|
|
1637
1429
|
// Invariant: pointer reporting is disabled on every path that ends the owning screen.
|
|
1638
1430
|
#setPointerReporting(enabled, forceOff = false) {
|
|
@@ -1644,11 +1436,14 @@ export class OwnedUiSessionShell {
|
|
|
1644
1436
|
return;
|
|
1645
1437
|
this.runtime.writeControl(effective ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
1646
1438
|
}
|
|
1439
|
+
// Invariant: one teardown per shell, starting on a microtask so never inside the event listener that
|
|
1440
|
+
// requested it. Callers during the teardown share its outcome; a later call finds it done.
|
|
1647
1441
|
dispose() {
|
|
1648
1442
|
if (this.#disposePromise !== undefined)
|
|
1649
1443
|
return this.#disposePromise;
|
|
1650
1444
|
if (this.#disposed)
|
|
1651
1445
|
return Promise.resolve();
|
|
1446
|
+
this.#disposed = true;
|
|
1652
1447
|
const pending = Promise.resolve().then(() => this.#dispose());
|
|
1653
1448
|
this.#disposePromise = pending;
|
|
1654
1449
|
const clear = () => { if (this.#disposePromise === pending)
|
|
@@ -1657,13 +1452,6 @@ export class OwnedUiSessionShell {
|
|
|
1657
1452
|
return pending;
|
|
1658
1453
|
}
|
|
1659
1454
|
async #dispose() {
|
|
1660
|
-
this.#disposed = true;
|
|
1661
|
-
this.#copyIntentSequence += 1;
|
|
1662
|
-
// Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
|
|
1663
|
-
const outroFrame = this.#captureQuitOutroFrame();
|
|
1664
|
-
this.#responseCopy?.dispose();
|
|
1665
|
-
this.#copyExecutor?.dispose();
|
|
1666
|
-
this.#cancelWaitingImages();
|
|
1667
1455
|
const failures = [];
|
|
1668
1456
|
const attempt = (action) => { try {
|
|
1669
1457
|
action();
|
|
@@ -1671,6 +1459,14 @@ export class OwnedUiSessionShell {
|
|
|
1671
1459
|
catch (error) {
|
|
1672
1460
|
failures.push(error);
|
|
1673
1461
|
} };
|
|
1462
|
+
// Invariant: events stop first, so nothing re-enters the view during history close, outro, or restoration.
|
|
1463
|
+
attempt(() => this.#unsubscribe());
|
|
1464
|
+
this.#copyIntentSequence += 1;
|
|
1465
|
+
// Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
|
|
1466
|
+
const outroFrame = this.#captureQuitOutroFrame();
|
|
1467
|
+
this.#responseCopy?.dispose();
|
|
1468
|
+
this.#copyExecutor?.dispose();
|
|
1469
|
+
this.#cancelWaitingImages();
|
|
1674
1470
|
let pasteCleanup = Promise.resolve();
|
|
1675
1471
|
let historyCleanup = Promise.resolve(true);
|
|
1676
1472
|
attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
|
|
@@ -1689,10 +1485,11 @@ export class OwnedUiSessionShell {
|
|
|
1689
1485
|
attempt(() => this.#unsubscribePromptImages());
|
|
1690
1486
|
attempt(() => this.#unsubscribeSkills());
|
|
1691
1487
|
attempt(() => this.#unsubscribeSettings());
|
|
1488
|
+
attempt(() => this.#unsubscribeBackgroundSettings());
|
|
1692
1489
|
let fullscreenExitText = "";
|
|
1693
1490
|
attempt(() => {
|
|
1694
|
-
const exitMode = this.backend.disposed ? this.#fullscreenExitOutput : this.backend.
|
|
1695
|
-
const resume = this.backend.currentSessionResumeMetadata();
|
|
1491
|
+
const exitMode = this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput;
|
|
1492
|
+
const resume = this.backend.identity.currentSessionResumeMetadata();
|
|
1696
1493
|
const resumeHint = resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
|
|
1697
1494
|
// Invariant: bare A1 leaves only the hint behind; the pinned comparison profile still
|
|
1698
1495
|
// honors fullscreenExitOutput, including the styled transcript.
|
|
@@ -1704,7 +1501,7 @@ export class OwnedUiSessionShell {
|
|
|
1704
1501
|
attempt(() => this.#unbindPiSettings());
|
|
1705
1502
|
attempt(() => this.#unbindTerminalSettings());
|
|
1706
1503
|
attempt(() => this.#unbindShutdownSettings());
|
|
1707
|
-
attempt(() => this
|
|
1504
|
+
attempt(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
|
|
1708
1505
|
attempt(() => this.#dialogHandle?.hide());
|
|
1709
1506
|
attempt(() => this.#extensionBridge.dispose());
|
|
1710
1507
|
attempt(() => this.#programStatus.clear());
|
|
@@ -1719,7 +1516,7 @@ export class OwnedUiSessionShell {
|
|
|
1719
1516
|
await this.runtime.dispose({ preserveScreen: this.runtime.mode === "fullscreen" }).catch(error => failures.push(error));
|
|
1720
1517
|
await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
|
|
1721
1518
|
await boundedCleanup(() => pasteCleanup).catch(error => failures.push(error));
|
|
1722
|
-
await boundedCleanup(() => this.backend.unbindExtensionUi()).catch(error => failures.push(error));
|
|
1519
|
+
await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => failures.push(error));
|
|
1723
1520
|
if (failures.length > 0)
|
|
1724
1521
|
throw new AggregateError(failures, "Owned UI disposal failed");
|
|
1725
1522
|
if (fullscreenExitText.length > 0)
|
|
@@ -1738,7 +1535,11 @@ export class OwnedUiSessionShell {
|
|
|
1738
1535
|
const viewport = this.runtime.viewport();
|
|
1739
1536
|
return {
|
|
1740
1537
|
rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows,
|
|
1741
|
-
settings: {
|
|
1538
|
+
settings: {
|
|
1539
|
+
effect: QUIT_OUTRO_EFFECT,
|
|
1540
|
+
durationMs: QUIT_OUTRO_DURATION_MS,
|
|
1541
|
+
canvasBackgroundAnsi: this.#damageTerminal.canvasBackgroundAnsi,
|
|
1542
|
+
},
|
|
1742
1543
|
};
|
|
1743
1544
|
}
|
|
1744
1545
|
catch {
|
|
@@ -1763,6 +1564,9 @@ export class OwnedUiSessionShell {
|
|
|
1763
1564
|
return;
|
|
1764
1565
|
await playQuitOutro(frame, capture.settings.effect, capture.settings.durationMs, {
|
|
1765
1566
|
write: data => this.runtime.writeControl(data),
|
|
1567
|
+
...(capture.settings.canvasBackgroundAnsi === null
|
|
1568
|
+
? {}
|
|
1569
|
+
: { canvasBackgroundAnsi: capture.settings.canvasBackgroundAnsi }),
|
|
1766
1570
|
...(outro.now === undefined ? {} : { now: outro.now }),
|
|
1767
1571
|
...(outro.sleep === undefined ? {} : { sleep: outro.sleep }),
|
|
1768
1572
|
...(outro.seed === undefined ? {} : { seed: outro.seed }),
|
|
@@ -1816,17 +1620,17 @@ export class OwnedUiSessionShell {
|
|
|
1816
1620
|
#syncView() {
|
|
1817
1621
|
this.#streamPresentation.noteImmediatePresentation();
|
|
1818
1622
|
const view = this.view();
|
|
1819
|
-
if (this.backend.sessionGeneration !== this.#sessionGeneration) {
|
|
1623
|
+
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration) {
|
|
1820
1624
|
this.#copyIntentSequence += 1;
|
|
1821
1625
|
this.#responseCopy?.reset();
|
|
1822
1626
|
this.#cancelWaitingImages();
|
|
1823
1627
|
this.root.resetPendingPastes();
|
|
1824
1628
|
this.#promptSuggestions?.invalidate();
|
|
1825
|
-
this.#sessionGeneration = this.backend.sessionGeneration;
|
|
1629
|
+
this.#sessionGeneration = this.backend.identity.sessionGeneration;
|
|
1826
1630
|
this.#programStatus.reset();
|
|
1827
1631
|
// Invariant: delivery recovery invalidates callbacks, not same-session local recall or its draft.
|
|
1828
|
-
if (this.#sessionBindingGeneration !== this.backend.sessionBindingGeneration) {
|
|
1829
|
-
this.#sessionBindingGeneration = this.backend.sessionBindingGeneration;
|
|
1632
|
+
if (this.#sessionBindingGeneration !== this.backend.identity.sessionBindingGeneration) {
|
|
1633
|
+
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
1830
1634
|
this.root.resetTranscript();
|
|
1831
1635
|
this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
|
|
1832
1636
|
}
|
|
@@ -2059,24 +1863,24 @@ export class OwnedUiSessionShell {
|
|
|
2059
1863
|
return this.#execute({
|
|
2060
1864
|
type: this.view().lifecycle === "busy" ? "steer" : "prompt",
|
|
2061
1865
|
correlationId: this.#correlation("prompt-command"),
|
|
2062
|
-
sessionId: this.backend.sessionId,
|
|
1866
|
+
sessionId: this.backend.identity.sessionId,
|
|
2063
1867
|
text,
|
|
2064
1868
|
});
|
|
2065
1869
|
}
|
|
2066
1870
|
#installAutocompleteCommands() {
|
|
2067
|
-
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
1871
|
+
this.root.editor.setAutocompleteCommands(this.backend.workflows.workflowAutocompleteCommands());
|
|
2068
1872
|
this.#installedCommandSignature = this.#commandListSignature();
|
|
2069
1873
|
}
|
|
2070
1874
|
#commandListSignature() {
|
|
2071
|
-
return JSON.stringify([this.#skills?.presentation() ?? "expand", this.backend.disposed ? [] : this.backend.workflowAutocompleteCommands().map(command => command.name)]);
|
|
1875
|
+
return JSON.stringify([this.#skills?.presentation() ?? "expand", this.backend.identity.disposed ? [] : this.backend.workflows.workflowAutocompleteCommands().map(command => command.name)]);
|
|
2072
1876
|
}
|
|
2073
1877
|
// Invariant: collapse applies only while the engine registers skill commands; otherwise there is nothing to collapse.
|
|
2074
1878
|
#skillsCollapsed() {
|
|
2075
1879
|
return this.#skills !== null && this.#skills.presentation() === "collapse"
|
|
2076
|
-
&& !this.backend.disposed && this.backend.
|
|
1880
|
+
&& !this.backend.identity.disposed && this.backend.settings.snapshot().enableSkillCommands;
|
|
2077
1881
|
}
|
|
2078
1882
|
#skillSummaries() {
|
|
2079
|
-
return skillsFromCommands(this.backend.workflowAutocompleteCommands());
|
|
1883
|
+
return skillsFromCommands(this.backend.workflows.workflowAutocompleteCommands());
|
|
2080
1884
|
}
|
|
2081
1885
|
// Protocol: bare "/skills" opens the dialog, "/skills <name> [args]" applies directly, and an unknown name is a command outcome.
|
|
2082
1886
|
async #runSkillsCommand(text, argument) {
|
|
@@ -2104,7 +1908,7 @@ export class OwnedUiSessionShell {
|
|
|
2104
1908
|
return this.#execute({
|
|
2105
1909
|
type: this.view().lifecycle === "busy" ? "steer" : "prompt",
|
|
2106
1910
|
correlationId: this.#correlation("prompt-command"),
|
|
2107
|
-
sessionId: this.backend.sessionId,
|
|
1911
|
+
sessionId: this.backend.identity.sessionId,
|
|
2108
1912
|
text: prompt,
|
|
2109
1913
|
});
|
|
2110
1914
|
}
|
|
@@ -2226,7 +2030,7 @@ export class OwnedUiSessionShell {
|
|
|
2226
2030
|
}
|
|
2227
2031
|
async #execute(command, draft) {
|
|
2228
2032
|
if (draft === undefined)
|
|
2229
|
-
return this.backend.execute(command);
|
|
2033
|
+
return this.backend.session.execute(command);
|
|
2230
2034
|
const revision = this.#editorRevision;
|
|
2231
2035
|
try {
|
|
2232
2036
|
assertOwnedUiCommand(command);
|
|
@@ -2235,7 +2039,7 @@ export class OwnedUiSessionShell {
|
|
|
2235
2039
|
return this.#recoverSubmission(draft, revision, error);
|
|
2236
2040
|
}
|
|
2237
2041
|
try {
|
|
2238
|
-
const result = await this.backend.execute(command);
|
|
2042
|
+
const result = await this.backend.session.execute(command);
|
|
2239
2043
|
if (result.outcome === "rejected")
|
|
2240
2044
|
return this.#recoverSubmission(draft, revision);
|
|
2241
2045
|
return result;
|
|
@@ -2281,7 +2085,7 @@ export class OwnedUiSessionShell {
|
|
|
2281
2085
|
catch { /* Security: error presentation cannot create another rejected submission callback. */ }
|
|
2282
2086
|
}
|
|
2283
2087
|
#simple(type) {
|
|
2284
|
-
return { type, correlationId: this.#correlation(type), sessionId: this.backend.sessionId };
|
|
2088
|
+
return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
|
|
2285
2089
|
}
|
|
2286
2090
|
#correlation(prefix) {
|
|
2287
2091
|
this.#sequence += 1;
|
|
@@ -2317,14 +2121,6 @@ export function quoteCommandArgument(value) {
|
|
|
2317
2121
|
function dim(value) {
|
|
2318
2122
|
return `\u001b[2m${value}\u001b[22m`;
|
|
2319
2123
|
}
|
|
2320
|
-
function modelReference(model) {
|
|
2321
|
-
if (typeof model !== "object" || model === null)
|
|
2322
|
-
return "";
|
|
2323
|
-
const value = model;
|
|
2324
|
-
const provider = typeof value.provider === "string" ? value.provider : "";
|
|
2325
|
-
const id = typeof value.id === "string" ? value.id : typeof value.modelId === "string" ? value.modelId : "";
|
|
2326
|
-
return provider && id ? `${provider}/${id}` : "";
|
|
2327
|
-
}
|
|
2328
2124
|
function rejected(diagnostic) {
|
|
2329
2125
|
return { outcome: "rejected", diagnostic };
|
|
2330
2126
|
}
|