@timurproko/a1 0.2.6-dev.727 → 0.2.6-dev.736
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 +4 -2
- package/dist/app/session-shell/index.js +2 -1
- 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 +29 -11
- package/dist/app/session-shell/session-shell-root.js +62 -53
- package/dist/app/session-shell/session-shell.d.ts +55 -29
- package/dist/app/session-shell/session-shell.js +748 -1132
- package/dist/app/session-shell/shell-host.d.ts +105 -0
- package/dist/app/session-shell/shell-host.js +523 -0
- package/dist/cli/session-context.js +6 -3
- package/dist/composition/owned-ui.d.ts +20 -2
- package/dist/composition/owned-ui.js +107 -56
- 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 +82 -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/launch/index.d.ts +1 -1
- package/dist/features/launch/index.js +1 -1
- package/dist/features/launch/session-context.d.ts +1 -0
- package/dist/features/launch/session-context.js +1 -0
- package/dist/features/owned-ui/settings-app.js +35 -15
- package/dist/foundation/lifecycle/index.d.ts +1 -1
- package/dist/foundation/lifecycle/index.js +1 -1
- package/dist/foundation/lifecycle/session-repository-context.d.ts +8 -2
- package/dist/foundation/lifecycle/session-repository-context.js +200 -65
- 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 +18 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +84 -3
- 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/docs/architecture/ui-reference-provenance.md +1 -1
- 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,34 +4,35 @@ 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
|
-
import {
|
|
11
|
-
import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
|
|
12
|
-
import { PINNED_PI_HIDDEN_COMMAND_NAMES, workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
|
|
10
|
+
import { workflowCommandNames } from "../../integrations/pi/engine/workflows.js";
|
|
13
11
|
import { createPiExtensionUiBridge } from "../../integrations/pi/components/shell-extension-ui.js";
|
|
14
12
|
import { ProgramStatusReporter } from "../../integrations/pi/components/upstream/program-status-reporter.js";
|
|
15
|
-
import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
|
|
16
13
|
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";
|
|
22
|
-
import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
|
|
23
|
-
import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
24
|
-
import { classifyPiTuiInput } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
14
|
+
import { createPiShellAuthProviderSelector, createPiShellDialog, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSelector, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
15
|
+
import { renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
16
|
+
import { OwnedUiTerminalHost, sessionTerminalHostOptions, } from "./shell-host.js";
|
|
25
17
|
import { runImageWorker } from "./image-preparation-client.js";
|
|
26
18
|
import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
|
|
27
19
|
import { writeSystemClipboardText, } from "./system-clipboard.js";
|
|
28
20
|
import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-root.js";
|
|
21
|
+
// Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
|
|
22
|
+
const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
|
|
29
23
|
export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
export { OwnedUiTerminalHost, sessionTerminalHostOptions } from "./shell-host.js";
|
|
25
|
+
/**
|
|
26
|
+
* Presents one engine session: its transcript root, editor, controllers, submission, workflows, and dialogs.
|
|
27
|
+
* It reaches the terminal only through its terminal host's handle, and paints only while it is attached.
|
|
28
|
+
*/
|
|
29
|
+
export class OwnedUiSessionPresenter {
|
|
32
30
|
backend;
|
|
31
|
+
#presenters;
|
|
32
|
+
#dialogHost;
|
|
33
33
|
root;
|
|
34
|
-
|
|
34
|
+
#host;
|
|
35
|
+
#terminal;
|
|
35
36
|
#cwd;
|
|
36
37
|
#listeners = new Set();
|
|
37
38
|
#unsubscribe;
|
|
@@ -57,9 +58,11 @@ export class OwnedUiSessionShell {
|
|
|
57
58
|
#editorRevision = 0;
|
|
58
59
|
#started = false;
|
|
59
60
|
#disposed = false;
|
|
61
|
+
#released;
|
|
60
62
|
#shutdownPromise;
|
|
61
63
|
#disposePromise;
|
|
62
|
-
#
|
|
64
|
+
#layoutParts;
|
|
65
|
+
#pendingClipboardWrite = Promise.resolve();
|
|
63
66
|
// Invariant: translated startup diagnostics create at most one dock notice per kind per shell.
|
|
64
67
|
#startupTrustHandled = false;
|
|
65
68
|
#customViewport;
|
|
@@ -70,11 +73,9 @@ export class OwnedUiSessionShell {
|
|
|
70
73
|
#copyExecutor;
|
|
71
74
|
#copyIntentSequence = 0;
|
|
72
75
|
#unbindClipboardWriter;
|
|
73
|
-
#damageTerminal;
|
|
74
|
-
#quitOutro;
|
|
75
76
|
#reloadPresentation;
|
|
76
77
|
#streamPresentation;
|
|
77
|
-
#
|
|
78
|
+
#inputCoordination;
|
|
78
79
|
#unsubscribeSettings;
|
|
79
80
|
#unbindPiSettings;
|
|
80
81
|
#unbindTerminalSettings;
|
|
@@ -91,556 +92,572 @@ export class OwnedUiSessionShell {
|
|
|
91
92
|
#sessionGeneration;
|
|
92
93
|
#sessionBindingGeneration;
|
|
93
94
|
#suggestionModelKey;
|
|
94
|
-
constructor(options) {
|
|
95
|
+
constructor(host, options) {
|
|
95
96
|
const { backend, cwd, routeHost, sessionLayout } = options.engine;
|
|
96
|
-
const { terminal, startup, viewportSettings,
|
|
97
|
+
const { terminal, startup, viewportSettings, reload: reloadPresentation, stream: streamPresentationOptions, input: inputPresentation, } = options.presentation ?? {};
|
|
97
98
|
const { clipboard, responseCopy, paste: pasteDiagnostics, pastePreparation } = options.diagnostics ?? {};
|
|
98
99
|
const promptHistory = options.history;
|
|
100
|
+
const { clipboard: sharedClipboard, keybindings: sharedKeybindings } = options.shared ?? {};
|
|
99
101
|
this.backend = backend;
|
|
100
|
-
this.#
|
|
101
|
-
this.#
|
|
102
|
-
this.#
|
|
102
|
+
this.#presenters = options.presenters;
|
|
103
|
+
this.#sessionGeneration = this.backend.identity.sessionGeneration;
|
|
104
|
+
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
105
|
+
this.#suggestionModelKey = modelKey(this.backend.session.view());
|
|
103
106
|
this.#cwd = cwd;
|
|
104
107
|
this.#routeHost = routeHost ?? null;
|
|
105
108
|
this.#startupRoute = options.engine.startupRoute;
|
|
106
109
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
110
|
+
this.#inputCoordination = inputPresentation?.coordination !== false;
|
|
111
|
+
this.#host = host;
|
|
112
|
+
this.#terminal = host.connect(this);
|
|
113
|
+
// Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
|
|
114
|
+
this.#dialogHost = this.#createDialogHost();
|
|
107
115
|
this.#lazySelectors = options.diagnostics?.lazySelectors;
|
|
108
116
|
this.#stopped = new Promise(resolve => {
|
|
109
117
|
this.#resolveStopped = resolve;
|
|
110
118
|
});
|
|
111
|
-
let runtime;
|
|
112
|
-
let damageTerminal;
|
|
113
|
-
let streamPresentation;
|
|
114
|
-
let pendingClipboardWrite = Promise.resolve();
|
|
115
119
|
let promptSuggestionController = null;
|
|
116
120
|
// Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
|
|
117
121
|
this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
|
|
118
122
|
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());
|
|
123
|
+
// Invariant: construction is transactional. Each binding registers its release as it is made, and a
|
|
124
|
+
// throw releases them in reverse, so a shell that never finished constructing holds nothing.
|
|
125
|
+
const constructed = [() => host.disconnect(this)];
|
|
126
|
+
try {
|
|
127
|
+
const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
|
|
128
|
+
this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
|
|
129
|
+
execute: responseCopy?.execute ?? (this.#copyExecutor = createResponseCopyExecutor({
|
|
130
|
+
...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.copy }),
|
|
131
|
+
...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
|
|
132
|
+
...(clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => clipboard.writeText(text, signal) }),
|
|
133
|
+
...(terminalCopy ? { terminal: { submit: async (control, signal) => {
|
|
134
|
+
if (signal.aborted || this.#disposed || !this.#terminal.running)
|
|
135
|
+
throw new Error("Copy canceled");
|
|
136
|
+
// Performance: never grow the real terminal's pending buffer with another clipboard payload.
|
|
137
|
+
if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
|
|
138
|
+
throw new Error("Clipboard terminal is busy");
|
|
139
|
+
}
|
|
140
|
+
this.#terminal.writeControl(control);
|
|
141
|
+
} } } : {}),
|
|
142
|
+
})),
|
|
143
|
+
...(responseCopy?.onEvent === undefined ? {} : { onEvent: responseCopy.onEvent }),
|
|
144
|
+
onFailure: result => {
|
|
145
|
+
if (this.#disposed || !this.#terminal.running)
|
|
146
|
+
return;
|
|
147
|
+
this.root.appendWorkflowStatus(result.outcome === "timed-out" ? "Copy timed out; the clipboard did not respond."
|
|
148
|
+
: result.failure === "size" ? "Selection exceeds this clipboard route's size limit."
|
|
149
|
+
: "Could not copy the selection; the clipboard is unavailable.");
|
|
150
|
+
this.#terminal.requestRender();
|
|
151
|
+
},
|
|
152
|
+
}) : null;
|
|
153
|
+
constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
|
|
154
|
+
const readClipboard = async (signal) => {
|
|
155
|
+
if (signal.aborted)
|
|
156
|
+
throw new ImageAttachmentError("image-canceled");
|
|
157
|
+
if (clipboard === undefined)
|
|
158
|
+
return runImageWorker({ kind: "clipboard" }, signal);
|
|
159
|
+
try {
|
|
160
|
+
const image = await clipboard.readImage?.(signal);
|
|
161
|
+
if (image !== null && image !== undefined) {
|
|
162
|
+
const canonical = await runImageWorker({ kind: "canonicalize", source: image }, signal);
|
|
163
|
+
if (canonical !== null)
|
|
164
|
+
return { kind: "image", ...canonical };
|
|
165
|
+
}
|
|
200
166
|
}
|
|
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;
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof ImageAttachmentError)
|
|
169
|
+
throw error;
|
|
170
|
+
// Compatibility: an unavailable image reader can still provide text.
|
|
215
171
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
172
|
+
const text = await clipboard.readText(signal);
|
|
173
|
+
return text === null ? null : { kind: "text", text };
|
|
174
|
+
};
|
|
175
|
+
this.root = new OwnedUiSessionShellRoot(this.backend.session.view(), cwd, {
|
|
176
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
177
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
178
|
+
requestRender: force => this.#terminal.requestRender(force),
|
|
179
|
+
requestHyperlinkCleanup: rows => this.#terminal.requestHyperlinkCleanup(rows),
|
|
180
|
+
onViewportFrame: frame => this.#terminal.armFrame(frame.descriptor, {
|
|
181
|
+
overlayActive: this.#terminal.hasOverlay(),
|
|
182
|
+
selectionActive: this.root.hasActiveSelection(),
|
|
183
|
+
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
184
|
+
}),
|
|
185
|
+
enableDockInputReuse: inputPresentation?.viewportReuse !== false,
|
|
186
|
+
persistentHistory: this.#customViewport && promptHistory !== undefined,
|
|
187
|
+
...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
|
|
188
|
+
onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
|
|
189
|
+
onPasteRejected: error => this.#reportSubmissionError(error),
|
|
190
|
+
onInterrupt: () => { void this.interrupt(); },
|
|
191
|
+
onClear: () => { void this.clearOrExit(); },
|
|
192
|
+
onExit: () => { void this.shutdown(); },
|
|
193
|
+
onModelSelect: () => this.#customViewport ? this.showModelsDialog() : this.showModelSelector(),
|
|
194
|
+
onModelCycle: direction => { void this.cycleModel(direction); },
|
|
195
|
+
onThinkingCycle: () => { void this.cycleThinkingLevel(); },
|
|
196
|
+
onThinkingToggle: () => {
|
|
197
|
+
this.root.toggleThinkingVisibility();
|
|
198
|
+
this.#terminal.requestRender();
|
|
199
|
+
},
|
|
200
|
+
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
201
|
+
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
|
|
202
|
+
onDequeue: () => this.restoreQueuedInput(),
|
|
203
|
+
onEditorChange: text => {
|
|
204
|
+
this.#editorRevision++;
|
|
205
|
+
promptSuggestionController?.abortPending();
|
|
206
|
+
if (text.length === 0) {
|
|
207
|
+
// Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
|
|
208
|
+
// Reassert retained ghost text in the microtask before Pi emits its scheduled frame.
|
|
209
|
+
queueMicrotask(() => promptSuggestionController?.restoreAvailable());
|
|
210
|
+
}
|
|
211
|
+
this.root.reconcilePromptImageLimitNotice(text);
|
|
212
|
+
},
|
|
213
|
+
onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
|
|
214
|
+
onPromptSuggestionEligibilityChanged: () => {
|
|
215
|
+
queueMicrotask(() => promptSuggestionController?.restoreAvailable());
|
|
216
|
+
},
|
|
217
|
+
onInputSurfaceChanged: () => {
|
|
218
|
+
promptSuggestionController?.invalidate();
|
|
219
|
+
this.#promptHistory?.synchronize();
|
|
220
|
+
},
|
|
221
|
+
onCopyText: text => {
|
|
222
|
+
if (this.#responseCopy !== null) {
|
|
223
|
+
void this.#responseCopy.submitText(text);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const write = () => {
|
|
227
|
+
if (this.#disposed)
|
|
228
|
+
return Promise.resolve();
|
|
229
|
+
this.#terminal.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
|
|
230
|
+
return clipboard === undefined ? writeSystemClipboardText(text)
|
|
231
|
+
: clipboard.writeText?.(text) ?? Promise.resolve();
|
|
237
232
|
};
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}, {
|
|
245
|
-
...startup,
|
|
246
|
-
resources: startup?.resources ?? shellResourceEntries(this.backend),
|
|
247
|
-
}, this.backend.agentDir, {
|
|
248
|
-
getMessageRenderer: customType => this.backend.pinnedMessageRenderer(customType),
|
|
249
|
-
getToolRenderers: toolName => this.backend.pinnedToolRenderers(toolName),
|
|
250
|
-
getShortcuts: bindings => this.backend.pinnedShortcutDescriptions(bindings),
|
|
251
|
-
}, sessionLayout, {
|
|
252
|
-
resolve: assetId => this.backend.resolveTranscriptImage(assetId),
|
|
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;
|
|
233
|
+
// Compatibility: comparison profiles retain their existing clipboard path.
|
|
234
|
+
this.#pendingClipboardWrite = write().catch(() => { });
|
|
235
|
+
},
|
|
236
|
+
readClipboardContent: async (signal = new AbortController().signal) => {
|
|
237
|
+
await this.#pendingClipboardWrite;
|
|
238
|
+
return readClipboard(signal);
|
|
277
239
|
},
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
:
|
|
240
|
+
captureClipboardPaste: () => {
|
|
241
|
+
const before = this.#responseCopy?.capturePasteBarrier() ?? (async () => true);
|
|
242
|
+
if (clipboard !== undefined)
|
|
243
|
+
return { kind: "provided", read: readClipboard, before };
|
|
244
|
+
if (responseCopyDestination(process.env) === "terminal")
|
|
245
|
+
return {
|
|
246
|
+
kind: "provided", before, read: async () => { throw new ImageAttachmentError("paste-unavailable"); },
|
|
247
|
+
};
|
|
248
|
+
return { kind: "native", before };
|
|
249
|
+
},
|
|
250
|
+
...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
|
|
251
|
+
...(pastePreparation === undefined && sharedClipboard === undefined ? {} : {
|
|
252
|
+
pastePreparation: { ...pastePreparation, ...(sharedClipboard === undefined ? {} : { pool: sharedClipboard.paste }) },
|
|
253
|
+
}),
|
|
254
|
+
...(sharedKeybindings === undefined ? {} : { keybindings: sharedKeybindings }),
|
|
255
|
+
...(this.#promptImages === null ? {} : { promptImageLimit: this.#promptImages.limit }),
|
|
256
|
+
skillsPresentation: () => this.#skills?.presentation() ?? "expand",
|
|
257
|
+
}, {
|
|
258
|
+
...startup,
|
|
259
|
+
resources: startup?.resources ?? shellResourceEntries(this.backend),
|
|
260
|
+
}, this.backend.identity.agentDir, this.#presenters.transcriptRenderers(), sessionLayout, {
|
|
261
|
+
resolve: assetId => this.backend.extensions.resolveTranscriptImage(assetId),
|
|
262
|
+
});
|
|
263
|
+
// Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
|
|
264
|
+
// terminal no longer renders it.
|
|
265
|
+
constructed.push(() => this.root.dispose());
|
|
266
|
+
const initialPiSettings = this.backend.settings.snapshot();
|
|
267
|
+
this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
|
|
268
|
+
this.#reloadPresentation = reloadPresentation;
|
|
269
|
+
const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
|
|
270
|
+
this.#streamPresentation = streamPresentationOptions?.scheduler === undefined
|
|
271
|
+
? new StreamPresentationCoalescer(() => this.#terminal.requestRender(), presentationInterval)
|
|
272
|
+
: new StreamPresentationCoalescer(() => this.#terminal.requestRender(), presentationInterval, streamPresentationOptions.scheduler);
|
|
273
|
+
constructed.push(() => this.#streamPresentation.dispose());
|
|
274
|
+
const promptSuggestionOptions = this.#customViewport ? options.suggestions : undefined;
|
|
275
|
+
promptSuggestionController = promptSuggestionOptions === undefined ? null : new ContextualPromptSuggestionController({
|
|
276
|
+
generator: promptSuggestionOptions.generator,
|
|
277
|
+
enabled: promptSuggestionOptions.enabled(),
|
|
278
|
+
...(promptSuggestionOptions.diagnostics === undefined ? {} : { diagnostics: promptSuggestionOptions.diagnostics }),
|
|
279
|
+
surface: {
|
|
280
|
+
canPresent: identity => this.#promptSuggestionPresentationBlockReason(identity) === null,
|
|
281
|
+
presentationBlockReason: identity => this.#promptSuggestionPresentationBlockReason(identity),
|
|
282
|
+
present: text => {
|
|
283
|
+
if (!this.root.canPresentPromptSuggestion())
|
|
284
|
+
return false;
|
|
285
|
+
this.root.setPromptSuggestion(text);
|
|
286
|
+
return true;
|
|
285
287
|
},
|
|
288
|
+
clear: () => this.root.setPromptSuggestion(null),
|
|
289
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
this.#promptSuggestions = promptSuggestionController;
|
|
293
|
+
constructed.push(() => this.#promptSuggestions?.dispose());
|
|
294
|
+
this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
|
|
295
|
+
? () => { }
|
|
296
|
+
: promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
|
|
297
|
+
constructed.push(this.#unsubscribePromptSuggestions);
|
|
298
|
+
this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
|
|
299
|
+
if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
|
|
300
|
+
this.#terminal.requestRender();
|
|
301
|
+
});
|
|
302
|
+
constructed.push(this.#unsubscribePromptImages);
|
|
303
|
+
// Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
|
|
304
|
+
// skill-command registration, both of which the owned settings manager reports through one change.
|
|
305
|
+
this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
|
|
306
|
+
// Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
|
|
307
|
+
if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
|
|
308
|
+
return;
|
|
309
|
+
this.#installAutocompleteCommands();
|
|
310
|
+
this.#terminal.requestRender();
|
|
311
|
+
});
|
|
312
|
+
constructed.push(this.#unsubscribeSkills);
|
|
313
|
+
this.#terminal.setHardwareCursor(initialPiSettings.showHardwareCursor);
|
|
314
|
+
this.#terminal.setClearOnShrink(initialPiSettings.clearOnShrink);
|
|
315
|
+
this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
|
|
316
|
+
this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
|
|
317
|
+
// Invariant: bare A1 prints only the resume hint at exit, so the pinned exit-output
|
|
318
|
+
// choice is hidden there and cannot be bound; the comparison profile binds and honors it.
|
|
319
|
+
this.#unbindShutdownSettings = this.backend.settings.productMode === "bare" ? () => { } : this.backend.settings.bindOwner("shutdown", {
|
|
320
|
+
fullscreenExitOutput: { apply() { } },
|
|
321
|
+
});
|
|
322
|
+
constructed.push(this.#unbindShutdownSettings);
|
|
323
|
+
this.#unbindTerminalSettings = this.backend.settings.bindOwner("terminal", {
|
|
324
|
+
showHardwareCursor: { apply: value => {
|
|
325
|
+
if (typeof value !== "boolean")
|
|
326
|
+
throw new TypeError("Hardware cursor setting is invalid");
|
|
327
|
+
this.#terminal.setHardwareCursor(value);
|
|
328
|
+
} },
|
|
329
|
+
clearOnShrink: { apply: value => {
|
|
330
|
+
if (typeof value !== "boolean")
|
|
331
|
+
throw new TypeError("Clear-on-shrink setting is invalid");
|
|
332
|
+
this.#terminal.setClearOnShrink(value);
|
|
333
|
+
} },
|
|
334
|
+
showTerminalProgress: { apply: value => {
|
|
335
|
+
if (typeof value !== "boolean")
|
|
336
|
+
throw new TypeError("Terminal progress setting is invalid");
|
|
337
|
+
this.#terminalProgressEnabled = value;
|
|
338
|
+
this.#syncTerminalProgress(this.view());
|
|
339
|
+
} },
|
|
340
|
+
});
|
|
341
|
+
constructed.push(this.#unbindTerminalSettings);
|
|
342
|
+
const applyViewportSettings = () => {
|
|
343
|
+
const snapshot = viewportSettings?.snapshot();
|
|
344
|
+
this.root.setViewportConfig(snapshot ?? {
|
|
345
|
+
scrollbarAppearance: "auto",
|
|
346
|
+
scrollbarStyle: "thin",
|
|
347
|
+
scrollbarSpeed: "normal",
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
applyViewportSettings();
|
|
351
|
+
this.#unsubscribeSettings = this.#customViewport && viewportSettings
|
|
352
|
+
? viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
|
|
353
|
+
: () => { };
|
|
354
|
+
constructed.push(this.#unsubscribeSettings);
|
|
355
|
+
this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
|
|
356
|
+
this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
|
|
357
|
+
this.root.setOutputPad(initialPiSettings.outputPad);
|
|
358
|
+
this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
|
|
359
|
+
this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
|
|
360
|
+
this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
|
|
361
|
+
this.#showImages = initialPiSettings.showImages;
|
|
362
|
+
this.#imageWidthCells = initialPiSettings.imageWidthCells;
|
|
363
|
+
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
364
|
+
this.#unbindPiSettings = this.backend.settings.bindOwner("shell", {
|
|
365
|
+
editorPaddingX: { apply: value => {
|
|
366
|
+
if (typeof value !== "number")
|
|
367
|
+
throw new TypeError("Editor padding is invalid");
|
|
368
|
+
this.root.setEditorPaddingX(value);
|
|
369
|
+
} },
|
|
370
|
+
autocompleteMaxVisible: { apply: value => {
|
|
371
|
+
if (typeof value !== "number")
|
|
372
|
+
throw new TypeError("Autocomplete maximum is invalid");
|
|
373
|
+
this.root.setAutocompleteMaxVisible(value);
|
|
374
|
+
} },
|
|
375
|
+
outputPad: { apply: value => {
|
|
376
|
+
if (value !== 0 && value !== 1)
|
|
377
|
+
throw new TypeError("Output padding is invalid");
|
|
378
|
+
this.root.setOutputPad(value);
|
|
379
|
+
} },
|
|
380
|
+
hideThinkingBlock: { apply: value => {
|
|
381
|
+
if (typeof value !== "boolean")
|
|
382
|
+
throw new TypeError("Thinking-block visibility is invalid");
|
|
383
|
+
this.root.setHideThinkingBlock(value);
|
|
384
|
+
} },
|
|
385
|
+
mermaidRenderingMode: { apply: value => {
|
|
386
|
+
if (value !== "off" && value !== "final" && value !== "streaming")
|
|
387
|
+
throw new TypeError("Mermaid mode is invalid");
|
|
388
|
+
this.root.setMermaidRenderingMode(value);
|
|
389
|
+
} },
|
|
390
|
+
showImages: { apply: value => {
|
|
391
|
+
if (typeof value !== "boolean")
|
|
392
|
+
throw new TypeError("Image visibility is invalid");
|
|
393
|
+
this.#showImages = value;
|
|
394
|
+
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
395
|
+
} },
|
|
396
|
+
imageWidthCells: { apply: value => {
|
|
397
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
398
|
+
throw new TypeError("Image width is invalid");
|
|
399
|
+
this.#imageWidthCells = value;
|
|
400
|
+
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
401
|
+
} },
|
|
402
|
+
...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
|
|
403
|
+
if (typeof value !== "boolean")
|
|
404
|
+
throw new TypeError("Fullscreen copy-on-select setting is invalid");
|
|
405
|
+
this.root.setFullscreenCopyOnSelect(value);
|
|
406
|
+
} } } : {
|
|
407
|
+
fullscreenWheelScrollLines: { apply: (value) => {
|
|
408
|
+
if (value !== "auto" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100)) {
|
|
409
|
+
throw new TypeError("Fullscreen wheel-scroll distance is invalid");
|
|
410
|
+
}
|
|
411
|
+
this.#terminal.setWheelScrollLines(value);
|
|
412
|
+
} },
|
|
286
413
|
}),
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
414
|
+
});
|
|
415
|
+
constructed.push(this.#unbindPiSettings);
|
|
416
|
+
if (this.#customViewport && promptHistory !== undefined) {
|
|
417
|
+
this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
|
|
418
|
+
const sidecar = this.#promptHistoryImageSidecar;
|
|
419
|
+
this.#promptHistory = new PromptHistoryController({
|
|
420
|
+
editor: this.root.editor,
|
|
421
|
+
store: promptHistory.store,
|
|
422
|
+
limit: promptHistory.limit,
|
|
423
|
+
fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
|
|
424
|
+
active: () => this.root.usesDefaultInputSurface(),
|
|
425
|
+
render: () => this.#terminal.requestRender(),
|
|
426
|
+
rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
|
|
427
|
+
});
|
|
428
|
+
constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
|
|
429
|
+
}
|
|
430
|
+
this.#extensionBridge = createPiExtensionUiBridge({
|
|
431
|
+
runtime: {
|
|
432
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
433
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
434
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
292
435
|
},
|
|
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.
|
|
322
|
-
|
|
436
|
+
agentDir: this.backend.identity.agentDir,
|
|
437
|
+
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
438
|
+
setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
|
|
439
|
+
showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
|
|
440
|
+
listenInput: handler => this.#terminal.addInputListener(handler),
|
|
441
|
+
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
442
|
+
replaceHeader: component => this.root.setExtensionHeader(component),
|
|
443
|
+
replaceFooter: component => this.root.setExtensionFooter(component),
|
|
444
|
+
setStatus: (key, text) => this.root.setExtensionStatus(key, text),
|
|
445
|
+
setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
|
|
446
|
+
notify: (message, type) => this.root.addExtensionNotification(message, type),
|
|
447
|
+
setTitle: title => this.#terminal.setTitle(title),
|
|
448
|
+
getEditorText: () => this.root.editor.getText(),
|
|
449
|
+
setEditorText: text => this.root.editor.setText(text),
|
|
450
|
+
pasteToEditor: text => this.root.editor.insertText(text),
|
|
451
|
+
addAutocompleteProvider: factory => this.root.editor.addAutocompleteProvider(factory),
|
|
452
|
+
setCustomEditor: component => this.root.setInputSurface(component, true, "opaque"),
|
|
453
|
+
getFooterData: () => this.root.extensionFooterData(),
|
|
454
|
+
getToolsExpanded: () => this.root.toolsExpanded,
|
|
455
|
+
setToolsExpanded: expanded => this.root.setToolsExpanded(expanded),
|
|
456
|
+
});
|
|
457
|
+
constructed.push(() => this.#extensionBridge.dispose());
|
|
458
|
+
constructed.push(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
|
|
459
|
+
this.backend.workflows.setWorkflowInteractionHost({
|
|
460
|
+
startLogin: request => this.#startWorkflowLogin(request),
|
|
461
|
+
prompt: request => this.#requestWorkflowInput(request),
|
|
462
|
+
notify: event => this.#notifyWorkflowLogin(event),
|
|
463
|
+
publish: message => {
|
|
464
|
+
this.root.appendWorkflowMessage(message);
|
|
465
|
+
this.#terminal.requestRender();
|
|
323
466
|
},
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
this.#promptSuggestions = promptSuggestionController;
|
|
329
|
-
this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
|
|
330
|
-
? () => { }
|
|
331
|
-
: promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
|
|
332
|
-
this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
|
|
333
|
-
if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
|
|
334
|
-
this.runtime.requestRender();
|
|
335
|
-
});
|
|
336
|
-
// Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
|
|
337
|
-
// skill-command registration, both of which the owned settings manager reports through one change.
|
|
338
|
-
this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
|
|
339
|
-
// Invariant: reinstalling drops extension provider wrappers, so an unrelated setting change leaves the list alone.
|
|
340
|
-
if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
|
|
341
|
-
return;
|
|
467
|
+
finishLogin: () => this.#finishWorkflowLogin(),
|
|
468
|
+
});
|
|
342
469
|
this.#installAutocompleteCommands();
|
|
343
|
-
this.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (typeof value !== "boolean")
|
|
357
|
-
throw new TypeError("Hardware cursor setting is invalid");
|
|
358
|
-
this.runtime.setHardwareCursor(value);
|
|
359
|
-
} },
|
|
360
|
-
clearOnShrink: { apply: value => {
|
|
361
|
-
if (typeof value !== "boolean")
|
|
362
|
-
throw new TypeError("Clear-on-shrink setting is invalid");
|
|
363
|
-
this.runtime.setClearOnShrink(value);
|
|
364
|
-
} },
|
|
365
|
-
showTerminalProgress: { apply: value => {
|
|
366
|
-
if (typeof value !== "boolean")
|
|
367
|
-
throw new TypeError("Terminal progress setting is invalid");
|
|
368
|
-
this.#terminalProgressEnabled = value;
|
|
369
|
-
this.#syncTerminalProgress(this.view());
|
|
370
|
-
} },
|
|
371
|
-
});
|
|
372
|
-
this.#removeViewportPreInput = this.#customViewport
|
|
373
|
-
? this.runtime.addPreInputListener(data => {
|
|
374
|
-
if (inputPresentation?.coordination === false)
|
|
375
|
-
this.#streamPresentation.noteImmediatePresentation();
|
|
376
|
-
// Compatibility: route modal paging and boundary keys before Pi's fullscreen handlers.
|
|
377
|
-
const isDefault = this.root.usesDefaultInputSurface();
|
|
378
|
-
if (!this.runtime.hasOverlay() && !isDefault
|
|
379
|
-
&& (this.root.editor.matchesTerminalKey(data, "pageUp")
|
|
380
|
-
|| this.root.editor.matchesTerminalKey(data, "pageDown"))) {
|
|
381
|
-
this.root.handleInput(data);
|
|
382
|
-
this.runtime.requestRender();
|
|
383
|
-
return { consume: true };
|
|
470
|
+
this.#unsubscribe = this.backend.session.onEvent(event => {
|
|
471
|
+
// Invariant: once disposal begins no event touches the view; only the stop still settles the shell.
|
|
472
|
+
if (this.#disposed) {
|
|
473
|
+
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
474
|
+
this.#settleStoppedLifecycle();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// Performance: a streamed chunk names one block, and touching only that block is what keeps the
|
|
478
|
+
// cost of a chunk the same in a long session as in a new one. Everything else
|
|
479
|
+
// resynchronizes the view, which is cheap next to re-reading the transcript.
|
|
480
|
+
if (event.type === "agent-run-started") {
|
|
481
|
+
this.#promptSuggestions?.invalidate();
|
|
482
|
+
this.root.resumeViewportFollowing();
|
|
384
483
|
}
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|| !isDefault && (this.root.editor.matchesTerminalKey(data, "ctrl+home")
|
|
388
|
-
|| this.root.editor.matchesTerminalKey(data, "ctrl+end")))) {
|
|
389
|
-
if (isDefault)
|
|
390
|
-
this.root.handleViewportPreInput(data, true);
|
|
391
|
-
this.root.handleInput(data);
|
|
392
|
-
this.runtime.requestRender();
|
|
393
|
-
return { consume: true };
|
|
484
|
+
if (event.type === "assistant-message-completed" && !event.successful) {
|
|
485
|
+
this.#programStatus.setErrorMessage(this.view().status.diagnostics.at(-1));
|
|
394
486
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
this.
|
|
487
|
+
if (event.type === "agent-run-started" || event.type === "assistant-message-completed" || event.type === "agent-run-settled") {
|
|
488
|
+
this.#programStatus.handleEvent(event);
|
|
489
|
+
}
|
|
490
|
+
if (event.type === "assistant-message-completed") {
|
|
491
|
+
this.root.noteCompletedAssistantMessage();
|
|
492
|
+
const identity = {
|
|
493
|
+
sessionId: event.sessionId,
|
|
494
|
+
sessionGeneration: event.sessionGeneration,
|
|
495
|
+
runSequence: event.runSequence,
|
|
496
|
+
responseSequence: event.responseSequence,
|
|
497
|
+
model: event.model,
|
|
498
|
+
};
|
|
499
|
+
if (event.model === null)
|
|
500
|
+
this.#promptSuggestions?.skip(identity, "no-model");
|
|
501
|
+
else
|
|
502
|
+
this.#promptSuggestions?.consider({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
503
|
+
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
504
|
+
: event.stopReason !== "stop" ? "incomplete-response"
|
|
505
|
+
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
506
|
+
: this.root.promptSuggestionPrepareBlockReason());
|
|
400
507
|
}
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (acknowledgement.hasNonWhitespace) {
|
|
411
|
-
this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
|
|
412
|
-
}
|
|
413
|
-
});
|
|
508
|
+
const semanticOnly = event.type === "agent-run-started" || event.type === "assistant-message-completed";
|
|
509
|
+
const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.identity.sessionGeneration
|
|
510
|
+
? this.#syncBlock(event.block)
|
|
511
|
+
: semanticOnly ? this.view() : this.#syncView();
|
|
512
|
+
this.#syncTerminalProgress(view);
|
|
513
|
+
const currentModelKey = modelKey(view);
|
|
514
|
+
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration || currentModelKey !== this.#suggestionModelKey) {
|
|
515
|
+
this.#promptSuggestions?.invalidate();
|
|
516
|
+
this.#suggestionModelKey = currentModelKey;
|
|
414
517
|
}
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
518
|
+
if (event.type === "status" && /^(Retrying|Compacting)/.test(event.status.workingMessage ?? "")) {
|
|
519
|
+
this.#promptSuggestions?.invalidate();
|
|
520
|
+
}
|
|
521
|
+
if (event.type === "agent-run-settled") {
|
|
522
|
+
const identity = {
|
|
523
|
+
sessionId: event.sessionId,
|
|
524
|
+
sessionGeneration: event.sessionGeneration,
|
|
525
|
+
runSequence: event.runSequence,
|
|
526
|
+
responseSequence: event.responseSequence,
|
|
527
|
+
model: event.model,
|
|
528
|
+
};
|
|
529
|
+
if (event.model === null)
|
|
530
|
+
this.#promptSuggestions?.skip(identity, "no-model");
|
|
531
|
+
else
|
|
532
|
+
this.#promptSuggestions?.settle({ ...identity, model: event.model }, !event.successful ? "failed-response"
|
|
533
|
+
: event.toolContinuation || event.stopReason === "toolUse" ? "tool-continuation"
|
|
534
|
+
: event.stopReason !== "stop" ? "incomplete-response"
|
|
535
|
+
: event.assistantMessageCount < 2 ? "early-conversation"
|
|
536
|
+
: this.root.promptSuggestionPrepareBlockReason());
|
|
537
|
+
}
|
|
538
|
+
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
539
|
+
this.#settleStoppedLifecycle();
|
|
426
540
|
});
|
|
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),
|
|
541
|
+
constructed.push(this.#unsubscribe);
|
|
542
|
+
this.#unbindClipboardWriter = this.#responseCopy === null ? () => { } : this.backend.extensions.bindClipboardWriter(async (text) => {
|
|
543
|
+
const result = await this.#responseCopy.submitText(text);
|
|
544
|
+
if (result.outcome === "delivered")
|
|
545
|
+
return true;
|
|
546
|
+
if (result.outcome === "submitted-unverified")
|
|
547
|
+
return false;
|
|
548
|
+
throw new Error(result.outcome === "timed-out" ? "Clipboard delivery timed out" : "Clipboard delivery could not be completed");
|
|
503
549
|
});
|
|
550
|
+
constructed.push(this.#unbindClipboardWriter);
|
|
551
|
+
if (this.backend.session.view().lifecycle === "stopped")
|
|
552
|
+
this.#resolveStopped?.();
|
|
504
553
|
}
|
|
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());
|
|
554
|
+
catch (error) {
|
|
555
|
+
for (const release of constructed.reverse()) {
|
|
556
|
+
try {
|
|
557
|
+
release();
|
|
558
|
+
}
|
|
559
|
+
catch { /* Rationale: the construction failure is the error worth reporting. */ }
|
|
604
560
|
}
|
|
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?.();
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
618
563
|
}
|
|
619
564
|
view() {
|
|
620
|
-
return this.backend.view();
|
|
565
|
+
return this.backend.session.view();
|
|
566
|
+
}
|
|
567
|
+
get terminalHost() {
|
|
568
|
+
return this.#host;
|
|
569
|
+
}
|
|
570
|
+
render(width) {
|
|
571
|
+
return this.root.render(width);
|
|
572
|
+
}
|
|
573
|
+
handleInput(data) {
|
|
574
|
+
this.root.handleInput(data);
|
|
575
|
+
}
|
|
576
|
+
invalidate() {
|
|
577
|
+
this.root.invalidate();
|
|
578
|
+
}
|
|
579
|
+
setFocused(focused) {
|
|
580
|
+
this.root.setFocused(focused);
|
|
581
|
+
}
|
|
582
|
+
inputCoordinationSurface() {
|
|
583
|
+
return this.root.inputCoordinationSurface();
|
|
584
|
+
}
|
|
585
|
+
setOverlaySurfaces(surfaces) {
|
|
586
|
+
this.root.setViewportOverlaySurfaces(surfaces);
|
|
587
|
+
}
|
|
588
|
+
noteImmediatePresentation() {
|
|
589
|
+
this.#streamPresentation.noteImmediatePresentation();
|
|
590
|
+
}
|
|
591
|
+
layoutPart(part) {
|
|
592
|
+
this.#layoutParts ??= this.root.layoutParts();
|
|
593
|
+
return this.#layoutParts[part];
|
|
594
|
+
}
|
|
595
|
+
activated() {
|
|
596
|
+
this.#syncTerminalProgress(this.view());
|
|
597
|
+
}
|
|
598
|
+
handleViewportInput(data) {
|
|
599
|
+
if (!this.#inputCoordination)
|
|
600
|
+
this.#streamPresentation.noteImmediatePresentation();
|
|
601
|
+
// Compatibility: route modal paging and boundary keys before Pi's fullscreen handlers.
|
|
602
|
+
const isDefault = this.root.usesDefaultInputSurface();
|
|
603
|
+
if (!this.#terminal.hasOverlay() && !isDefault
|
|
604
|
+
&& (this.root.editor.matchesTerminalKey(data, "pageUp")
|
|
605
|
+
|| this.root.editor.matchesTerminalKey(data, "pageDown"))) {
|
|
606
|
+
this.root.handleInput(data);
|
|
607
|
+
this.#terminal.requestRender();
|
|
608
|
+
return { consume: true };
|
|
609
|
+
}
|
|
610
|
+
if (!this.#terminal.hasOverlay() && (this.root.editor.matchesTerminalKey(data, "home")
|
|
611
|
+
|| this.root.editor.matchesTerminalKey(data, "end")
|
|
612
|
+
|| !isDefault && (this.root.editor.matchesTerminalKey(data, "ctrl+home")
|
|
613
|
+
|| this.root.editor.matchesTerminalKey(data, "ctrl+end")))) {
|
|
614
|
+
if (isDefault)
|
|
615
|
+
this.root.handleViewportPreInput(data, true);
|
|
616
|
+
this.root.handleInput(data);
|
|
617
|
+
this.#terminal.requestRender();
|
|
618
|
+
return { consume: true };
|
|
619
|
+
}
|
|
620
|
+
// Invariant: geometry must belong to the painted frame, including newly opened/nested
|
|
621
|
+
// surfaces. Steady pointer input does not trigger a synchronous composition.
|
|
622
|
+
const viewport = this.#terminal.viewport();
|
|
623
|
+
if (data.includes("\u001b[<") && !this.root.viewportInputGeometryReady(viewport.columns, viewport.rows)) {
|
|
624
|
+
this.#terminal.renderNow();
|
|
625
|
+
}
|
|
626
|
+
const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
|
|
627
|
+
if (routed.copySelection !== undefined) {
|
|
628
|
+
const snapshot = routed.copySelection;
|
|
629
|
+
const acknowledgement = copyAcknowledgement(snapshot);
|
|
630
|
+
const intent = ++this.#copyIntentSequence;
|
|
631
|
+
void this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite).then(result => {
|
|
632
|
+
if (this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
|
|
633
|
+
|| result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
|
|
634
|
+
return;
|
|
635
|
+
if (acknowledgement.hasNonWhitespace) {
|
|
636
|
+
this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (!routed.consumed)
|
|
641
|
+
return routed.data === data ? undefined : { data: routed.data };
|
|
642
|
+
return routed.data.length === 0 ? { consume: true } : { data: routed.data };
|
|
621
643
|
}
|
|
622
644
|
#promptSuggestionPresentationBlockReason(identity) {
|
|
623
645
|
const view = this.view();
|
|
624
646
|
if (this.#disposed)
|
|
625
647
|
return "disposed";
|
|
626
|
-
if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.sessionGeneration
|
|
648
|
+
if (identity.sessionId !== view.sessionId || identity.sessionGeneration !== this.backend.identity.sessionGeneration
|
|
627
649
|
|| modelKey(view) !== `${identity.model.providerId}/${identity.model.modelId}`)
|
|
628
650
|
return "stale-identity";
|
|
629
651
|
return this.root.promptSuggestionPresentationBlockReason();
|
|
630
652
|
}
|
|
631
|
-
damagePresentationDecision() {
|
|
632
|
-
return this.#damageTerminal?.lastDecision ?? null;
|
|
633
|
-
}
|
|
634
653
|
start() {
|
|
635
654
|
if (this.#started)
|
|
636
655
|
return;
|
|
637
656
|
this.#started = true;
|
|
638
|
-
this.
|
|
657
|
+
this.#terminal.start();
|
|
639
658
|
this.#promptHistory?.start();
|
|
640
659
|
this.#syncTerminalProgress(this.view());
|
|
641
|
-
|
|
642
|
-
this.#setPointerReporting(true);
|
|
643
|
-
void this.backend.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
|
|
660
|
+
void this.backend.extensions.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
|
|
644
661
|
this.#syncView();
|
|
645
662
|
// Performance: the spare clipboard helpers fork after the first frame is out, so startup never waits on them.
|
|
646
663
|
setImmediate(() => {
|
|
@@ -670,10 +687,10 @@ export class OwnedUiSessionShell {
|
|
|
670
687
|
if (!this.root.hasPendingPastes(draft))
|
|
671
688
|
return this.#guardSubmission(draft, action);
|
|
672
689
|
const controller = new AbortController();
|
|
673
|
-
const generation = this.backend.sessionGeneration;
|
|
690
|
+
const generation = this.backend.identity.sessionGeneration;
|
|
674
691
|
const result = this.#guardSubmission(draft, async () => {
|
|
675
692
|
await this.root.waitForPromptPastes(draft, controller.signal);
|
|
676
|
-
if (controller.signal.aborted || this.#disposed || this.backend.sessionGeneration !== generation)
|
|
693
|
+
if (controller.signal.aborted || this.#disposed || this.backend.identity.sessionGeneration !== generation)
|
|
677
694
|
return rejected("image submission canceled");
|
|
678
695
|
return action();
|
|
679
696
|
}).finally(() => {
|
|
@@ -693,7 +710,7 @@ export class OwnedUiSessionShell {
|
|
|
693
710
|
render: width => [...renderPiShellStatusText(`Waiting for images (${count} submission${count === 1 ? "" : "s"}) — Esc cancels; dequeue restores`, width)],
|
|
694
711
|
invalidate: () => { },
|
|
695
712
|
}, "aboveEditor");
|
|
696
|
-
this.
|
|
713
|
+
this.#terminal.requestRender();
|
|
697
714
|
}
|
|
698
715
|
#cancelWaitingImages() {
|
|
699
716
|
for (const item of this.#waitingImages.values())
|
|
@@ -707,7 +724,7 @@ export class OwnedUiSessionShell {
|
|
|
707
724
|
this.#promptHistory.rememberRecovery(text);
|
|
708
725
|
else {
|
|
709
726
|
this.#persistImageSidecars(text);
|
|
710
|
-
this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.sessionId);
|
|
727
|
+
this.#promptHistory.capture(reusable, kind, this.#cwd, this.backend.identity.sessionId);
|
|
711
728
|
}
|
|
712
729
|
}
|
|
713
730
|
}
|
|
@@ -738,7 +755,7 @@ export class OwnedUiSessionShell {
|
|
|
738
755
|
if (command) {
|
|
739
756
|
this.#rememberInput(displayInput, "bash");
|
|
740
757
|
try {
|
|
741
|
-
const result = await this.backend.executeBashWorkflow(command, excludeFromContext);
|
|
758
|
+
const result = await this.backend.workflows.executeBashWorkflow(command, excludeFromContext);
|
|
742
759
|
const workflow = {
|
|
743
760
|
command: "debug",
|
|
744
761
|
outcome: result.cancelled ? "cancelled" : result.exitCode === 0 || result.exitCode === undefined ? "completed" : "failed",
|
|
@@ -746,13 +763,13 @@ export class OwnedUiSessionShell {
|
|
|
746
763
|
...(result.output ? { detail: result.output } : {}),
|
|
747
764
|
};
|
|
748
765
|
this.root.appendWorkflowResult(workflow);
|
|
749
|
-
this.
|
|
766
|
+
this.#terminal.requestRender();
|
|
750
767
|
return workflow.outcome === "failed" ? rejected(workflow.message) : { outcome: "completed", diagnostic: null };
|
|
751
768
|
}
|
|
752
769
|
catch (error) {
|
|
753
770
|
const message = `Bash command failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
754
771
|
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message });
|
|
755
|
-
this.
|
|
772
|
+
this.#terminal.requestRender();
|
|
756
773
|
return rejected(message);
|
|
757
774
|
}
|
|
758
775
|
}
|
|
@@ -767,7 +784,7 @@ export class OwnedUiSessionShell {
|
|
|
767
784
|
return await this.#execute({
|
|
768
785
|
type,
|
|
769
786
|
correlationId: this.#correlation(type),
|
|
770
|
-
sessionId: this.backend.sessionId,
|
|
787
|
+
sessionId: this.backend.identity.sessionId,
|
|
771
788
|
text: input,
|
|
772
789
|
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
773
790
|
}, displayInput);
|
|
@@ -782,7 +799,7 @@ export class OwnedUiSessionShell {
|
|
|
782
799
|
return this.shutdown();
|
|
783
800
|
this.root.editor.setText("");
|
|
784
801
|
this.#lastClearTime = now;
|
|
785
|
-
this.
|
|
802
|
+
this.#terminal.requestRender();
|
|
786
803
|
return { outcome: "completed", diagnostic: null };
|
|
787
804
|
}
|
|
788
805
|
async interrupt(now = Date.now()) {
|
|
@@ -800,7 +817,7 @@ export class OwnedUiSessionShell {
|
|
|
800
817
|
// Compatibility: match Pi: with an empty editor, two escapes inside 500 ms open the
|
|
801
818
|
// configured session navigator. The first escape intentionally does not
|
|
802
819
|
// interrupt or mutate the prompt.
|
|
803
|
-
const action = this.backend.
|
|
820
|
+
const action = this.backend.settings.snapshot().doubleEscapeAction;
|
|
804
821
|
if (action === "none")
|
|
805
822
|
return { outcome: "rejected", diagnostic: "nothing to interrupt" };
|
|
806
823
|
if (now - this.#lastEscapeTime < 500) {
|
|
@@ -834,7 +851,7 @@ export class OwnedUiSessionShell {
|
|
|
834
851
|
return this.#execute({
|
|
835
852
|
type: "resume-session",
|
|
836
853
|
correlationId: this.#correlation("resume"),
|
|
837
|
-
sessionId: this.backend.sessionId,
|
|
854
|
+
sessionId: this.backend.identity.sessionId,
|
|
838
855
|
sessionPath,
|
|
839
856
|
});
|
|
840
857
|
}
|
|
@@ -843,7 +860,7 @@ export class OwnedUiSessionShell {
|
|
|
843
860
|
return this.#execute({
|
|
844
861
|
type: "set-model",
|
|
845
862
|
correlationId: this.#correlation("model"),
|
|
846
|
-
sessionId: this.backend.sessionId,
|
|
863
|
+
sessionId: this.backend.identity.sessionId,
|
|
847
864
|
model: { providerId, modelId, displayName: modelId },
|
|
848
865
|
});
|
|
849
866
|
}
|
|
@@ -851,7 +868,7 @@ export class OwnedUiSessionShell {
|
|
|
851
868
|
return this.#execute({
|
|
852
869
|
type: "set-thinking-level",
|
|
853
870
|
correlationId: this.#correlation("thinking"),
|
|
854
|
-
sessionId: this.backend.sessionId,
|
|
871
|
+
sessionId: this.backend.identity.sessionId,
|
|
855
872
|
thinkingLevel,
|
|
856
873
|
});
|
|
857
874
|
}
|
|
@@ -861,9 +878,9 @@ export class OwnedUiSessionShell {
|
|
|
861
878
|
return this.setThinkingLevel(levels[(current + 1) % levels.length] ?? "off");
|
|
862
879
|
}
|
|
863
880
|
async cycleModel(direction) {
|
|
864
|
-
const result = await this.backend.cycleModelWorkflow(direction);
|
|
881
|
+
const result = await this.backend.workflows.cycleModelWorkflow(direction);
|
|
865
882
|
this.root.appendWorkflowResult(result);
|
|
866
|
-
this.
|
|
883
|
+
this.#terminal.requestRender();
|
|
867
884
|
return workflowAdapterResult(result);
|
|
868
885
|
}
|
|
869
886
|
async queueFollowUp() {
|
|
@@ -884,18 +901,18 @@ export class OwnedUiSessionShell {
|
|
|
884
901
|
return this.#execute({
|
|
885
902
|
type: "follow-up",
|
|
886
903
|
correlationId: this.#correlation("follow-up"),
|
|
887
|
-
sessionId: this.backend.sessionId,
|
|
904
|
+
sessionId: this.backend.identity.sessionId,
|
|
888
905
|
text,
|
|
889
906
|
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
890
907
|
}, displayInput);
|
|
891
908
|
}
|
|
892
909
|
restoreQueuedInput() {
|
|
893
|
-
const queued = [...this.#waitingImages.keys(), ...this.backend.clearQueuedWorkflows()];
|
|
910
|
+
const queued = [...this.#waitingImages.keys(), ...this.backend.workflows.clearQueuedWorkflows()];
|
|
894
911
|
this.#cancelWaitingImages();
|
|
895
912
|
if (queued.length === 0)
|
|
896
913
|
return;
|
|
897
914
|
this.root.editor.setText(queued.join("\n"));
|
|
898
|
-
this.
|
|
915
|
+
this.#terminal.requestRender();
|
|
899
916
|
}
|
|
900
917
|
showSelector(title, options, onSelect, onCancel) {
|
|
901
918
|
this.#dialogHandle?.hide();
|
|
@@ -917,7 +934,7 @@ export class OwnedUiSessionShell {
|
|
|
917
934
|
onCancel?.();
|
|
918
935
|
},
|
|
919
936
|
});
|
|
920
|
-
const handle = this.
|
|
937
|
+
const handle = this.#terminal.showOverlay(component, {
|
|
921
938
|
width: "70%",
|
|
922
939
|
maxHeight: "80%",
|
|
923
940
|
anchor: "center",
|
|
@@ -926,6 +943,24 @@ export class OwnedUiSessionShell {
|
|
|
926
943
|
this.#dialogHandle = handle;
|
|
927
944
|
return handle;
|
|
928
945
|
}
|
|
946
|
+
// Rationale: presenters see the shell only as a dialog host; selectors open in the input dock and run choices as workflows.
|
|
947
|
+
#createDialogHost() {
|
|
948
|
+
const isDisposed = () => this.#disposed;
|
|
949
|
+
return {
|
|
950
|
+
get disposed() { return isDisposed(); },
|
|
951
|
+
setInputSurface: surface => this.root.setInputSurface(surface),
|
|
952
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
953
|
+
viewport: () => this.#terminal.viewport(),
|
|
954
|
+
appendWorkflowStatus: text => this.root.appendWorkflowStatus(text),
|
|
955
|
+
appendWorkflowResult: result => this.root.appendWorkflowResult(result),
|
|
956
|
+
runWorkflow: request => this.runWorkflow(request),
|
|
957
|
+
// Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
|
|
958
|
+
// fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
|
|
959
|
+
...(this.#customViewport
|
|
960
|
+
? { beginPresentationHold: () => this.#terminal.beginPresentationHold() }
|
|
961
|
+
: { switchTuiMode: mode => this.#terminal.switchMode(mode) }),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
929
964
|
#loadLazySelectors() {
|
|
930
965
|
if (this.#lazySelectors !== undefined)
|
|
931
966
|
return Promise.resolve(this.#lazySelectors);
|
|
@@ -935,17 +970,17 @@ export class OwnedUiSessionShell {
|
|
|
935
970
|
});
|
|
936
971
|
}
|
|
937
972
|
async showThinkingSelector() {
|
|
938
|
-
const snapshot = this.backend.
|
|
973
|
+
const snapshot = this.backend.settings.snapshot();
|
|
939
974
|
const close = () => {
|
|
940
975
|
this.root.setFooterLevel(true);
|
|
941
976
|
this.root.setInputSurface(null);
|
|
942
|
-
this.
|
|
977
|
+
this.#terminal.requestRender();
|
|
943
978
|
};
|
|
944
979
|
const select = (level) => {
|
|
945
980
|
close();
|
|
946
981
|
void this.runWorkflow({ command: "thinking", argument: "", selection: level });
|
|
947
982
|
};
|
|
948
|
-
const releasePresentation = this.#customViewport ? this.
|
|
983
|
+
const releasePresentation = this.#customViewport ? this.#terminal.beginPresentationHold() : undefined;
|
|
949
984
|
try {
|
|
950
985
|
const selectors = await this.#loadLazySelectors();
|
|
951
986
|
const component = await selectors.createThinking({
|
|
@@ -953,7 +988,7 @@ export class OwnedUiSessionShell {
|
|
|
953
988
|
availableLevels: snapshot.availableThinkingLevels,
|
|
954
989
|
onSelect: select,
|
|
955
990
|
onCancel: close,
|
|
956
|
-
onSelectAsDefault: level => this.backend.setDefaultThinkingLevel(level),
|
|
991
|
+
onSelectAsDefault: level => this.backend.settings.setDefaultThinkingLevel(level),
|
|
957
992
|
defaultLevel: snapshot.defaultThinkingLevel,
|
|
958
993
|
...(this.#customViewport ? {
|
|
959
994
|
presentation: {
|
|
@@ -966,7 +1001,7 @@ export class OwnedUiSessionShell {
|
|
|
966
1001
|
return;
|
|
967
1002
|
this.root.setFooterLevel(false);
|
|
968
1003
|
this.root.setInputSurface(component);
|
|
969
|
-
this.
|
|
1004
|
+
this.#terminal.requestRender();
|
|
970
1005
|
}
|
|
971
1006
|
finally {
|
|
972
1007
|
releasePresentation?.();
|
|
@@ -976,7 +1011,7 @@ export class OwnedUiSessionShell {
|
|
|
976
1011
|
showSkillsSelector(skills = this.#skillSummaries()) {
|
|
977
1012
|
const close = () => {
|
|
978
1013
|
this.root.setInputSurface(null);
|
|
979
|
-
this.
|
|
1014
|
+
this.#terminal.requestRender();
|
|
980
1015
|
};
|
|
981
1016
|
const component = createPiShellSkillsSelector({
|
|
982
1017
|
skills,
|
|
@@ -987,57 +1022,33 @@ export class OwnedUiSessionShell {
|
|
|
987
1022
|
onCancel: close,
|
|
988
1023
|
});
|
|
989
1024
|
this.root.setInputSurface(component);
|
|
990
|
-
this.
|
|
1025
|
+
this.#terminal.requestRender();
|
|
991
1026
|
}
|
|
992
1027
|
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();
|
|
1028
|
+
this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
|
|
1018
1029
|
}
|
|
1019
1030
|
showForkSelector() {
|
|
1020
|
-
const options = this.backend.pinnedForkOptions();
|
|
1031
|
+
const options = this.backend.catalog.pinnedForkOptions();
|
|
1021
1032
|
if (options.length === 0) {
|
|
1022
1033
|
this.root.appendWorkflowResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
|
|
1023
|
-
this.
|
|
1034
|
+
this.#terminal.requestRender();
|
|
1024
1035
|
return;
|
|
1025
1036
|
}
|
|
1026
1037
|
const close = () => {
|
|
1027
1038
|
this.root.setInputSurface(null);
|
|
1028
|
-
this.
|
|
1039
|
+
this.#terminal.requestRender();
|
|
1029
1040
|
};
|
|
1030
1041
|
const component = createPiShellUserMessageSelector(options, selection => {
|
|
1031
1042
|
close();
|
|
1032
1043
|
void this.runWorkflow({ command: "fork", argument: "", selection });
|
|
1033
1044
|
}, close);
|
|
1034
1045
|
this.root.setInputSurface(component);
|
|
1035
|
-
this.
|
|
1046
|
+
this.#terminal.requestRender();
|
|
1036
1047
|
}
|
|
1037
1048
|
async showLogoutSelector() {
|
|
1038
1049
|
let options;
|
|
1039
1050
|
try {
|
|
1040
|
-
options = await this.backend.pinnedLogoutOptions();
|
|
1051
|
+
options = await this.backend.catalog.pinnedLogoutOptions();
|
|
1041
1052
|
}
|
|
1042
1053
|
catch (error) {
|
|
1043
1054
|
this.root.appendWorkflowResult({
|
|
@@ -1045,27 +1056,27 @@ export class OwnedUiSessionShell {
|
|
|
1045
1056
|
outcome: "failed",
|
|
1046
1057
|
message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
|
|
1047
1058
|
});
|
|
1048
|
-
this.
|
|
1059
|
+
this.#terminal.requestRender();
|
|
1049
1060
|
return;
|
|
1050
1061
|
}
|
|
1051
1062
|
if (options.length === 0) {
|
|
1052
1063
|
this.root.appendWorkflowStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
|
|
1053
|
-
this.
|
|
1064
|
+
this.#terminal.requestRender();
|
|
1054
1065
|
return;
|
|
1055
1066
|
}
|
|
1056
1067
|
const close = () => {
|
|
1057
1068
|
this.root.setInputSurface(null);
|
|
1058
|
-
this.
|
|
1069
|
+
this.#terminal.requestRender();
|
|
1059
1070
|
};
|
|
1060
1071
|
const component = createPiShellAuthProviderSelector("logout", options, selection => {
|
|
1061
1072
|
close();
|
|
1062
1073
|
void this.runWorkflow({ command: "logout", argument: "", selection });
|
|
1063
1074
|
}, close);
|
|
1064
1075
|
this.root.setInputSurface(component);
|
|
1065
|
-
this.
|
|
1076
|
+
this.#terminal.requestRender();
|
|
1066
1077
|
}
|
|
1067
1078
|
showLoginMethodSelector(providerReference) {
|
|
1068
|
-
const method = this.backend.pinnedLoginMethodOptions(providerReference);
|
|
1079
|
+
const method = this.backend.catalog.pinnedLoginMethodOptions(providerReference);
|
|
1069
1080
|
if (method.options.length === 0) {
|
|
1070
1081
|
this.showLoginProviderSelector(undefined, providerReference);
|
|
1071
1082
|
return;
|
|
@@ -1078,7 +1089,7 @@ export class OwnedUiSessionShell {
|
|
|
1078
1089
|
}
|
|
1079
1090
|
const close = () => {
|
|
1080
1091
|
this.root.setInputSurface(null);
|
|
1081
|
-
this.
|
|
1092
|
+
this.#terminal.requestRender();
|
|
1082
1093
|
};
|
|
1083
1094
|
const labels = method.options.map(option => option.label);
|
|
1084
1095
|
const component = createPiShellExtensionSelector(method.title, labels, label => {
|
|
@@ -1089,74 +1100,17 @@ export class OwnedUiSessionShell {
|
|
|
1089
1100
|
void this.runWorkflow({ command: "login", argument: "", selection });
|
|
1090
1101
|
}, close);
|
|
1091
1102
|
this.root.setInputSurface(component);
|
|
1092
|
-
this.
|
|
1103
|
+
this.#terminal.requestRender();
|
|
1093
1104
|
}
|
|
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
|
-
}
|
|
1105
|
+
showTreeSelector(initialSelectedId) {
|
|
1106
|
+
return this.#presenters.openTreeSelector(this.#dialogHost, (entryId, skipSummaryPrompt) => {
|
|
1107
|
+
void this.#completeTreeSelection(entryId, skipSummaryPrompt);
|
|
1108
|
+
}, initialSelectedId);
|
|
1155
1109
|
}
|
|
1156
1110
|
showLoginAuthTypeSelector() {
|
|
1157
1111
|
const close = () => {
|
|
1158
1112
|
this.root.setInputSurface(null);
|
|
1159
|
-
this.
|
|
1113
|
+
this.#terminal.requestRender();
|
|
1160
1114
|
};
|
|
1161
1115
|
const labels = ["Sign in with an account", "Sign in with an API key"];
|
|
1162
1116
|
const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
|
|
@@ -1164,18 +1118,18 @@ export class OwnedUiSessionShell {
|
|
|
1164
1118
|
this.showLoginProviderSelector(label === labels[0] ? "oauth" : "api_key");
|
|
1165
1119
|
}, close);
|
|
1166
1120
|
this.root.setInputSurface(component);
|
|
1167
|
-
this.
|
|
1121
|
+
this.#terminal.requestRender();
|
|
1168
1122
|
}
|
|
1169
1123
|
showLoginProviderSelector(authType, initialSearchInput) {
|
|
1170
|
-
const options = this.backend.pinnedLoginOptions(authType);
|
|
1124
|
+
const options = this.backend.catalog.pinnedLoginOptions(authType);
|
|
1171
1125
|
if (options.length === 0) {
|
|
1172
1126
|
this.root.appendWorkflowStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
|
|
1173
|
-
this.
|
|
1127
|
+
this.#terminal.requestRender();
|
|
1174
1128
|
return;
|
|
1175
1129
|
}
|
|
1176
1130
|
const close = () => {
|
|
1177
1131
|
this.root.setInputSurface(null);
|
|
1178
|
-
this.
|
|
1132
|
+
this.#terminal.requestRender();
|
|
1179
1133
|
};
|
|
1180
1134
|
const component = createPiShellAuthProviderSelector("login", options, id => {
|
|
1181
1135
|
close();
|
|
@@ -1186,84 +1140,13 @@ export class OwnedUiSessionShell {
|
|
|
1186
1140
|
this.showLoginAuthTypeSelector();
|
|
1187
1141
|
}, initialSearchInput);
|
|
1188
1142
|
this.root.setInputSurface(component);
|
|
1189
|
-
this.
|
|
1143
|
+
this.#terminal.requestRender();
|
|
1190
1144
|
}
|
|
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();
|
|
1145
|
+
showSessionSelector() {
|
|
1146
|
+
return this.#presenters.openSessionSelector(this.#dialogHost, () => { void this.shutdown(); });
|
|
1215
1147
|
}
|
|
1216
1148
|
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();
|
|
1149
|
+
this.#presenters.openSettingsSelector(this.#dialogHost);
|
|
1267
1150
|
}
|
|
1268
1151
|
shutdown() {
|
|
1269
1152
|
this.#shutdownPromise ??= this.#shutdown();
|
|
@@ -1271,7 +1154,13 @@ export class OwnedUiSessionShell {
|
|
|
1271
1154
|
}
|
|
1272
1155
|
async #shutdown() {
|
|
1273
1156
|
try {
|
|
1274
|
-
const
|
|
1157
|
+
const quit = this.backend.workflows.executeWorkflow({ command: "quit", argument: "" });
|
|
1158
|
+
// Invariant: a slow or hung engine quit never holds the terminal past the cleanup deadline. The
|
|
1159
|
+
// shell restores it then, and the result still waits for the engine to finish quitting.
|
|
1160
|
+
const timedOut = await boundedCleanup(() => quit.catch(() => { })).then(() => false, () => true);
|
|
1161
|
+
if (timedOut)
|
|
1162
|
+
await this.dispose();
|
|
1163
|
+
const result = await quit;
|
|
1275
1164
|
await this.dispose();
|
|
1276
1165
|
return workflowAdapterResult(result);
|
|
1277
1166
|
}
|
|
@@ -1291,26 +1180,26 @@ export class OwnedUiSessionShell {
|
|
|
1291
1180
|
if (this.#disposed)
|
|
1292
1181
|
return;
|
|
1293
1182
|
this.root.appendWorkflowResult({ command: "name", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
1294
|
-
this.
|
|
1183
|
+
this.#terminal.requestRender();
|
|
1295
1184
|
});
|
|
1296
1185
|
return { outcome: "completed", diagnostic: null };
|
|
1297
1186
|
}
|
|
1298
|
-
const copyGeneration = request.command === "copy" ? this.backend.sessionBindingGeneration : undefined;
|
|
1187
|
+
const copyGeneration = request.command === "copy" ? this.backend.identity.sessionBindingGeneration : undefined;
|
|
1299
1188
|
if (request.command === "login" && request.selection !== undefined) {
|
|
1300
|
-
const setup = this.backend.pinnedAmbientAuthentication(request.selection);
|
|
1189
|
+
const setup = this.backend.catalog.pinnedAmbientAuthentication(request.selection);
|
|
1301
1190
|
if (setup) {
|
|
1302
1191
|
const close = () => {
|
|
1303
1192
|
this.root.setInputSurface(null);
|
|
1304
|
-
this.
|
|
1193
|
+
this.#terminal.requestRender();
|
|
1305
1194
|
};
|
|
1306
1195
|
const dialog = createPiShellLoginDialog({
|
|
1307
|
-
getColumns: () => this.
|
|
1308
|
-
getRows: () => this.
|
|
1309
|
-
requestRender: () => this.
|
|
1196
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
1197
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
1198
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
1310
1199
|
}, setup.providerId, close, setup.providerName, setup.title);
|
|
1311
1200
|
dialog.showInfo(setup.message, [], true);
|
|
1312
1201
|
this.root.setInputSurface(dialog);
|
|
1313
|
-
this.
|
|
1202
|
+
this.#terminal.requestRender();
|
|
1314
1203
|
return { outcome: "completed", diagnostic: null };
|
|
1315
1204
|
}
|
|
1316
1205
|
}
|
|
@@ -1362,10 +1251,10 @@ export class OwnedUiSessionShell {
|
|
|
1362
1251
|
return { outcome: "completed", diagnostic: null };
|
|
1363
1252
|
}
|
|
1364
1253
|
if (request.command === "reload") {
|
|
1365
|
-
const blocked = this.backend.reloadBlockedResult();
|
|
1254
|
+
const blocked = this.backend.workflows.reloadBlockedResult();
|
|
1366
1255
|
if (blocked) {
|
|
1367
1256
|
this.root.appendWorkflowResult(blocked);
|
|
1368
|
-
this.
|
|
1257
|
+
this.#terminal.requestRender();
|
|
1369
1258
|
return workflowAdapterResult(blocked);
|
|
1370
1259
|
}
|
|
1371
1260
|
this.root.resetExtensionUi();
|
|
@@ -1375,20 +1264,20 @@ export class OwnedUiSessionShell {
|
|
|
1375
1264
|
: undefined;
|
|
1376
1265
|
const shareSurface = shareDialog === undefined ? undefined
|
|
1377
1266
|
: (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)({
|
|
1378
|
-
getColumns: () => this.
|
|
1379
|
-
getRows: () => this.
|
|
1380
|
-
requestRender: () => this.
|
|
1267
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
1268
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
1269
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
1381
1270
|
}, "Creating gist…");
|
|
1382
1271
|
const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
|
|
1383
1272
|
const now = this.#reloadPresentation?.now ?? Date.now;
|
|
1384
1273
|
const shownAt = now();
|
|
1385
1274
|
if (operationSurface) {
|
|
1386
1275
|
this.root.setInputSurface(operationSurface);
|
|
1387
|
-
this.
|
|
1276
|
+
this.#terminal.requestRender();
|
|
1388
1277
|
}
|
|
1389
1278
|
let result;
|
|
1390
1279
|
try {
|
|
1391
|
-
result = await this.backend.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
|
|
1280
|
+
result = await this.backend.workflows.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
|
|
1392
1281
|
}
|
|
1393
1282
|
finally {
|
|
1394
1283
|
if (operationSurface) {
|
|
@@ -1397,10 +1286,10 @@ export class OwnedUiSessionShell {
|
|
|
1397
1286
|
if (request.command === "reload")
|
|
1398
1287
|
await this.#holdReloadSurface(now() - shownAt);
|
|
1399
1288
|
this.root.setInputSurface(null);
|
|
1400
|
-
this.
|
|
1289
|
+
this.#terminal.requestRender();
|
|
1401
1290
|
}
|
|
1402
1291
|
}
|
|
1403
|
-
if (copyGeneration !== undefined && (this.#disposed || copyGeneration !== this.backend.sessionBindingGeneration)) {
|
|
1292
|
+
if (copyGeneration !== undefined && (this.#disposed || copyGeneration !== this.backend.identity.sessionBindingGeneration)) {
|
|
1404
1293
|
return workflowAdapterResult({ command: "copy", outcome: "cancelled", message: "" });
|
|
1405
1294
|
}
|
|
1406
1295
|
if (result.outcome === "requires-selection" && request.command === "model") {
|
|
@@ -1426,7 +1315,7 @@ export class OwnedUiSessionShell {
|
|
|
1426
1315
|
}
|
|
1427
1316
|
if (result.outcome === "requires-selection" || result.outcome === "requires-confirmation") {
|
|
1428
1317
|
this.root.appendWorkflowResult({ command: request.command, outcome: "failed", message: `Owned controller missing for ${request.command}` });
|
|
1429
|
-
this.
|
|
1318
|
+
this.#terminal.requestRender();
|
|
1430
1319
|
return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
|
|
1431
1320
|
}
|
|
1432
1321
|
if (request.command === "reload" && result.outcome === "completed") {
|
|
@@ -1445,7 +1334,7 @@ export class OwnedUiSessionShell {
|
|
|
1445
1334
|
else {
|
|
1446
1335
|
this.root.appendWorkflowResult(result);
|
|
1447
1336
|
}
|
|
1448
|
-
this.
|
|
1337
|
+
this.#terminal.requestRender();
|
|
1449
1338
|
return workflowAdapterResult(result);
|
|
1450
1339
|
}
|
|
1451
1340
|
async #holdReloadSurface(visibleMs) {
|
|
@@ -1457,16 +1346,16 @@ export class OwnedUiSessionShell {
|
|
|
1457
1346
|
await sleep(remaining);
|
|
1458
1347
|
}
|
|
1459
1348
|
showTrustSelector() {
|
|
1460
|
-
const context = this.backend.pinnedProjectTrustContext();
|
|
1349
|
+
const context = this.backend.catalog.pinnedProjectTrustContext();
|
|
1461
1350
|
const close = () => {
|
|
1462
1351
|
this.root.setInputSurface(null);
|
|
1463
|
-
this.
|
|
1352
|
+
this.#terminal.requestRender();
|
|
1464
1353
|
};
|
|
1465
1354
|
const component = createPiShellTrustSelector({
|
|
1466
1355
|
...context,
|
|
1467
1356
|
onSelect: selection => {
|
|
1468
1357
|
try {
|
|
1469
|
-
this.backend.persistProjectTrust(selection.updates);
|
|
1358
|
+
this.backend.catalog.persistProjectTrust(selection.updates);
|
|
1470
1359
|
close();
|
|
1471
1360
|
this.root.appendWorkflowStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
|
|
1472
1361
|
}
|
|
@@ -1474,196 +1363,45 @@ export class OwnedUiSessionShell {
|
|
|
1474
1363
|
close();
|
|
1475
1364
|
this.root.appendWorkflowResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
1476
1365
|
}
|
|
1477
|
-
this.
|
|
1366
|
+
this.#terminal.requestRender();
|
|
1478
1367
|
},
|
|
1479
1368
|
onCancel: close,
|
|
1480
1369
|
});
|
|
1481
1370
|
this.root.setInputSurface(component);
|
|
1482
|
-
this.
|
|
1371
|
+
this.#terminal.requestRender();
|
|
1483
1372
|
}
|
|
1484
1373
|
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));
|
|
1374
|
+
this.#presenters.openScopedModelsSelector(this.#dialogHost);
|
|
1552
1375
|
}
|
|
1553
1376
|
// Invariant: bare A1 routes `models`; the comparison profile keeps the pinned `model`/`scoped-models` pair. Hidden routes are shared.
|
|
1554
1377
|
#isWorkflowRoute(value) {
|
|
1555
1378
|
return workflowCommandNames(this.#customViewport ? "bare" : "comparison").includes(value)
|
|
1556
|
-
||
|
|
1379
|
+
|| OWNED_UI_HIDDEN_COMMAND_NAMES.includes(value);
|
|
1557
1380
|
}
|
|
1558
1381
|
/** The bare-A1 unified Models dialog: switch on Enter, scope on Space, persist on Ctrl+S, all through the engine. */
|
|
1559
1382
|
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));
|
|
1636
|
-
}
|
|
1637
|
-
// Invariant: pointer reporting is disabled on every path that ends the owning screen.
|
|
1638
|
-
#setPointerReporting(enabled, forceOff = false) {
|
|
1639
|
-
const effective = forceOff ? false : this.#customViewport || enabled;
|
|
1640
|
-
if (this.#pointerReporting === effective)
|
|
1641
|
-
return;
|
|
1642
|
-
this.#pointerReporting = effective;
|
|
1643
|
-
if (!this.runtime.active)
|
|
1644
|
-
return;
|
|
1645
|
-
this.runtime.writeControl(effective ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
1383
|
+
this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
|
|
1646
1384
|
}
|
|
1385
|
+
// Invariant: one teardown per presenter, starting on a microtask so never inside the event listener that
|
|
1386
|
+
// requested it. Callers during the teardown share its outcome; a later call finds it done. Closing the
|
|
1387
|
+
// terminal host's last presenter ends the terminal.
|
|
1647
1388
|
dispose() {
|
|
1648
1389
|
if (this.#disposePromise !== undefined)
|
|
1649
1390
|
return this.#disposePromise;
|
|
1650
1391
|
if (this.#disposed)
|
|
1651
1392
|
return Promise.resolve();
|
|
1652
|
-
|
|
1393
|
+
this.#disposed = true;
|
|
1394
|
+
const pending = Promise.resolve().then(() => this.#host.close(this));
|
|
1653
1395
|
this.#disposePromise = pending;
|
|
1654
1396
|
const clear = () => { if (this.#disposePromise === pending)
|
|
1655
1397
|
this.#disposePromise = undefined; };
|
|
1656
1398
|
void pending.then(clear, clear);
|
|
1657
1399
|
return pending;
|
|
1658
1400
|
}
|
|
1659
|
-
|
|
1401
|
+
release(terminal) {
|
|
1402
|
+
if (this.#released !== undefined)
|
|
1403
|
+
return { exitText: "", failures: [], settle: async () => [] };
|
|
1660
1404
|
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
1405
|
const failures = [];
|
|
1668
1406
|
const attempt = (action) => { try {
|
|
1669
1407
|
action();
|
|
@@ -1671,13 +1409,17 @@ export class OwnedUiSessionShell {
|
|
|
1671
1409
|
catch (error) {
|
|
1672
1410
|
failures.push(error);
|
|
1673
1411
|
} };
|
|
1412
|
+
// Invariant: events stop first, so nothing re-enters the view during history close, outro, or restoration.
|
|
1413
|
+
attempt(() => this.#unsubscribe());
|
|
1414
|
+
this.#copyIntentSequence += 1;
|
|
1415
|
+
this.#responseCopy?.dispose();
|
|
1416
|
+
this.#copyExecutor?.dispose();
|
|
1417
|
+
this.#cancelWaitingImages();
|
|
1674
1418
|
let pasteCleanup = Promise.resolve();
|
|
1675
1419
|
let historyCleanup = Promise.resolve(true);
|
|
1676
1420
|
attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
|
|
1677
1421
|
attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
|
|
1678
1422
|
attempt(() => this.root.clearViewportPointerState());
|
|
1679
|
-
attempt(() => this.#setPointerReporting(false, true));
|
|
1680
|
-
attempt(() => this.#removeViewportPreInput());
|
|
1681
1423
|
attempt(() => this.#streamPresentation.dispose());
|
|
1682
1424
|
attempt(() => {
|
|
1683
1425
|
if (this.#startupRouteTimer !== undefined)
|
|
@@ -1689,88 +1431,41 @@ export class OwnedUiSessionShell {
|
|
|
1689
1431
|
attempt(() => this.#unsubscribePromptImages());
|
|
1690
1432
|
attempt(() => this.#unsubscribeSkills());
|
|
1691
1433
|
attempt(() => this.#unsubscribeSettings());
|
|
1692
|
-
let
|
|
1434
|
+
let exitText = "";
|
|
1693
1435
|
attempt(() => {
|
|
1694
|
-
const exitMode = this.backend.disposed ? this.#fullscreenExitOutput : this.backend.
|
|
1695
|
-
const resume = this.backend.currentSessionResumeMetadata();
|
|
1436
|
+
const exitMode = this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput;
|
|
1437
|
+
const resume = this.backend.identity.currentSessionResumeMetadata();
|
|
1696
1438
|
const resumeHint = resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
|
|
1697
1439
|
// Invariant: bare A1 leaves only the hint behind; the pinned comparison profile still
|
|
1698
1440
|
// honors fullscreenExitOutput, including the styled transcript.
|
|
1699
|
-
|
|
1441
|
+
exitText = terminal.mode !== "fullscreen" ? ""
|
|
1700
1442
|
: this.#customViewport || exitMode === "resume-hint" ? resumeHint
|
|
1701
|
-
: [this.root.exitTranscript(
|
|
1443
|
+
: [this.root.exitTranscript(terminal.columns), resumeHint].filter(Boolean).join("\n\n");
|
|
1702
1444
|
});
|
|
1703
1445
|
attempt(() => this.#unbindClipboardWriter());
|
|
1704
1446
|
attempt(() => this.#unbindPiSettings());
|
|
1705
1447
|
attempt(() => this.#unbindTerminalSettings());
|
|
1706
1448
|
attempt(() => this.#unbindShutdownSettings());
|
|
1707
|
-
attempt(() => this
|
|
1449
|
+
attempt(() => this.backend.workflows.setWorkflowInteractionHost(DETACHED_WORKFLOW_INTERACTION));
|
|
1708
1450
|
attempt(() => this.#dialogHandle?.hide());
|
|
1709
1451
|
attempt(() => this.#extensionBridge.dispose());
|
|
1710
1452
|
attempt(() => this.#programStatus.clear());
|
|
1711
|
-
// Invariant:
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
}
|
|
1728
|
-
// Invariant: the snapshot is synchronous; the outro module itself loads only at quit.
|
|
1729
|
-
#captureQuitOutroFrame() {
|
|
1730
|
-
const outro = this.#quitOutro;
|
|
1731
|
-
if (outro === undefined || !outro.interactive || !this.#customViewport || this.#damageTerminal === null)
|
|
1732
|
-
return null;
|
|
1733
|
-
if (!this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1734
|
-
return null;
|
|
1735
|
-
try {
|
|
1736
|
-
if (!outro.snapshot().enabled)
|
|
1737
|
-
return null;
|
|
1738
|
-
const viewport = this.runtime.viewport();
|
|
1739
|
-
return {
|
|
1740
|
-
rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows,
|
|
1741
|
-
settings: { effect: QUIT_OUTRO_EFFECT, durationMs: QUIT_OUTRO_DURATION_MS },
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
catch {
|
|
1745
|
-
return null;
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
#freezeQuitPresentation() {
|
|
1749
|
-
if (!this.#customViewport || !this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1750
|
-
return;
|
|
1751
|
-
this.runtime.freezePresentation();
|
|
1752
|
-
}
|
|
1753
|
-
// Rationale: any failure here only skips the effect; restoration always follows.
|
|
1754
|
-
async #playQuitOutro(capture) {
|
|
1755
|
-
const outro = this.#quitOutro;
|
|
1756
|
-
if (capture === null || outro === undefined || !this.runtime.active)
|
|
1757
|
-
return;
|
|
1758
|
-
try {
|
|
1759
|
-
// Rationale: the effects stay off the startup graph; quit is the only time they load.
|
|
1760
|
-
const { captureQuitOutroFrame, playQuitOutro } = await import("./quit-outro.js");
|
|
1761
|
-
const frame = captureQuitOutroFrame(capture.rows, capture.columns, capture.height);
|
|
1762
|
-
if (frame === null || !this.runtime.active)
|
|
1763
|
-
return;
|
|
1764
|
-
await playQuitOutro(frame, capture.settings.effect, capture.settings.durationMs, {
|
|
1765
|
-
write: data => this.runtime.writeControl(data),
|
|
1766
|
-
...(outro.now === undefined ? {} : { now: outro.now }),
|
|
1767
|
-
...(outro.sleep === undefined ? {} : { sleep: outro.sleep }),
|
|
1768
|
-
...(outro.seed === undefined ? {} : { seed: outro.seed }),
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
catch {
|
|
1772
|
-
// Rationale: a failed or interrupted effect must never hold the terminal; restoration follows.
|
|
1773
|
-
}
|
|
1453
|
+
// Invariant: the host settles a presenter only once the terminal is restored, so these never stall it.
|
|
1454
|
+
const settle = async () => {
|
|
1455
|
+
const settled = [];
|
|
1456
|
+
try {
|
|
1457
|
+
this.root.dispose();
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
settled.push(error);
|
|
1461
|
+
}
|
|
1462
|
+
await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
|
|
1463
|
+
await boundedCleanup(() => pasteCleanup).catch(error => settled.push(error));
|
|
1464
|
+
await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => settled.push(error));
|
|
1465
|
+
return settled;
|
|
1466
|
+
};
|
|
1467
|
+
this.#released = { exitText, failures, settle };
|
|
1468
|
+
return this.#released;
|
|
1774
1469
|
}
|
|
1775
1470
|
#settleStoppedLifecycle() {
|
|
1776
1471
|
void this.dispose().then(() => this.#resolveStoppedLifecycle(), () => this.#resolveStoppedLifecycle());
|
|
@@ -1798,9 +1493,9 @@ export class OwnedUiSessionShell {
|
|
|
1798
1493
|
return view;
|
|
1799
1494
|
}
|
|
1800
1495
|
#syncTerminalProgress(view) {
|
|
1801
|
-
if (!this.
|
|
1496
|
+
if (!this.#terminal.running)
|
|
1802
1497
|
return;
|
|
1803
|
-
this.
|
|
1498
|
+
this.#terminal.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
|
|
1804
1499
|
if (view.lifecycle === "stopped" || view.lifecycle === "stopping") {
|
|
1805
1500
|
this.#programStatus.clear();
|
|
1806
1501
|
return;
|
|
@@ -1816,17 +1511,17 @@ export class OwnedUiSessionShell {
|
|
|
1816
1511
|
#syncView() {
|
|
1817
1512
|
this.#streamPresentation.noteImmediatePresentation();
|
|
1818
1513
|
const view = this.view();
|
|
1819
|
-
if (this.backend.sessionGeneration !== this.#sessionGeneration) {
|
|
1514
|
+
if (this.backend.identity.sessionGeneration !== this.#sessionGeneration) {
|
|
1820
1515
|
this.#copyIntentSequence += 1;
|
|
1821
1516
|
this.#responseCopy?.reset();
|
|
1822
1517
|
this.#cancelWaitingImages();
|
|
1823
1518
|
this.root.resetPendingPastes();
|
|
1824
1519
|
this.#promptSuggestions?.invalidate();
|
|
1825
|
-
this.#sessionGeneration = this.backend.sessionGeneration;
|
|
1520
|
+
this.#sessionGeneration = this.backend.identity.sessionGeneration;
|
|
1826
1521
|
this.#programStatus.reset();
|
|
1827
1522
|
// 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;
|
|
1523
|
+
if (this.#sessionBindingGeneration !== this.backend.identity.sessionBindingGeneration) {
|
|
1524
|
+
this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
|
|
1830
1525
|
this.root.resetTranscript();
|
|
1831
1526
|
this.#promptHistory?.reset(view.transcript.flatMap(block => block.kind === "user" ? [block.text] : []));
|
|
1832
1527
|
}
|
|
@@ -1838,7 +1533,6 @@ export class OwnedUiSessionShell {
|
|
|
1838
1533
|
this.#dialogHandle = undefined;
|
|
1839
1534
|
this.#dialogId = undefined;
|
|
1840
1535
|
this.#dialogSource = undefined;
|
|
1841
|
-
this.#setPointerReporting(false);
|
|
1842
1536
|
this.root.setInputSurface(null);
|
|
1843
1537
|
this.root.resetExtensionUi();
|
|
1844
1538
|
this.root.resetWorkflowPresentation();
|
|
@@ -1846,7 +1540,7 @@ export class OwnedUiSessionShell {
|
|
|
1846
1540
|
this.root.update(view);
|
|
1847
1541
|
this.#syncDialog(view.dialog);
|
|
1848
1542
|
this.#presentStartupTrustWarning(view);
|
|
1849
|
-
this.
|
|
1543
|
+
this.#terminal.requestRender();
|
|
1850
1544
|
for (const listener of this.#listeners)
|
|
1851
1545
|
listener(view);
|
|
1852
1546
|
return view;
|
|
@@ -1857,16 +1551,16 @@ export class OwnedUiSessionShell {
|
|
|
1857
1551
|
if (this.#startupTrustHandled || !this.#customViewport)
|
|
1858
1552
|
return;
|
|
1859
1553
|
const diagnostic = view.diagnostics.find(candidate => candidate.code === "project-trust");
|
|
1860
|
-
if (diagnostic === undefined || !this.
|
|
1554
|
+
if (diagnostic === undefined || !this.#terminal.running)
|
|
1861
1555
|
return;
|
|
1862
1556
|
this.#startupTrustHandled = true;
|
|
1863
1557
|
this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
|
|
1864
1558
|
}
|
|
1865
1559
|
#presentStartupRoute() {
|
|
1866
1560
|
const pending = this.#startupRoute;
|
|
1867
|
-
if (pending === undefined || this.#disposed || !this.
|
|
1561
|
+
if (pending === undefined || this.#disposed || !this.#terminal.running)
|
|
1868
1562
|
return;
|
|
1869
|
-
if (this.
|
|
1563
|
+
if (this.#terminal.hasOverlay() || !this.root.usesDefaultInputSurface()) {
|
|
1870
1564
|
this.#startupRouteTimer = setTimeout(() => {
|
|
1871
1565
|
this.#startupRouteTimer = undefined;
|
|
1872
1566
|
this.#presentStartupRoute();
|
|
@@ -1881,121 +1575,33 @@ export class OwnedUiSessionShell {
|
|
|
1881
1575
|
const surface = this.#routeHost?.open(route, input) ?? null;
|
|
1882
1576
|
if (surface === null)
|
|
1883
1577
|
return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
|
|
1884
|
-
if (!this.
|
|
1578
|
+
if (!this.#terminal.running)
|
|
1885
1579
|
return { outcome: "failed", diagnostic: "runtime is not active" };
|
|
1886
1580
|
this.root.dismissNotice();
|
|
1887
1581
|
this.#dialogHandle?.hide();
|
|
1888
1582
|
this.#dialogSource = "route";
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
const removeInterruptWatch = this.runtime.addInputListener(data => {
|
|
1897
|
-
if (!data.includes(INTERRUPT))
|
|
1898
|
-
return undefined;
|
|
1899
|
-
// Protocol: route hosts decide whether one interrupt closes their app. Raw-input forwarding
|
|
1900
|
-
// is required because Pi handles Ctrl+C before the fullscreen overlay receives normal input.
|
|
1901
|
-
const consumedBySurface = surface.handleInput(INTERRUPT);
|
|
1902
|
-
if (surface.isClosed()) {
|
|
1903
|
-
armedAt = 0;
|
|
1904
|
-
closeSurface();
|
|
1905
|
-
return { consume: true };
|
|
1906
|
-
}
|
|
1907
|
-
if (consumedBySurface) {
|
|
1908
|
-
armedAt = 0;
|
|
1909
|
-
this.runtime.requestRender();
|
|
1910
|
-
return { consume: true };
|
|
1911
|
-
}
|
|
1912
|
-
const now = Date.now();
|
|
1913
|
-
if (armedAt !== 0 && now - armedAt <= INTERRUPT_CHORD_MS) {
|
|
1914
|
-
armedAt = 0;
|
|
1915
|
-
closeSurface();
|
|
1916
|
-
void this.shutdown();
|
|
1917
|
-
return { consume: true };
|
|
1918
|
-
}
|
|
1919
|
-
armedAt = now;
|
|
1920
|
-
this.runtime.requestRender();
|
|
1921
|
-
// Invariant: the presented screen owns the chord, so the pinned shell never sees a
|
|
1922
|
-
// stray interrupt while it is up.
|
|
1923
|
-
return { consume: true };
|
|
1924
|
-
});
|
|
1925
|
-
let removeSurfacePreInput = () => { };
|
|
1926
|
-
let rendered = false;
|
|
1927
|
-
let closed = false;
|
|
1928
|
-
const closeSurface = () => {
|
|
1929
|
-
if (closed)
|
|
1930
|
-
return;
|
|
1931
|
-
closed = true;
|
|
1932
|
-
removeSurfacePreInput();
|
|
1933
|
-
removeInterruptWatch();
|
|
1934
|
-
this.#setPointerReporting(false);
|
|
1935
|
-
this.#dialogHandle?.hide();
|
|
1936
|
-
this.#dialogHandle = undefined;
|
|
1937
|
-
this.#dialogId = undefined;
|
|
1938
|
-
this.#dialogSource = undefined;
|
|
1939
|
-
if (rendered) {
|
|
1940
|
-
try {
|
|
1941
|
-
void onClosed?.()?.catch(() => undefined);
|
|
1583
|
+
const handle = this.#terminal.openRoute(surface, {
|
|
1584
|
+
exit: () => { void this.shutdown(); },
|
|
1585
|
+
closed: rendered => {
|
|
1586
|
+
if (this.#dialogHandle === handle) {
|
|
1587
|
+
this.#dialogHandle = undefined;
|
|
1588
|
+
this.#dialogId = undefined;
|
|
1589
|
+
this.#dialogSource = undefined;
|
|
1942
1590
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
// at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
|
|
1949
|
-
// and consume even unhandled reports so settings content is never selected.
|
|
1950
|
-
removeSurfacePreInput = this.runtime.addPreInputListener(data => {
|
|
1951
|
-
const { events, rest } = parseMouseInput(data);
|
|
1952
|
-
if (events.length === 0)
|
|
1953
|
-
return undefined;
|
|
1954
|
-
for (const event of events)
|
|
1955
|
-
surface.handleMouse(event);
|
|
1956
|
-
if (surface.isClosed())
|
|
1957
|
-
closeSurface();
|
|
1958
|
-
else
|
|
1959
|
-
this.runtime.requestRender();
|
|
1960
|
-
return rest.length === 0 ? { consume: true } : { data: rest };
|
|
1961
|
-
});
|
|
1962
|
-
const rows = () => Math.max(1, this.runtime.viewport().rows);
|
|
1963
|
-
const component = {
|
|
1964
|
-
render: (width) => {
|
|
1965
|
-
const frame = [...surface.render(Math.max(1, width), rows())];
|
|
1966
|
-
rendered = true;
|
|
1967
|
-
return frame;
|
|
1968
|
-
},
|
|
1969
|
-
handleInput: (data) => {
|
|
1970
|
-
const { events, rest } = parseMouseInput(data);
|
|
1971
|
-
for (const event of events)
|
|
1972
|
-
surface.handleMouse(event);
|
|
1973
|
-
if (rest.length > 0)
|
|
1974
|
-
surface.handleInput(rest);
|
|
1975
|
-
if (surface.isClosed()) {
|
|
1976
|
-
closeSurface();
|
|
1977
|
-
return;
|
|
1591
|
+
if (rendered) {
|
|
1592
|
+
try {
|
|
1593
|
+
void onClosed?.()?.catch(() => undefined);
|
|
1594
|
+
}
|
|
1595
|
+
catch { /* Rationale: a failed acknowledgement stays pending. */ }
|
|
1978
1596
|
}
|
|
1979
|
-
this.runtime.requestRender();
|
|
1980
1597
|
},
|
|
1981
|
-
invalidate: () => this.runtime.requestRender(),
|
|
1982
|
-
};
|
|
1983
|
-
surface.onRenderRequested(() => this.runtime.requestRender());
|
|
1984
|
-
surface.onExitRequested(() => {
|
|
1985
|
-
closeSurface();
|
|
1986
|
-
void this.shutdown();
|
|
1987
|
-
});
|
|
1988
|
-
this.#dialogHandle = this.runtime.showOverlay(component, {
|
|
1989
|
-
width: "100%",
|
|
1990
|
-
maxHeight: "100%",
|
|
1991
|
-
anchor: "top-left",
|
|
1992
|
-
inputCoordination: "owned",
|
|
1993
1598
|
});
|
|
1599
|
+
this.#dialogHandle = handle;
|
|
1994
1600
|
this.#dialogId = surface.id;
|
|
1995
1601
|
return { outcome: "completed", diagnostic: null };
|
|
1996
1602
|
}
|
|
1997
1603
|
#syncDialog(dialog) {
|
|
1998
|
-
if (!this.
|
|
1604
|
+
if (!this.#terminal.running)
|
|
1999
1605
|
return;
|
|
2000
1606
|
if (dialog === null) {
|
|
2001
1607
|
// Invariant: locally owned routes (notably /settings) are independent of backend
|
|
@@ -2026,7 +1632,7 @@ export class OwnedUiSessionShell {
|
|
|
2026
1632
|
this.#dialogSource = undefined;
|
|
2027
1633
|
},
|
|
2028
1634
|
});
|
|
2029
|
-
this.#dialogHandle = this.
|
|
1635
|
+
this.#dialogHandle = this.#terminal.showOverlay(component, {
|
|
2030
1636
|
width: "70%",
|
|
2031
1637
|
maxHeight: "80%",
|
|
2032
1638
|
anchor: "center",
|
|
@@ -2059,24 +1665,24 @@ export class OwnedUiSessionShell {
|
|
|
2059
1665
|
return this.#execute({
|
|
2060
1666
|
type: this.view().lifecycle === "busy" ? "steer" : "prompt",
|
|
2061
1667
|
correlationId: this.#correlation("prompt-command"),
|
|
2062
|
-
sessionId: this.backend.sessionId,
|
|
1668
|
+
sessionId: this.backend.identity.sessionId,
|
|
2063
1669
|
text,
|
|
2064
1670
|
});
|
|
2065
1671
|
}
|
|
2066
1672
|
#installAutocompleteCommands() {
|
|
2067
|
-
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
1673
|
+
this.root.editor.setAutocompleteCommands(this.backend.workflows.workflowAutocompleteCommands());
|
|
2068
1674
|
this.#installedCommandSignature = this.#commandListSignature();
|
|
2069
1675
|
}
|
|
2070
1676
|
#commandListSignature() {
|
|
2071
|
-
return JSON.stringify([this.#skills?.presentation() ?? "expand", this.backend.disposed ? [] : this.backend.workflowAutocompleteCommands().map(command => command.name)]);
|
|
1677
|
+
return JSON.stringify([this.#skills?.presentation() ?? "expand", this.backend.identity.disposed ? [] : this.backend.workflows.workflowAutocompleteCommands().map(command => command.name)]);
|
|
2072
1678
|
}
|
|
2073
1679
|
// Invariant: collapse applies only while the engine registers skill commands; otherwise there is nothing to collapse.
|
|
2074
1680
|
#skillsCollapsed() {
|
|
2075
1681
|
return this.#skills !== null && this.#skills.presentation() === "collapse"
|
|
2076
|
-
&& !this.backend.disposed && this.backend.
|
|
1682
|
+
&& !this.backend.identity.disposed && this.backend.settings.snapshot().enableSkillCommands;
|
|
2077
1683
|
}
|
|
2078
1684
|
#skillSummaries() {
|
|
2079
|
-
return skillsFromCommands(this.backend.workflowAutocompleteCommands());
|
|
1685
|
+
return skillsFromCommands(this.backend.workflows.workflowAutocompleteCommands());
|
|
2080
1686
|
}
|
|
2081
1687
|
// Protocol: bare "/skills" opens the dialog, "/skills <name> [args]" applies directly, and an unknown name is a command outcome.
|
|
2082
1688
|
async #runSkillsCommand(text, argument) {
|
|
@@ -2092,7 +1698,7 @@ export class OwnedUiSessionShell {
|
|
|
2092
1698
|
if (skill === undefined) {
|
|
2093
1699
|
const message = "Unknown skill: " + token;
|
|
2094
1700
|
this.root.appendWorkflowMessage({ kind: "error", message });
|
|
2095
|
-
this.
|
|
1701
|
+
this.#terminal.requestRender();
|
|
2096
1702
|
return { outcome: "failed", diagnostic: message };
|
|
2097
1703
|
}
|
|
2098
1704
|
return this.#submitSkillPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
|
|
@@ -2104,14 +1710,14 @@ export class OwnedUiSessionShell {
|
|
|
2104
1710
|
return this.#execute({
|
|
2105
1711
|
type: this.view().lifecycle === "busy" ? "steer" : "prompt",
|
|
2106
1712
|
correlationId: this.#correlation("prompt-command"),
|
|
2107
|
-
sessionId: this.backend.sessionId,
|
|
1713
|
+
sessionId: this.backend.identity.sessionId,
|
|
2108
1714
|
text: prompt,
|
|
2109
1715
|
});
|
|
2110
1716
|
}
|
|
2111
1717
|
#treeSummary() {
|
|
2112
1718
|
return new Promise(resolve => {
|
|
2113
1719
|
this.root.setInputSurface(createPiShellExtensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
|
|
2114
|
-
this.
|
|
1720
|
+
this.#terminal.requestRender();
|
|
2115
1721
|
});
|
|
2116
1722
|
}
|
|
2117
1723
|
async #completeTreeSelection(entryId, skipSummaryPrompt) {
|
|
@@ -2134,7 +1740,7 @@ export class OwnedUiSessionShell {
|
|
|
2134
1740
|
}
|
|
2135
1741
|
}
|
|
2136
1742
|
this.root.setInputSurface(null);
|
|
2137
|
-
this.
|
|
1743
|
+
this.#terminal.requestRender();
|
|
2138
1744
|
const result = await this.runWorkflow({
|
|
2139
1745
|
command: "tree",
|
|
2140
1746
|
argument: "",
|
|
@@ -2150,9 +1756,9 @@ export class OwnedUiSessionShell {
|
|
|
2150
1756
|
#startWorkflowLogin(request) {
|
|
2151
1757
|
this.#finishWorkflowLogin();
|
|
2152
1758
|
const dialog = createPiShellLoginDialog({
|
|
2153
|
-
getColumns: () => this.
|
|
2154
|
-
getRows: () => this.
|
|
2155
|
-
requestRender: () => this.
|
|
1759
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
1760
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
1761
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
2156
1762
|
}, request.providerId, success => {
|
|
2157
1763
|
if (!success)
|
|
2158
1764
|
this.#finishWorkflowLogin();
|
|
@@ -2164,7 +1770,7 @@ export class OwnedUiSessionShell {
|
|
|
2164
1770
|
message: `Log in to ${request.providerName}`,
|
|
2165
1771
|
});
|
|
2166
1772
|
this.#syncTerminalProgress(this.view());
|
|
2167
|
-
this.
|
|
1773
|
+
this.#terminal.requestRender();
|
|
2168
1774
|
}
|
|
2169
1775
|
#requestWorkflowInput(request) {
|
|
2170
1776
|
const dialog = this.#activeLoginDialog;
|
|
@@ -2177,7 +1783,7 @@ export class OwnedUiSessionShell {
|
|
|
2177
1783
|
const restoreDialog = () => {
|
|
2178
1784
|
if (this.#activeLoginDialog === dialog)
|
|
2179
1785
|
this.root.setInputSurface(dialog);
|
|
2180
|
-
this.
|
|
1786
|
+
this.#terminal.requestRender();
|
|
2181
1787
|
};
|
|
2182
1788
|
const selector = createPiShellExtensionSelector(request.message, labels, label => {
|
|
2183
1789
|
const id = options.find(option => option.label === label)?.id;
|
|
@@ -2188,13 +1794,13 @@ export class OwnedUiSessionShell {
|
|
|
2188
1794
|
resolve(null);
|
|
2189
1795
|
});
|
|
2190
1796
|
this.root.setInputSurface(selector, false);
|
|
2191
|
-
this.
|
|
1797
|
+
this.#terminal.requestRender();
|
|
2192
1798
|
});
|
|
2193
1799
|
}
|
|
2194
1800
|
const response = request.type === "manual-code"
|
|
2195
1801
|
? dialog.showManualInput(request.message)
|
|
2196
1802
|
: dialog.showPrompt(request.message, request.placeholder);
|
|
2197
|
-
this.
|
|
1803
|
+
this.#terminal.requestRender();
|
|
2198
1804
|
return response.then(value => value, () => null);
|
|
2199
1805
|
}
|
|
2200
1806
|
#notifyWorkflowLogin(event) {
|
|
@@ -2213,7 +1819,7 @@ export class OwnedUiSessionShell {
|
|
|
2213
1819
|
dialog.showWaiting(event.message);
|
|
2214
1820
|
else
|
|
2215
1821
|
dialog.showProgress(event.message);
|
|
2216
|
-
this.
|
|
1822
|
+
this.#terminal.requestRender();
|
|
2217
1823
|
}
|
|
2218
1824
|
#finishWorkflowLogin() {
|
|
2219
1825
|
if (!this.#activeLoginDialog)
|
|
@@ -2222,11 +1828,11 @@ export class OwnedUiSessionShell {
|
|
|
2222
1828
|
this.root.setInputSurface(null);
|
|
2223
1829
|
this.#programStatus.setBlocked("authentication", undefined);
|
|
2224
1830
|
this.#syncTerminalProgress(this.view());
|
|
2225
|
-
this.
|
|
1831
|
+
this.#terminal.requestRender();
|
|
2226
1832
|
}
|
|
2227
1833
|
async #execute(command, draft) {
|
|
2228
1834
|
if (draft === undefined)
|
|
2229
|
-
return this.backend.execute(command);
|
|
1835
|
+
return this.backend.session.execute(command);
|
|
2230
1836
|
const revision = this.#editorRevision;
|
|
2231
1837
|
try {
|
|
2232
1838
|
assertOwnedUiCommand(command);
|
|
@@ -2235,7 +1841,7 @@ export class OwnedUiSessionShell {
|
|
|
2235
1841
|
return this.#recoverSubmission(draft, revision, error);
|
|
2236
1842
|
}
|
|
2237
1843
|
try {
|
|
2238
|
-
const result = await this.backend.execute(command);
|
|
1844
|
+
const result = await this.backend.session.execute(command);
|
|
2239
1845
|
if (result.outcome === "rejected")
|
|
2240
1846
|
return this.#recoverSubmission(draft, revision);
|
|
2241
1847
|
return result;
|
|
@@ -2276,18 +1882,41 @@ export class OwnedUiSessionShell {
|
|
|
2276
1882
|
try {
|
|
2277
1883
|
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
|
|
2278
1884
|
?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
|
|
2279
|
-
this.
|
|
1885
|
+
this.#terminal.requestRender();
|
|
2280
1886
|
}
|
|
2281
1887
|
catch { /* Security: error presentation cannot create another rejected submission callback. */ }
|
|
2282
1888
|
}
|
|
2283
1889
|
#simple(type) {
|
|
2284
|
-
return { type, correlationId: this.#correlation(type), sessionId: this.backend.sessionId };
|
|
1890
|
+
return { type, correlationId: this.#correlation(type), sessionId: this.backend.identity.sessionId };
|
|
2285
1891
|
}
|
|
2286
1892
|
#correlation(prefix) {
|
|
2287
1893
|
this.#sequence += 1;
|
|
2288
1894
|
return `pi-shell-${prefix}-${this.#sequence}`;
|
|
2289
1895
|
}
|
|
2290
1896
|
}
|
|
1897
|
+
/**
|
|
1898
|
+
* One session on a terminal of its own: a terminal host with this presenter attached. Disposing it ends the
|
|
1899
|
+
* terminal, as quitting the single-session product does.
|
|
1900
|
+
*/
|
|
1901
|
+
export class OwnedUiSessionShell extends OwnedUiSessionPresenter {
|
|
1902
|
+
constructor(options) {
|
|
1903
|
+
const host = new OwnedUiTerminalHost(sessionTerminalHostOptions(options));
|
|
1904
|
+
try {
|
|
1905
|
+
super(host, options);
|
|
1906
|
+
}
|
|
1907
|
+
catch (error) {
|
|
1908
|
+
void host.dispose().catch(() => { });
|
|
1909
|
+
throw error;
|
|
1910
|
+
}
|
|
1911
|
+
host.attach(this);
|
|
1912
|
+
}
|
|
1913
|
+
get runtime() {
|
|
1914
|
+
return this.terminalHost.runtime;
|
|
1915
|
+
}
|
|
1916
|
+
damagePresentationDecision() {
|
|
1917
|
+
return this.terminalHost.damagePresentationDecision;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
2291
1920
|
function isCoalescedStreamBlock(kind) {
|
|
2292
1921
|
return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
|
|
2293
1922
|
}
|
|
@@ -2317,14 +1946,6 @@ export function quoteCommandArgument(value) {
|
|
|
2317
1946
|
function dim(value) {
|
|
2318
1947
|
return `\u001b[2m${value}\u001b[22m`;
|
|
2319
1948
|
}
|
|
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
1949
|
function rejected(diagnostic) {
|
|
2329
1950
|
return { outcome: "rejected", diagnostic };
|
|
2330
1951
|
}
|
|
@@ -2338,9 +1959,4 @@ function workflowAdapterResult(result) {
|
|
|
2338
1959
|
return { outcome: "failed", diagnostic: result.message };
|
|
2339
1960
|
return { outcome: "rejected", diagnostic: result.message };
|
|
2340
1961
|
}
|
|
2341
|
-
const INTERRUPT = "\u0003";
|
|
2342
|
-
const INTERRUPT_CHORD_MS = 1_500;
|
|
2343
1962
|
const RELOAD_SURFACE_MIN_VISIBLE_MS = 400;
|
|
2344
|
-
// Rationale: the outro is not configurable; the switch only decides whether this plan plays.
|
|
2345
|
-
const QUIT_OUTRO_EFFECT = "fall";
|
|
2346
|
-
const QUIT_OUTRO_DURATION_MS = 800;
|