@timurproko/a1 0.2.6-dev.734 → 0.2.6
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/index.d.ts +2 -2
- package/dist/app/session-shell/index.js +1 -1
- package/dist/app/session-shell/session-shell-root.d.ts +9 -1
- package/dist/app/session-shell/session-shell-root.js +56 -47
- package/dist/app/session-shell/session-shell.d.ts +37 -9
- package/dist/app/session-shell/session-shell.js +266 -446
- 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.js +45 -25
- package/dist/contracts/owned-ui/index.d.ts +1 -1
- package/dist/contracts/owned-ui/session-presenters.d.ts +35 -8
- 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/resources/release-notes.json +4 -0
- package/dist/features/owned-ui/settings-app.js +3 -3
- 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/cohort-state.js +5 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +18 -2
- 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/components/value-menu.d.ts +6 -3
- package/dist/ui/components/value-menu.js +6 -5
- package/docs/architecture/boundaries.md +1 -1
- package/docs/architecture/evidence/terminal-host-spike/manual-one-pane-terminal-host.md +70 -0
- package/docs/architecture/evidence/terminal-host-spike/manual-two-by-two-basic-terminal-host.md +32 -0
- package/docs/architecture/evidence/terminal-host-spike/milestone-baseline.json +115 -0
- package/docs/architecture/evidence/terminal-host-spike/owned-ui-foundation-prerequisite.json +48 -0
- package/docs/architecture/evidence/terminal-host-spike/terminal-host-provenance.json +125 -0
- package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-1.json +56 -0
- package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-3.json +174 -0
- package/docs/architecture/evidence/terminal-host-spike/terminal-spike-acceptance-record.json +18 -0
- package/docs/architecture/terminal-host-proof-gate.md +1 -1
- package/docs/architecture/terminal-host-spike-evidence.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +1 -1
- package/docs/releases/0.2.6.md +44 -0
- package/package.json +1 -1
|
@@ -7,18 +7,13 @@ import { boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset
|
|
|
7
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 { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../ui/components/mouse.js";
|
|
11
|
-
import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
|
|
12
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
13
|
import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
|
|
16
14
|
import { createPiShellAuthProviderSelector, createPiShellDialog, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellReloadBox, createPiShellSelector, createPiShellSkillsSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
17
15
|
import { renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
18
|
-
import {
|
|
19
|
-
import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
|
|
20
|
-
import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
21
|
-
import { classifyPiTuiInput } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
16
|
+
import { OwnedUiTerminalHost, sessionTerminalHostOptions, } from "./shell-host.js";
|
|
22
17
|
import { runImageWorker } from "./image-preparation-client.js";
|
|
23
18
|
import { STREAM_PRESENTATION_INTERVAL_MS, StreamPresentationCoalescer, } from "./stream-presentation-coalescer.js";
|
|
24
19
|
import { writeSystemClipboardText, } from "./system-clipboard.js";
|
|
@@ -26,13 +21,18 @@ import { OwnedUiSessionShellRoot, shellResourceEntries, } from "./session-shell-
|
|
|
26
21
|
// Invariant: a disposed shell leaves the engine a host that holds no reference back to it.
|
|
27
22
|
const DETACHED_WORKFLOW_INTERACTION = { prompt: async () => null, notify() { } };
|
|
28
23
|
export { OwnedUiSessionShellRoot, } from "./session-shell-root.js";
|
|
29
|
-
|
|
30
|
-
|
|
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 {
|
|
31
30
|
backend;
|
|
32
31
|
#presenters;
|
|
33
32
|
#dialogHost;
|
|
34
33
|
root;
|
|
35
|
-
|
|
34
|
+
#host;
|
|
35
|
+
#terminal;
|
|
36
36
|
#cwd;
|
|
37
37
|
#listeners = new Set();
|
|
38
38
|
#unsubscribe;
|
|
@@ -58,9 +58,11 @@ export class OwnedUiSessionShell {
|
|
|
58
58
|
#editorRevision = 0;
|
|
59
59
|
#started = false;
|
|
60
60
|
#disposed = false;
|
|
61
|
+
#released;
|
|
61
62
|
#shutdownPromise;
|
|
62
63
|
#disposePromise;
|
|
63
|
-
#
|
|
64
|
+
#layoutParts;
|
|
65
|
+
#pendingClipboardWrite = Promise.resolve();
|
|
64
66
|
// Invariant: translated startup diagnostics create at most one dock notice per kind per shell.
|
|
65
67
|
#startupTrustHandled = false;
|
|
66
68
|
#customViewport;
|
|
@@ -71,13 +73,10 @@ export class OwnedUiSessionShell {
|
|
|
71
73
|
#copyExecutor;
|
|
72
74
|
#copyIntentSequence = 0;
|
|
73
75
|
#unbindClipboardWriter;
|
|
74
|
-
#damageTerminal;
|
|
75
|
-
#quitOutro;
|
|
76
76
|
#reloadPresentation;
|
|
77
77
|
#streamPresentation;
|
|
78
|
-
#
|
|
78
|
+
#inputCoordination;
|
|
79
79
|
#unsubscribeSettings;
|
|
80
|
-
#unsubscribeBackgroundSettings;
|
|
81
80
|
#unbindPiSettings;
|
|
82
81
|
#unbindTerminalSettings;
|
|
83
82
|
#unbindShutdownSettings;
|
|
@@ -93,9 +92,9 @@ export class OwnedUiSessionShell {
|
|
|
93
92
|
#sessionGeneration;
|
|
94
93
|
#sessionBindingGeneration;
|
|
95
94
|
#suggestionModelKey;
|
|
96
|
-
constructor(options) {
|
|
95
|
+
constructor(host, options) {
|
|
97
96
|
const { backend, cwd, routeHost, sessionLayout } = options.engine;
|
|
98
|
-
const { terminal, startup, viewportSettings,
|
|
97
|
+
const { terminal, startup, viewportSettings, reload: reloadPresentation, stream: streamPresentationOptions, input: inputPresentation, } = options.presentation ?? {};
|
|
99
98
|
const { clipboard, responseCopy, paste: pasteDiagnostics, pastePreparation } = options.diagnostics ?? {};
|
|
100
99
|
const promptHistory = options.history;
|
|
101
100
|
const { clipboard: sharedClipboard, keybindings: sharedKeybindings } = options.shared ?? {};
|
|
@@ -108,23 +107,22 @@ export class OwnedUiSessionShell {
|
|
|
108
107
|
this.#routeHost = routeHost ?? null;
|
|
109
108
|
this.#startupRoute = options.engine.startupRoute;
|
|
110
109
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
110
|
+
this.#inputCoordination = inputPresentation?.coordination !== false;
|
|
111
|
+
this.#host = host;
|
|
112
|
+
this.#terminal = host.connect(this);
|
|
111
113
|
// Invariant: the host's optional members follow the layout, so it is built only once the layout is known.
|
|
112
114
|
this.#dialogHost = this.#createDialogHost();
|
|
113
115
|
this.#lazySelectors = options.diagnostics?.lazySelectors;
|
|
114
116
|
this.#stopped = new Promise(resolve => {
|
|
115
117
|
this.#resolveStopped = resolve;
|
|
116
118
|
});
|
|
117
|
-
let runtime;
|
|
118
|
-
let damageTerminal;
|
|
119
|
-
let streamPresentation;
|
|
120
|
-
let pendingClipboardWrite = Promise.resolve();
|
|
121
119
|
let promptSuggestionController = null;
|
|
122
120
|
// Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
|
|
123
121
|
this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
|
|
124
122
|
this.#skills = this.#customViewport ? options.skills ?? null : null;
|
|
125
123
|
// Invariant: construction is transactional. Each binding registers its release as it is made, and a
|
|
126
124
|
// throw releases them in reverse, so a shell that never finished constructing holds nothing.
|
|
127
|
-
const constructed = [];
|
|
125
|
+
const constructed = [() => host.disconnect(this)];
|
|
128
126
|
try {
|
|
129
127
|
const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
|
|
130
128
|
this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
|
|
@@ -133,23 +131,23 @@ export class OwnedUiSessionShell {
|
|
|
133
131
|
...(terminal === undefined && clipboard === undefined ? {} : { destination: "terminal" }),
|
|
134
132
|
...(clipboard?.writeText === undefined ? {} : { writeText: (text, signal) => clipboard.writeText(text, signal) }),
|
|
135
133
|
...(terminalCopy ? { terminal: { submit: async (control, signal) => {
|
|
136
|
-
if (signal.aborted || this.#disposed || !
|
|
134
|
+
if (signal.aborted || this.#disposed || !this.#terminal.running)
|
|
137
135
|
throw new Error("Copy canceled");
|
|
138
136
|
// Performance: never grow the real terminal's pending buffer with another clipboard payload.
|
|
139
137
|
if (terminal === undefined && process.stdout.writableLength + control.length > MAX_COPY_CONTROL_BYTES) {
|
|
140
138
|
throw new Error("Clipboard terminal is busy");
|
|
141
139
|
}
|
|
142
|
-
|
|
140
|
+
this.#terminal.writeControl(control);
|
|
143
141
|
} } } : {}),
|
|
144
142
|
})),
|
|
145
143
|
...(responseCopy?.onEvent === undefined ? {} : { onEvent: responseCopy.onEvent }),
|
|
146
144
|
onFailure: result => {
|
|
147
|
-
if (this.#disposed || !
|
|
145
|
+
if (this.#disposed || !this.#terminal.running)
|
|
148
146
|
return;
|
|
149
147
|
this.root.appendWorkflowStatus(result.outcome === "timed-out" ? "Copy timed out; the clipboard did not respond."
|
|
150
148
|
: result.failure === "size" ? "Selection exceeds this clipboard route's size limit."
|
|
151
149
|
: "Could not copy the selection; the clipboard is unavailable.");
|
|
152
|
-
|
|
150
|
+
this.#terminal.requestRender();
|
|
153
151
|
},
|
|
154
152
|
}) : null;
|
|
155
153
|
constructed.push(() => { this.#responseCopy?.dispose(); this.#copyExecutor?.dispose(); });
|
|
@@ -175,12 +173,12 @@ export class OwnedUiSessionShell {
|
|
|
175
173
|
return text === null ? null : { kind: "text", text };
|
|
176
174
|
};
|
|
177
175
|
this.root = new OwnedUiSessionShellRoot(this.backend.session.view(), cwd, {
|
|
178
|
-
getColumns: () =>
|
|
179
|
-
getRows: () =>
|
|
180
|
-
requestRender: force =>
|
|
181
|
-
requestHyperlinkCleanup: rows =>
|
|
182
|
-
onViewportFrame: frame =>
|
|
183
|
-
overlayActive:
|
|
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(),
|
|
184
182
|
selectionActive: this.root.hasActiveSelection(),
|
|
185
183
|
replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
|
|
186
184
|
}),
|
|
@@ -197,7 +195,7 @@ export class OwnedUiSessionShell {
|
|
|
197
195
|
onThinkingCycle: () => { void this.cycleThinkingLevel(); },
|
|
198
196
|
onThinkingToggle: () => {
|
|
199
197
|
this.root.toggleThinkingVisibility();
|
|
200
|
-
this.
|
|
198
|
+
this.#terminal.requestRender();
|
|
201
199
|
},
|
|
202
200
|
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
203
201
|
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
|
|
@@ -228,15 +226,15 @@ export class OwnedUiSessionShell {
|
|
|
228
226
|
const write = () => {
|
|
229
227
|
if (this.#disposed)
|
|
230
228
|
return Promise.resolve();
|
|
231
|
-
|
|
229
|
+
this.#terminal.writeControl(`\u001b]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`);
|
|
232
230
|
return clipboard === undefined ? writeSystemClipboardText(text)
|
|
233
231
|
: clipboard.writeText?.(text) ?? Promise.resolve();
|
|
234
232
|
};
|
|
235
233
|
// Compatibility: comparison profiles retain their existing clipboard path.
|
|
236
|
-
pendingClipboardWrite = write().catch(() => { });
|
|
234
|
+
this.#pendingClipboardWrite = write().catch(() => { });
|
|
237
235
|
},
|
|
238
236
|
readClipboardContent: async (signal = new AbortController().signal) => {
|
|
239
|
-
await pendingClipboardWrite;
|
|
237
|
+
await this.#pendingClipboardWrite;
|
|
240
238
|
return readClipboard(signal);
|
|
241
239
|
},
|
|
242
240
|
captureClipboardPaste: () => {
|
|
@@ -262,73 +260,16 @@ export class OwnedUiSessionShell {
|
|
|
262
260
|
}, this.backend.identity.agentDir, this.#presenters.transcriptRenderers(), sessionLayout, {
|
|
263
261
|
resolve: assetId => this.backend.extensions.resolveTranscriptImage(assetId),
|
|
264
262
|
});
|
|
265
|
-
// Invariant: the
|
|
266
|
-
//
|
|
267
|
-
constructed.push(() =>
|
|
268
|
-
if (runtime === undefined)
|
|
269
|
-
this.root.dispose();
|
|
270
|
-
else
|
|
271
|
-
void runtime.dispose().catch(() => { });
|
|
272
|
-
});
|
|
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());
|
|
273
266
|
const initialPiSettings = this.backend.settings.snapshot();
|
|
274
|
-
|
|
275
|
-
// fullscreen surface. The pinned comparison profiles still honor Pi's mode.
|
|
276
|
-
const tuiMode = this.#customViewport
|
|
277
|
-
? "fullscreen"
|
|
278
|
-
: this.backend.identity.disposed ? "regular" : initialPiSettings.tuiMode;
|
|
279
|
-
const runtimeOptions = {
|
|
280
|
-
root: this.root,
|
|
281
|
-
mode: tuiMode,
|
|
282
|
-
...(this.#customViewport ? {
|
|
283
|
-
// Invariant: the owned shell enables and routes mouse reports itself. Pi's
|
|
284
|
-
// enclosing fullscreen renderer must never establish a competing white selection.
|
|
285
|
-
mouse: false,
|
|
286
|
-
consumeUnhandledMouse: true,
|
|
287
|
-
onOverlayGeometry: surfaces => this.root.setViewportOverlaySurfaces(surfaces),
|
|
288
|
-
decorateTerminal: (terminal) => {
|
|
289
|
-
damageTerminal = new DamageAwareTerminalAdapter(terminal, {
|
|
290
|
-
regionalScroll: process.env.TERM !== "dumb",
|
|
291
|
-
inspectHyperlinks: readVisibleHyperlinks,
|
|
292
|
-
onResize: () => streamPresentation?.noteImmediatePresentation(),
|
|
293
|
-
onHyperlinkCleanupRequired: () => runtime?.requestRender(true),
|
|
294
|
-
});
|
|
295
|
-
return damageTerminal;
|
|
296
|
-
},
|
|
297
|
-
...(inputPresentation?.coordination === false ? {} : {
|
|
298
|
-
inputCoordination: {
|
|
299
|
-
classify: (data, focusedOverlay) => classifyPiTuiInput(data, focusedOverlay ?? this.root.inputCoordinationSurface()),
|
|
300
|
-
onReceipt: () => streamPresentation?.noteImmediatePresentation(),
|
|
301
|
-
...(inputPresentation?.scheduler === undefined
|
|
302
|
-
? {}
|
|
303
|
-
: { scheduler: inputPresentation.scheduler }),
|
|
304
|
-
},
|
|
305
|
-
}),
|
|
306
|
-
} : { layoutRoot: this.root.layoutRoot() }),
|
|
307
|
-
...(inputPresentation?.onEvent === undefined ? {} : {
|
|
308
|
-
inputDiagnostics: {
|
|
309
|
-
onEvent: inputPresentation.onEvent,
|
|
310
|
-
...(inputPresentation.now === undefined ? {} : { now: inputPresentation.now }),
|
|
311
|
-
},
|
|
312
|
-
}),
|
|
313
|
-
...(terminal === undefined ? {} : { terminal: terminal }),
|
|
314
|
-
hardwareCursor: this.backend.session.view().terminal.hardwareCursor,
|
|
315
|
-
...(this.#customViewport ? {} : { wheelScrollLines: initialPiSettings.fullscreenWheelScrollLines }),
|
|
316
|
-
};
|
|
317
|
-
runtime = new PiTuiRuntimeAdapter(runtimeOptions);
|
|
318
|
-
this.runtime = runtime;
|
|
319
|
-
this.#programStatus = new ProgramStatusReporter(status => { if (this.runtime.active)
|
|
320
|
-
this.runtime.setProgramStatus(status); }, () => this.view().status.footer?.sessionName ?? undefined);
|
|
321
|
-
this.#damageTerminal = damageTerminal ?? null;
|
|
322
|
-
if (this.#customViewport && backgroundSettings !== undefined && damageTerminal !== undefined) {
|
|
323
|
-
damageTerminal.setCanvasBackground(piCanvasBackgroundAnsi(backgroundSettings.snapshot()));
|
|
324
|
-
}
|
|
325
|
-
this.#quitOutro = quitOutro;
|
|
267
|
+
this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
|
|
326
268
|
this.#reloadPresentation = reloadPresentation;
|
|
327
269
|
const presentationInterval = streamPresentationOptions?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
|
|
328
|
-
streamPresentation = streamPresentationOptions?.scheduler === undefined
|
|
329
|
-
? new StreamPresentationCoalescer(() => this.
|
|
330
|
-
: new StreamPresentationCoalescer(() => this.
|
|
331
|
-
this.#streamPresentation = streamPresentation;
|
|
270
|
+
this.#streamPresentation = streamPresentationOptions?.scheduler === undefined
|
|
271
|
+
? new StreamPresentationCoalescer(() => this.#terminal.requestRender(), presentationInterval)
|
|
272
|
+
: new StreamPresentationCoalescer(() => this.#terminal.requestRender(), presentationInterval, streamPresentationOptions.scheduler);
|
|
332
273
|
constructed.push(() => this.#streamPresentation.dispose());
|
|
333
274
|
const promptSuggestionOptions = this.#customViewport ? options.suggestions : undefined;
|
|
334
275
|
promptSuggestionController = promptSuggestionOptions === undefined ? null : new ContextualPromptSuggestionController({
|
|
@@ -345,7 +286,7 @@ export class OwnedUiSessionShell {
|
|
|
345
286
|
return true;
|
|
346
287
|
},
|
|
347
288
|
clear: () => this.root.setPromptSuggestion(null),
|
|
348
|
-
requestRender: () => this.
|
|
289
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
349
290
|
},
|
|
350
291
|
});
|
|
351
292
|
this.#promptSuggestions = promptSuggestionController;
|
|
@@ -356,7 +297,7 @@ export class OwnedUiSessionShell {
|
|
|
356
297
|
constructed.push(this.#unsubscribePromptSuggestions);
|
|
357
298
|
this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
|
|
358
299
|
if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
|
|
359
|
-
this.
|
|
300
|
+
this.#terminal.requestRender();
|
|
360
301
|
});
|
|
361
302
|
constructed.push(this.#unsubscribePromptImages);
|
|
362
303
|
// Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
|
|
@@ -366,11 +307,11 @@ export class OwnedUiSessionShell {
|
|
|
366
307
|
if (this.#disposed || this.#commandListSignature() === this.#installedCommandSignature)
|
|
367
308
|
return;
|
|
368
309
|
this.#installAutocompleteCommands();
|
|
369
|
-
this.
|
|
310
|
+
this.#terminal.requestRender();
|
|
370
311
|
});
|
|
371
312
|
constructed.push(this.#unsubscribeSkills);
|
|
372
|
-
this.
|
|
373
|
-
this.
|
|
313
|
+
this.#terminal.setHardwareCursor(initialPiSettings.showHardwareCursor);
|
|
314
|
+
this.#terminal.setClearOnShrink(initialPiSettings.clearOnShrink);
|
|
374
315
|
this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
|
|
375
316
|
this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
|
|
376
317
|
// Invariant: bare A1 prints only the resume hint at exit, so the pinned exit-output
|
|
@@ -383,12 +324,12 @@ export class OwnedUiSessionShell {
|
|
|
383
324
|
showHardwareCursor: { apply: value => {
|
|
384
325
|
if (typeof value !== "boolean")
|
|
385
326
|
throw new TypeError("Hardware cursor setting is invalid");
|
|
386
|
-
this.
|
|
327
|
+
this.#terminal.setHardwareCursor(value);
|
|
387
328
|
} },
|
|
388
329
|
clearOnShrink: { apply: value => {
|
|
389
330
|
if (typeof value !== "boolean")
|
|
390
331
|
throw new TypeError("Clear-on-shrink setting is invalid");
|
|
391
|
-
this.
|
|
332
|
+
this.#terminal.setClearOnShrink(value);
|
|
392
333
|
} },
|
|
393
334
|
showTerminalProgress: { apply: value => {
|
|
394
335
|
if (typeof value !== "boolean")
|
|
@@ -398,55 +339,6 @@ export class OwnedUiSessionShell {
|
|
|
398
339
|
} },
|
|
399
340
|
});
|
|
400
341
|
constructed.push(this.#unbindTerminalSettings);
|
|
401
|
-
this.#removeViewportPreInput = this.#customViewport
|
|
402
|
-
? this.runtime.addPreInputListener(data => {
|
|
403
|
-
if (inputPresentation?.coordination === false)
|
|
404
|
-
this.#streamPresentation.noteImmediatePresentation();
|
|
405
|
-
// Compatibility: route modal paging and boundary keys before Pi's fullscreen handlers.
|
|
406
|
-
const isDefault = this.root.usesDefaultInputSurface();
|
|
407
|
-
if (!this.runtime.hasOverlay() && !isDefault
|
|
408
|
-
&& (this.root.editor.matchesTerminalKey(data, "pageUp")
|
|
409
|
-
|| this.root.editor.matchesTerminalKey(data, "pageDown"))) {
|
|
410
|
-
this.root.handleInput(data);
|
|
411
|
-
this.runtime.requestRender();
|
|
412
|
-
return { consume: true };
|
|
413
|
-
}
|
|
414
|
-
if (!this.runtime.hasOverlay() && (this.root.editor.matchesTerminalKey(data, "home")
|
|
415
|
-
|| this.root.editor.matchesTerminalKey(data, "end")
|
|
416
|
-
|| !isDefault && (this.root.editor.matchesTerminalKey(data, "ctrl+home")
|
|
417
|
-
|| this.root.editor.matchesTerminalKey(data, "ctrl+end")))) {
|
|
418
|
-
if (isDefault)
|
|
419
|
-
this.root.handleViewportPreInput(data, true);
|
|
420
|
-
this.root.handleInput(data);
|
|
421
|
-
this.runtime.requestRender();
|
|
422
|
-
return { consume: true };
|
|
423
|
-
}
|
|
424
|
-
// Invariant: geometry must belong to the painted frame, including newly opened/nested
|
|
425
|
-
// surfaces. Steady pointer input does not trigger a synchronous composition.
|
|
426
|
-
const viewport = this.runtime.viewport();
|
|
427
|
-
if (data.includes("\u001b[<") && !this.root.viewportInputGeometryReady(viewport.columns, viewport.rows)) {
|
|
428
|
-
this.runtime.renderNow();
|
|
429
|
-
}
|
|
430
|
-
const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
|
|
431
|
-
if (routed.copySelection !== undefined) {
|
|
432
|
-
const snapshot = routed.copySelection;
|
|
433
|
-
const acknowledgement = copyAcknowledgement(snapshot);
|
|
434
|
-
const intent = ++this.#copyIntentSequence;
|
|
435
|
-
void this.#responseCopy?.submit(snapshot, pendingClipboardWrite).then(result => {
|
|
436
|
-
if (this.#disposed || !this.runtime.active || intent !== this.#copyIntentSequence
|
|
437
|
-
|| result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
|
|
438
|
-
return;
|
|
439
|
-
if (acknowledgement.hasNonWhitespace) {
|
|
440
|
-
this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
if (!routed.consumed)
|
|
445
|
-
return routed.data === data ? undefined : { data: routed.data };
|
|
446
|
-
return routed.data.length === 0 ? { consume: true } : { data: routed.data };
|
|
447
|
-
})
|
|
448
|
-
: () => { };
|
|
449
|
-
constructed.push(this.#removeViewportPreInput);
|
|
450
342
|
const applyViewportSettings = () => {
|
|
451
343
|
const snapshot = viewportSettings?.snapshot();
|
|
452
344
|
this.root.setViewportConfig(snapshot ?? {
|
|
@@ -460,14 +352,6 @@ export class OwnedUiSessionShell {
|
|
|
460
352
|
? viewportSettings.onChange(settings => this.root.setViewportConfig(settings))
|
|
461
353
|
: () => { };
|
|
462
354
|
constructed.push(this.#unsubscribeSettings);
|
|
463
|
-
this.#unsubscribeBackgroundSettings = this.#customViewport && backgroundSettings
|
|
464
|
-
? backgroundSettings.onChange(style => {
|
|
465
|
-
if (this.#damageTerminal?.setCanvasBackground(piCanvasBackgroundAnsi(style))) {
|
|
466
|
-
this.runtime.requestRender(true);
|
|
467
|
-
}
|
|
468
|
-
})
|
|
469
|
-
: () => { };
|
|
470
|
-
constructed.push(this.#unsubscribeBackgroundSettings);
|
|
471
355
|
this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
|
|
472
356
|
this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
|
|
473
357
|
this.root.setOutputPad(initialPiSettings.outputPad);
|
|
@@ -524,7 +408,7 @@ export class OwnedUiSessionShell {
|
|
|
524
408
|
if (value !== "auto" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 100)) {
|
|
525
409
|
throw new TypeError("Fullscreen wheel-scroll distance is invalid");
|
|
526
410
|
}
|
|
527
|
-
this.
|
|
411
|
+
this.#terminal.setWheelScrollLines(value);
|
|
528
412
|
} },
|
|
529
413
|
}),
|
|
530
414
|
});
|
|
@@ -538,29 +422,29 @@ export class OwnedUiSessionShell {
|
|
|
538
422
|
limit: promptHistory.limit,
|
|
539
423
|
fallback: this.view().transcript.flatMap(block => block.kind === "user" ? [block.text] : []),
|
|
540
424
|
active: () => this.root.usesDefaultInputSurface(),
|
|
541
|
-
render: () => this.
|
|
425
|
+
render: () => this.#terminal.requestRender(),
|
|
542
426
|
rehydrate: value => this.root.rehydrateHistoryText(value, id => sidecar?.readAttachment(id) ?? null),
|
|
543
427
|
});
|
|
544
428
|
constructed.push(() => { void this.#promptHistory?.close().catch(() => false); });
|
|
545
429
|
}
|
|
546
430
|
this.#extensionBridge = createPiExtensionUiBridge({
|
|
547
431
|
runtime: {
|
|
548
|
-
getColumns: () => this.
|
|
549
|
-
getRows: () => this.
|
|
550
|
-
requestRender: () => this.
|
|
432
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
433
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
434
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
551
435
|
},
|
|
552
436
|
agentDir: this.backend.identity.agentDir,
|
|
553
437
|
setInputSurface: component => this.root.setInputSurface(component, true, "opaque"),
|
|
554
438
|
setProgramStatusBlocked: status => this.#programStatus.setBlocked("extension-dialog", status),
|
|
555
|
-
showOverlay: (component, overlayOptions) => this.
|
|
556
|
-
listenInput: handler => this.
|
|
439
|
+
showOverlay: (component, overlayOptions) => this.#terminal.showOverlay(component, overlayOptions),
|
|
440
|
+
listenInput: handler => this.#terminal.addInputListener(handler),
|
|
557
441
|
replaceWidget: (key, component, placement) => this.root.setExtensionWidget(key, component, placement),
|
|
558
442
|
replaceHeader: component => this.root.setExtensionHeader(component),
|
|
559
443
|
replaceFooter: component => this.root.setExtensionFooter(component),
|
|
560
444
|
setStatus: (key, text) => this.root.setExtensionStatus(key, text),
|
|
561
445
|
setWorking: (message, visible) => this.root.setExtensionWorking(message, visible),
|
|
562
446
|
notify: (message, type) => this.root.addExtensionNotification(message, type),
|
|
563
|
-
setTitle: title => this.
|
|
447
|
+
setTitle: title => this.#terminal.setTitle(title),
|
|
564
448
|
getEditorText: () => this.root.editor.getText(),
|
|
565
449
|
setEditorText: text => this.root.editor.setText(text),
|
|
566
450
|
pasteToEditor: text => this.root.editor.insertText(text),
|
|
@@ -578,7 +462,7 @@ export class OwnedUiSessionShell {
|
|
|
578
462
|
notify: event => this.#notifyWorkflowLogin(event),
|
|
579
463
|
publish: message => {
|
|
580
464
|
this.root.appendWorkflowMessage(message);
|
|
581
|
-
this.
|
|
465
|
+
this.#terminal.requestRender();
|
|
582
466
|
},
|
|
583
467
|
finishLogin: () => this.#finishWorkflowLogin(),
|
|
584
468
|
});
|
|
@@ -680,6 +564,83 @@ export class OwnedUiSessionShell {
|
|
|
680
564
|
view() {
|
|
681
565
|
return this.backend.session.view();
|
|
682
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 };
|
|
643
|
+
}
|
|
683
644
|
#promptSuggestionPresentationBlockReason(identity) {
|
|
684
645
|
const view = this.view();
|
|
685
646
|
if (this.#disposed)
|
|
@@ -689,18 +650,13 @@ export class OwnedUiSessionShell {
|
|
|
689
650
|
return "stale-identity";
|
|
690
651
|
return this.root.promptSuggestionPresentationBlockReason();
|
|
691
652
|
}
|
|
692
|
-
damagePresentationDecision() {
|
|
693
|
-
return this.#damageTerminal?.lastDecision ?? null;
|
|
694
|
-
}
|
|
695
653
|
start() {
|
|
696
654
|
if (this.#started)
|
|
697
655
|
return;
|
|
698
656
|
this.#started = true;
|
|
699
|
-
this.
|
|
657
|
+
this.#terminal.start();
|
|
700
658
|
this.#promptHistory?.start();
|
|
701
659
|
this.#syncTerminalProgress(this.view());
|
|
702
|
-
if (this.#customViewport)
|
|
703
|
-
this.#setPointerReporting(true);
|
|
704
660
|
void this.backend.extensions.bindExtensionUi(this.#extensionBridge.context, () => { void this.shutdown(); });
|
|
705
661
|
this.#syncView();
|
|
706
662
|
// Performance: the spare clipboard helpers fork after the first frame is out, so startup never waits on them.
|
|
@@ -754,7 +710,7 @@ export class OwnedUiSessionShell {
|
|
|
754
710
|
render: width => [...renderPiShellStatusText(`Waiting for images (${count} submission${count === 1 ? "" : "s"}) — Esc cancels; dequeue restores`, width)],
|
|
755
711
|
invalidate: () => { },
|
|
756
712
|
}, "aboveEditor");
|
|
757
|
-
this.
|
|
713
|
+
this.#terminal.requestRender();
|
|
758
714
|
}
|
|
759
715
|
#cancelWaitingImages() {
|
|
760
716
|
for (const item of this.#waitingImages.values())
|
|
@@ -807,13 +763,13 @@ export class OwnedUiSessionShell {
|
|
|
807
763
|
...(result.output ? { detail: result.output } : {}),
|
|
808
764
|
};
|
|
809
765
|
this.root.appendWorkflowResult(workflow);
|
|
810
|
-
this.
|
|
766
|
+
this.#terminal.requestRender();
|
|
811
767
|
return workflow.outcome === "failed" ? rejected(workflow.message) : { outcome: "completed", diagnostic: null };
|
|
812
768
|
}
|
|
813
769
|
catch (error) {
|
|
814
770
|
const message = `Bash command failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
815
771
|
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message });
|
|
816
|
-
this.
|
|
772
|
+
this.#terminal.requestRender();
|
|
817
773
|
return rejected(message);
|
|
818
774
|
}
|
|
819
775
|
}
|
|
@@ -843,7 +799,7 @@ export class OwnedUiSessionShell {
|
|
|
843
799
|
return this.shutdown();
|
|
844
800
|
this.root.editor.setText("");
|
|
845
801
|
this.#lastClearTime = now;
|
|
846
|
-
this.
|
|
802
|
+
this.#terminal.requestRender();
|
|
847
803
|
return { outcome: "completed", diagnostic: null };
|
|
848
804
|
}
|
|
849
805
|
async interrupt(now = Date.now()) {
|
|
@@ -924,7 +880,7 @@ export class OwnedUiSessionShell {
|
|
|
924
880
|
async cycleModel(direction) {
|
|
925
881
|
const result = await this.backend.workflows.cycleModelWorkflow(direction);
|
|
926
882
|
this.root.appendWorkflowResult(result);
|
|
927
|
-
this.
|
|
883
|
+
this.#terminal.requestRender();
|
|
928
884
|
return workflowAdapterResult(result);
|
|
929
885
|
}
|
|
930
886
|
async queueFollowUp() {
|
|
@@ -956,7 +912,7 @@ export class OwnedUiSessionShell {
|
|
|
956
912
|
if (queued.length === 0)
|
|
957
913
|
return;
|
|
958
914
|
this.root.editor.setText(queued.join("\n"));
|
|
959
|
-
this.
|
|
915
|
+
this.#terminal.requestRender();
|
|
960
916
|
}
|
|
961
917
|
showSelector(title, options, onSelect, onCancel) {
|
|
962
918
|
this.#dialogHandle?.hide();
|
|
@@ -978,7 +934,7 @@ export class OwnedUiSessionShell {
|
|
|
978
934
|
onCancel?.();
|
|
979
935
|
},
|
|
980
936
|
});
|
|
981
|
-
const handle = this.
|
|
937
|
+
const handle = this.#terminal.showOverlay(component, {
|
|
982
938
|
width: "70%",
|
|
983
939
|
maxHeight: "80%",
|
|
984
940
|
anchor: "center",
|
|
@@ -993,16 +949,16 @@ export class OwnedUiSessionShell {
|
|
|
993
949
|
return {
|
|
994
950
|
get disposed() { return isDisposed(); },
|
|
995
951
|
setInputSurface: surface => this.root.setInputSurface(surface),
|
|
996
|
-
requestRender: () => this.
|
|
997
|
-
viewport: () => this.
|
|
952
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
953
|
+
viewport: () => this.#terminal.viewport(),
|
|
998
954
|
appendWorkflowStatus: text => this.root.appendWorkflowStatus(text),
|
|
999
955
|
appendWorkflowResult: result => this.root.appendWorkflowResult(result),
|
|
1000
956
|
runWorkflow: request => this.runWorkflow(request),
|
|
1001
957
|
// Invariant: the custom bare-A1 surface holds frames while optional selectors load and is permanently
|
|
1002
958
|
// fullscreen; the pinned comparison profiles render immediately and keep Pi's TUI mode switch.
|
|
1003
959
|
...(this.#customViewport
|
|
1004
|
-
? { beginPresentationHold: () => this.
|
|
1005
|
-
: { switchTuiMode: mode => this.
|
|
960
|
+
? { beginPresentationHold: () => this.#terminal.beginPresentationHold() }
|
|
961
|
+
: { switchTuiMode: mode => this.#terminal.switchMode(mode) }),
|
|
1006
962
|
};
|
|
1007
963
|
}
|
|
1008
964
|
#loadLazySelectors() {
|
|
@@ -1018,13 +974,13 @@ export class OwnedUiSessionShell {
|
|
|
1018
974
|
const close = () => {
|
|
1019
975
|
this.root.setFooterLevel(true);
|
|
1020
976
|
this.root.setInputSurface(null);
|
|
1021
|
-
this.
|
|
977
|
+
this.#terminal.requestRender();
|
|
1022
978
|
};
|
|
1023
979
|
const select = (level) => {
|
|
1024
980
|
close();
|
|
1025
981
|
void this.runWorkflow({ command: "thinking", argument: "", selection: level });
|
|
1026
982
|
};
|
|
1027
|
-
const releasePresentation = this.#customViewport ? this.
|
|
983
|
+
const releasePresentation = this.#customViewport ? this.#terminal.beginPresentationHold() : undefined;
|
|
1028
984
|
try {
|
|
1029
985
|
const selectors = await this.#loadLazySelectors();
|
|
1030
986
|
const component = await selectors.createThinking({
|
|
@@ -1045,7 +1001,7 @@ export class OwnedUiSessionShell {
|
|
|
1045
1001
|
return;
|
|
1046
1002
|
this.root.setFooterLevel(false);
|
|
1047
1003
|
this.root.setInputSurface(component);
|
|
1048
|
-
this.
|
|
1004
|
+
this.#terminal.requestRender();
|
|
1049
1005
|
}
|
|
1050
1006
|
finally {
|
|
1051
1007
|
releasePresentation?.();
|
|
@@ -1055,7 +1011,7 @@ export class OwnedUiSessionShell {
|
|
|
1055
1011
|
showSkillsSelector(skills = this.#skillSummaries()) {
|
|
1056
1012
|
const close = () => {
|
|
1057
1013
|
this.root.setInputSurface(null);
|
|
1058
|
-
this.
|
|
1014
|
+
this.#terminal.requestRender();
|
|
1059
1015
|
};
|
|
1060
1016
|
const component = createPiShellSkillsSelector({
|
|
1061
1017
|
skills,
|
|
@@ -1066,7 +1022,7 @@ export class OwnedUiSessionShell {
|
|
|
1066
1022
|
onCancel: close,
|
|
1067
1023
|
});
|
|
1068
1024
|
this.root.setInputSurface(component);
|
|
1069
|
-
this.
|
|
1025
|
+
this.#terminal.requestRender();
|
|
1070
1026
|
}
|
|
1071
1027
|
showModelSelector(initialSearchInput) {
|
|
1072
1028
|
this.#presenters.openModelSelector(this.#dialogHost, initialSearchInput);
|
|
@@ -1075,19 +1031,19 @@ export class OwnedUiSessionShell {
|
|
|
1075
1031
|
const options = this.backend.catalog.pinnedForkOptions();
|
|
1076
1032
|
if (options.length === 0) {
|
|
1077
1033
|
this.root.appendWorkflowResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
|
|
1078
|
-
this.
|
|
1034
|
+
this.#terminal.requestRender();
|
|
1079
1035
|
return;
|
|
1080
1036
|
}
|
|
1081
1037
|
const close = () => {
|
|
1082
1038
|
this.root.setInputSurface(null);
|
|
1083
|
-
this.
|
|
1039
|
+
this.#terminal.requestRender();
|
|
1084
1040
|
};
|
|
1085
1041
|
const component = createPiShellUserMessageSelector(options, selection => {
|
|
1086
1042
|
close();
|
|
1087
1043
|
void this.runWorkflow({ command: "fork", argument: "", selection });
|
|
1088
1044
|
}, close);
|
|
1089
1045
|
this.root.setInputSurface(component);
|
|
1090
|
-
this.
|
|
1046
|
+
this.#terminal.requestRender();
|
|
1091
1047
|
}
|
|
1092
1048
|
async showLogoutSelector() {
|
|
1093
1049
|
let options;
|
|
@@ -1100,24 +1056,24 @@ export class OwnedUiSessionShell {
|
|
|
1100
1056
|
outcome: "failed",
|
|
1101
1057
|
message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
|
|
1102
1058
|
});
|
|
1103
|
-
this.
|
|
1059
|
+
this.#terminal.requestRender();
|
|
1104
1060
|
return;
|
|
1105
1061
|
}
|
|
1106
1062
|
if (options.length === 0) {
|
|
1107
1063
|
this.root.appendWorkflowStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
|
|
1108
|
-
this.
|
|
1064
|
+
this.#terminal.requestRender();
|
|
1109
1065
|
return;
|
|
1110
1066
|
}
|
|
1111
1067
|
const close = () => {
|
|
1112
1068
|
this.root.setInputSurface(null);
|
|
1113
|
-
this.
|
|
1069
|
+
this.#terminal.requestRender();
|
|
1114
1070
|
};
|
|
1115
1071
|
const component = createPiShellAuthProviderSelector("logout", options, selection => {
|
|
1116
1072
|
close();
|
|
1117
1073
|
void this.runWorkflow({ command: "logout", argument: "", selection });
|
|
1118
1074
|
}, close);
|
|
1119
1075
|
this.root.setInputSurface(component);
|
|
1120
|
-
this.
|
|
1076
|
+
this.#terminal.requestRender();
|
|
1121
1077
|
}
|
|
1122
1078
|
showLoginMethodSelector(providerReference) {
|
|
1123
1079
|
const method = this.backend.catalog.pinnedLoginMethodOptions(providerReference);
|
|
@@ -1133,7 +1089,7 @@ export class OwnedUiSessionShell {
|
|
|
1133
1089
|
}
|
|
1134
1090
|
const close = () => {
|
|
1135
1091
|
this.root.setInputSurface(null);
|
|
1136
|
-
this.
|
|
1092
|
+
this.#terminal.requestRender();
|
|
1137
1093
|
};
|
|
1138
1094
|
const labels = method.options.map(option => option.label);
|
|
1139
1095
|
const component = createPiShellExtensionSelector(method.title, labels, label => {
|
|
@@ -1144,7 +1100,7 @@ export class OwnedUiSessionShell {
|
|
|
1144
1100
|
void this.runWorkflow({ command: "login", argument: "", selection });
|
|
1145
1101
|
}, close);
|
|
1146
1102
|
this.root.setInputSurface(component);
|
|
1147
|
-
this.
|
|
1103
|
+
this.#terminal.requestRender();
|
|
1148
1104
|
}
|
|
1149
1105
|
showTreeSelector(initialSelectedId) {
|
|
1150
1106
|
return this.#presenters.openTreeSelector(this.#dialogHost, (entryId, skipSummaryPrompt) => {
|
|
@@ -1154,7 +1110,7 @@ export class OwnedUiSessionShell {
|
|
|
1154
1110
|
showLoginAuthTypeSelector() {
|
|
1155
1111
|
const close = () => {
|
|
1156
1112
|
this.root.setInputSurface(null);
|
|
1157
|
-
this.
|
|
1113
|
+
this.#terminal.requestRender();
|
|
1158
1114
|
};
|
|
1159
1115
|
const labels = ["Sign in with an account", "Sign in with an API key"];
|
|
1160
1116
|
const component = createPiShellExtensionSelector("Select authentication method:", labels, label => {
|
|
@@ -1162,18 +1118,18 @@ export class OwnedUiSessionShell {
|
|
|
1162
1118
|
this.showLoginProviderSelector(label === labels[0] ? "oauth" : "api_key");
|
|
1163
1119
|
}, close);
|
|
1164
1120
|
this.root.setInputSurface(component);
|
|
1165
|
-
this.
|
|
1121
|
+
this.#terminal.requestRender();
|
|
1166
1122
|
}
|
|
1167
1123
|
showLoginProviderSelector(authType, initialSearchInput) {
|
|
1168
1124
|
const options = this.backend.catalog.pinnedLoginOptions(authType);
|
|
1169
1125
|
if (options.length === 0) {
|
|
1170
1126
|
this.root.appendWorkflowStatus(authType === "oauth" ? "No account providers available." : authType === "api_key" ? "No API key providers available." : "No login providers available.");
|
|
1171
|
-
this.
|
|
1127
|
+
this.#terminal.requestRender();
|
|
1172
1128
|
return;
|
|
1173
1129
|
}
|
|
1174
1130
|
const close = () => {
|
|
1175
1131
|
this.root.setInputSurface(null);
|
|
1176
|
-
this.
|
|
1132
|
+
this.#terminal.requestRender();
|
|
1177
1133
|
};
|
|
1178
1134
|
const component = createPiShellAuthProviderSelector("login", options, id => {
|
|
1179
1135
|
close();
|
|
@@ -1184,7 +1140,7 @@ export class OwnedUiSessionShell {
|
|
|
1184
1140
|
this.showLoginAuthTypeSelector();
|
|
1185
1141
|
}, initialSearchInput);
|
|
1186
1142
|
this.root.setInputSurface(component);
|
|
1187
|
-
this.
|
|
1143
|
+
this.#terminal.requestRender();
|
|
1188
1144
|
}
|
|
1189
1145
|
showSessionSelector() {
|
|
1190
1146
|
return this.#presenters.openSessionSelector(this.#dialogHost, () => { void this.shutdown(); });
|
|
@@ -1224,7 +1180,7 @@ export class OwnedUiSessionShell {
|
|
|
1224
1180
|
if (this.#disposed)
|
|
1225
1181
|
return;
|
|
1226
1182
|
this.root.appendWorkflowResult({ command: "name", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
1227
|
-
this.
|
|
1183
|
+
this.#terminal.requestRender();
|
|
1228
1184
|
});
|
|
1229
1185
|
return { outcome: "completed", diagnostic: null };
|
|
1230
1186
|
}
|
|
@@ -1234,16 +1190,16 @@ export class OwnedUiSessionShell {
|
|
|
1234
1190
|
if (setup) {
|
|
1235
1191
|
const close = () => {
|
|
1236
1192
|
this.root.setInputSurface(null);
|
|
1237
|
-
this.
|
|
1193
|
+
this.#terminal.requestRender();
|
|
1238
1194
|
};
|
|
1239
1195
|
const dialog = createPiShellLoginDialog({
|
|
1240
|
-
getColumns: () => this.
|
|
1241
|
-
getRows: () => this.
|
|
1242
|
-
requestRender: () => this.
|
|
1196
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
1197
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
1198
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
1243
1199
|
}, setup.providerId, close, setup.providerName, setup.title);
|
|
1244
1200
|
dialog.showInfo(setup.message, [], true);
|
|
1245
1201
|
this.root.setInputSurface(dialog);
|
|
1246
|
-
this.
|
|
1202
|
+
this.#terminal.requestRender();
|
|
1247
1203
|
return { outcome: "completed", diagnostic: null };
|
|
1248
1204
|
}
|
|
1249
1205
|
}
|
|
@@ -1298,7 +1254,7 @@ export class OwnedUiSessionShell {
|
|
|
1298
1254
|
const blocked = this.backend.workflows.reloadBlockedResult();
|
|
1299
1255
|
if (blocked) {
|
|
1300
1256
|
this.root.appendWorkflowResult(blocked);
|
|
1301
|
-
this.
|
|
1257
|
+
this.#terminal.requestRender();
|
|
1302
1258
|
return workflowAdapterResult(blocked);
|
|
1303
1259
|
}
|
|
1304
1260
|
this.root.resetExtensionUi();
|
|
@@ -1308,16 +1264,16 @@ export class OwnedUiSessionShell {
|
|
|
1308
1264
|
: undefined;
|
|
1309
1265
|
const shareSurface = shareDialog === undefined ? undefined
|
|
1310
1266
|
: (this.#customViewport ? shareDialog.createPiShellShareOperationDialog : shareDialog.createPiShellOperationLoader)({
|
|
1311
|
-
getColumns: () => this.
|
|
1312
|
-
getRows: () => this.
|
|
1313
|
-
requestRender: () => this.
|
|
1267
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
1268
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
1269
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
1314
1270
|
}, "Creating gist…");
|
|
1315
1271
|
const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
|
|
1316
1272
|
const now = this.#reloadPresentation?.now ?? Date.now;
|
|
1317
1273
|
const shownAt = now();
|
|
1318
1274
|
if (operationSurface) {
|
|
1319
1275
|
this.root.setInputSurface(operationSurface);
|
|
1320
|
-
this.
|
|
1276
|
+
this.#terminal.requestRender();
|
|
1321
1277
|
}
|
|
1322
1278
|
let result;
|
|
1323
1279
|
try {
|
|
@@ -1330,7 +1286,7 @@ export class OwnedUiSessionShell {
|
|
|
1330
1286
|
if (request.command === "reload")
|
|
1331
1287
|
await this.#holdReloadSurface(now() - shownAt);
|
|
1332
1288
|
this.root.setInputSurface(null);
|
|
1333
|
-
this.
|
|
1289
|
+
this.#terminal.requestRender();
|
|
1334
1290
|
}
|
|
1335
1291
|
}
|
|
1336
1292
|
if (copyGeneration !== undefined && (this.#disposed || copyGeneration !== this.backend.identity.sessionBindingGeneration)) {
|
|
@@ -1359,7 +1315,7 @@ export class OwnedUiSessionShell {
|
|
|
1359
1315
|
}
|
|
1360
1316
|
if (result.outcome === "requires-selection" || result.outcome === "requires-confirmation") {
|
|
1361
1317
|
this.root.appendWorkflowResult({ command: request.command, outcome: "failed", message: `Owned controller missing for ${request.command}` });
|
|
1362
|
-
this.
|
|
1318
|
+
this.#terminal.requestRender();
|
|
1363
1319
|
return { outcome: "failed", diagnostic: `Owned controller missing for ${request.command}` };
|
|
1364
1320
|
}
|
|
1365
1321
|
if (request.command === "reload" && result.outcome === "completed") {
|
|
@@ -1378,7 +1334,7 @@ export class OwnedUiSessionShell {
|
|
|
1378
1334
|
else {
|
|
1379
1335
|
this.root.appendWorkflowResult(result);
|
|
1380
1336
|
}
|
|
1381
|
-
this.
|
|
1337
|
+
this.#terminal.requestRender();
|
|
1382
1338
|
return workflowAdapterResult(result);
|
|
1383
1339
|
}
|
|
1384
1340
|
async #holdReloadSurface(visibleMs) {
|
|
@@ -1393,7 +1349,7 @@ export class OwnedUiSessionShell {
|
|
|
1393
1349
|
const context = this.backend.catalog.pinnedProjectTrustContext();
|
|
1394
1350
|
const close = () => {
|
|
1395
1351
|
this.root.setInputSurface(null);
|
|
1396
|
-
this.
|
|
1352
|
+
this.#terminal.requestRender();
|
|
1397
1353
|
};
|
|
1398
1354
|
const component = createPiShellTrustSelector({
|
|
1399
1355
|
...context,
|
|
@@ -1407,12 +1363,12 @@ export class OwnedUiSessionShell {
|
|
|
1407
1363
|
close();
|
|
1408
1364
|
this.root.appendWorkflowResult({ command: "trust", outcome: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
1409
1365
|
}
|
|
1410
|
-
this.
|
|
1366
|
+
this.#terminal.requestRender();
|
|
1411
1367
|
},
|
|
1412
1368
|
onCancel: close,
|
|
1413
1369
|
});
|
|
1414
1370
|
this.root.setInputSurface(component);
|
|
1415
|
-
this.
|
|
1371
|
+
this.#terminal.requestRender();
|
|
1416
1372
|
}
|
|
1417
1373
|
showScopedModelsSelector() {
|
|
1418
1374
|
this.#presenters.openScopedModelsSelector(this.#dialogHost);
|
|
@@ -1426,32 +1382,26 @@ export class OwnedUiSessionShell {
|
|
|
1426
1382
|
showModelsDialog(initialQuery) {
|
|
1427
1383
|
this.#presenters.openModelsDialog(this.#dialogHost, initialQuery);
|
|
1428
1384
|
}
|
|
1429
|
-
// Invariant:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
if (this.#pointerReporting === effective)
|
|
1433
|
-
return;
|
|
1434
|
-
this.#pointerReporting = effective;
|
|
1435
|
-
if (!this.runtime.active)
|
|
1436
|
-
return;
|
|
1437
|
-
this.runtime.writeControl(effective ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
|
|
1438
|
-
}
|
|
1439
|
-
// Invariant: one teardown per shell, starting on a microtask so never inside the event listener that
|
|
1440
|
-
// requested it. Callers during the teardown share its outcome; a later call finds it done.
|
|
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.
|
|
1441
1388
|
dispose() {
|
|
1442
1389
|
if (this.#disposePromise !== undefined)
|
|
1443
1390
|
return this.#disposePromise;
|
|
1444
1391
|
if (this.#disposed)
|
|
1445
1392
|
return Promise.resolve();
|
|
1446
1393
|
this.#disposed = true;
|
|
1447
|
-
const pending = Promise.resolve().then(() => this.#
|
|
1394
|
+
const pending = Promise.resolve().then(() => this.#host.close(this));
|
|
1448
1395
|
this.#disposePromise = pending;
|
|
1449
1396
|
const clear = () => { if (this.#disposePromise === pending)
|
|
1450
1397
|
this.#disposePromise = undefined; };
|
|
1451
1398
|
void pending.then(clear, clear);
|
|
1452
1399
|
return pending;
|
|
1453
1400
|
}
|
|
1454
|
-
|
|
1401
|
+
release(terminal) {
|
|
1402
|
+
if (this.#released !== undefined)
|
|
1403
|
+
return { exitText: "", failures: [], settle: async () => [] };
|
|
1404
|
+
this.#disposed = true;
|
|
1455
1405
|
const failures = [];
|
|
1456
1406
|
const attempt = (action) => { try {
|
|
1457
1407
|
action();
|
|
@@ -1462,8 +1412,6 @@ export class OwnedUiSessionShell {
|
|
|
1462
1412
|
// Invariant: events stop first, so nothing re-enters the view during history close, outro, or restoration.
|
|
1463
1413
|
attempt(() => this.#unsubscribe());
|
|
1464
1414
|
this.#copyIntentSequence += 1;
|
|
1465
|
-
// Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
|
|
1466
|
-
const outroFrame = this.#captureQuitOutroFrame();
|
|
1467
1415
|
this.#responseCopy?.dispose();
|
|
1468
1416
|
this.#copyExecutor?.dispose();
|
|
1469
1417
|
this.#cancelWaitingImages();
|
|
@@ -1472,8 +1420,6 @@ export class OwnedUiSessionShell {
|
|
|
1472
1420
|
attempt(() => { historyCleanup = this.#promptHistory?.close() ?? Promise.resolve(true); });
|
|
1473
1421
|
attempt(() => { pasteCleanup = this.root.disposePendingPastes(); });
|
|
1474
1422
|
attempt(() => this.root.clearViewportPointerState());
|
|
1475
|
-
attempt(() => this.#setPointerReporting(false, true));
|
|
1476
|
-
attempt(() => this.#removeViewportPreInput());
|
|
1477
1423
|
attempt(() => this.#streamPresentation.dispose());
|
|
1478
1424
|
attempt(() => {
|
|
1479
1425
|
if (this.#startupRouteTimer !== undefined)
|
|
@@ -1485,17 +1431,16 @@ export class OwnedUiSessionShell {
|
|
|
1485
1431
|
attempt(() => this.#unsubscribePromptImages());
|
|
1486
1432
|
attempt(() => this.#unsubscribeSkills());
|
|
1487
1433
|
attempt(() => this.#unsubscribeSettings());
|
|
1488
|
-
|
|
1489
|
-
let fullscreenExitText = "";
|
|
1434
|
+
let exitText = "";
|
|
1490
1435
|
attempt(() => {
|
|
1491
1436
|
const exitMode = this.backend.identity.disposed ? this.#fullscreenExitOutput : this.backend.settings.snapshot().fullscreenExitOutput;
|
|
1492
1437
|
const resume = this.backend.identity.currentSessionResumeMetadata();
|
|
1493
1438
|
const resumeHint = resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
|
|
1494
1439
|
// Invariant: bare A1 leaves only the hint behind; the pinned comparison profile still
|
|
1495
1440
|
// honors fullscreenExitOutput, including the styled transcript.
|
|
1496
|
-
|
|
1441
|
+
exitText = terminal.mode !== "fullscreen" ? ""
|
|
1497
1442
|
: this.#customViewport || exitMode === "resume-hint" ? resumeHint
|
|
1498
|
-
: [this.root.exitTranscript(
|
|
1443
|
+
: [this.root.exitTranscript(terminal.columns), resumeHint].filter(Boolean).join("\n\n");
|
|
1499
1444
|
});
|
|
1500
1445
|
attempt(() => this.#unbindClipboardWriter());
|
|
1501
1446
|
attempt(() => this.#unbindPiSettings());
|
|
@@ -1505,76 +1450,22 @@ export class OwnedUiSessionShell {
|
|
|
1505
1450
|
attempt(() => this.#dialogHandle?.hide());
|
|
1506
1451
|
attempt(() => this.#extensionBridge.dispose());
|
|
1507
1452
|
attempt(() => this.#programStatus.clear());
|
|
1508
|
-
// Invariant:
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
}
|
|
1525
|
-
// Invariant: the snapshot is synchronous; the outro module itself loads only at quit.
|
|
1526
|
-
#captureQuitOutroFrame() {
|
|
1527
|
-
const outro = this.#quitOutro;
|
|
1528
|
-
if (outro === undefined || !outro.interactive || !this.#customViewport || this.#damageTerminal === null)
|
|
1529
|
-
return null;
|
|
1530
|
-
if (!this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1531
|
-
return null;
|
|
1532
|
-
try {
|
|
1533
|
-
if (!outro.snapshot().enabled)
|
|
1534
|
-
return null;
|
|
1535
|
-
const viewport = this.runtime.viewport();
|
|
1536
|
-
return {
|
|
1537
|
-
rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows,
|
|
1538
|
-
settings: {
|
|
1539
|
-
effect: QUIT_OUTRO_EFFECT,
|
|
1540
|
-
durationMs: QUIT_OUTRO_DURATION_MS,
|
|
1541
|
-
canvasBackgroundAnsi: this.#damageTerminal.canvasBackgroundAnsi,
|
|
1542
|
-
},
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
catch {
|
|
1546
|
-
return null;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
#freezeQuitPresentation() {
|
|
1550
|
-
if (!this.#customViewport || !this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1551
|
-
return;
|
|
1552
|
-
this.runtime.freezePresentation();
|
|
1553
|
-
}
|
|
1554
|
-
// Rationale: any failure here only skips the effect; restoration always follows.
|
|
1555
|
-
async #playQuitOutro(capture) {
|
|
1556
|
-
const outro = this.#quitOutro;
|
|
1557
|
-
if (capture === null || outro === undefined || !this.runtime.active)
|
|
1558
|
-
return;
|
|
1559
|
-
try {
|
|
1560
|
-
// Rationale: the effects stay off the startup graph; quit is the only time they load.
|
|
1561
|
-
const { captureQuitOutroFrame, playQuitOutro } = await import("./quit-outro.js");
|
|
1562
|
-
const frame = captureQuitOutroFrame(capture.rows, capture.columns, capture.height);
|
|
1563
|
-
if (frame === null || !this.runtime.active)
|
|
1564
|
-
return;
|
|
1565
|
-
await playQuitOutro(frame, capture.settings.effect, capture.settings.durationMs, {
|
|
1566
|
-
write: data => this.runtime.writeControl(data),
|
|
1567
|
-
...(capture.settings.canvasBackgroundAnsi === null
|
|
1568
|
-
? {}
|
|
1569
|
-
: { canvasBackgroundAnsi: capture.settings.canvasBackgroundAnsi }),
|
|
1570
|
-
...(outro.now === undefined ? {} : { now: outro.now }),
|
|
1571
|
-
...(outro.sleep === undefined ? {} : { sleep: outro.sleep }),
|
|
1572
|
-
...(outro.seed === undefined ? {} : { seed: outro.seed }),
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
catch {
|
|
1576
|
-
// Rationale: a failed or interrupted effect must never hold the terminal; restoration follows.
|
|
1577
|
-
}
|
|
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;
|
|
1578
1469
|
}
|
|
1579
1470
|
#settleStoppedLifecycle() {
|
|
1580
1471
|
void this.dispose().then(() => this.#resolveStoppedLifecycle(), () => this.#resolveStoppedLifecycle());
|
|
@@ -1602,9 +1493,9 @@ export class OwnedUiSessionShell {
|
|
|
1602
1493
|
return view;
|
|
1603
1494
|
}
|
|
1604
1495
|
#syncTerminalProgress(view) {
|
|
1605
|
-
if (!this.
|
|
1496
|
+
if (!this.#terminal.running)
|
|
1606
1497
|
return;
|
|
1607
|
-
this.
|
|
1498
|
+
this.#terminal.setTerminalProgress(this.#terminalProgressEnabled && view.lifecycle === "busy");
|
|
1608
1499
|
if (view.lifecycle === "stopped" || view.lifecycle === "stopping") {
|
|
1609
1500
|
this.#programStatus.clear();
|
|
1610
1501
|
return;
|
|
@@ -1642,7 +1533,6 @@ export class OwnedUiSessionShell {
|
|
|
1642
1533
|
this.#dialogHandle = undefined;
|
|
1643
1534
|
this.#dialogId = undefined;
|
|
1644
1535
|
this.#dialogSource = undefined;
|
|
1645
|
-
this.#setPointerReporting(false);
|
|
1646
1536
|
this.root.setInputSurface(null);
|
|
1647
1537
|
this.root.resetExtensionUi();
|
|
1648
1538
|
this.root.resetWorkflowPresentation();
|
|
@@ -1650,7 +1540,7 @@ export class OwnedUiSessionShell {
|
|
|
1650
1540
|
this.root.update(view);
|
|
1651
1541
|
this.#syncDialog(view.dialog);
|
|
1652
1542
|
this.#presentStartupTrustWarning(view);
|
|
1653
|
-
this.
|
|
1543
|
+
this.#terminal.requestRender();
|
|
1654
1544
|
for (const listener of this.#listeners)
|
|
1655
1545
|
listener(view);
|
|
1656
1546
|
return view;
|
|
@@ -1661,16 +1551,16 @@ export class OwnedUiSessionShell {
|
|
|
1661
1551
|
if (this.#startupTrustHandled || !this.#customViewport)
|
|
1662
1552
|
return;
|
|
1663
1553
|
const diagnostic = view.diagnostics.find(candidate => candidate.code === "project-trust");
|
|
1664
|
-
if (diagnostic === undefined || !this.
|
|
1554
|
+
if (diagnostic === undefined || !this.#terminal.running)
|
|
1665
1555
|
return;
|
|
1666
1556
|
this.#startupTrustHandled = true;
|
|
1667
1557
|
this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
|
|
1668
1558
|
}
|
|
1669
1559
|
#presentStartupRoute() {
|
|
1670
1560
|
const pending = this.#startupRoute;
|
|
1671
|
-
if (pending === undefined || this.#disposed || !this.
|
|
1561
|
+
if (pending === undefined || this.#disposed || !this.#terminal.running)
|
|
1672
1562
|
return;
|
|
1673
|
-
if (this.
|
|
1563
|
+
if (this.#terminal.hasOverlay() || !this.root.usesDefaultInputSurface()) {
|
|
1674
1564
|
this.#startupRouteTimer = setTimeout(() => {
|
|
1675
1565
|
this.#startupRouteTimer = undefined;
|
|
1676
1566
|
this.#presentStartupRoute();
|
|
@@ -1685,121 +1575,33 @@ export class OwnedUiSessionShell {
|
|
|
1685
1575
|
const surface = this.#routeHost?.open(route, input) ?? null;
|
|
1686
1576
|
if (surface === null)
|
|
1687
1577
|
return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
|
|
1688
|
-
if (!this.
|
|
1578
|
+
if (!this.#terminal.running)
|
|
1689
1579
|
return { outcome: "failed", diagnostic: "runtime is not active" };
|
|
1690
1580
|
this.root.dismissNotice();
|
|
1691
1581
|
this.#dialogHandle?.hide();
|
|
1692
1582
|
this.#dialogSource = "route";
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
const removeInterruptWatch = this.runtime.addInputListener(data => {
|
|
1701
|
-
if (!data.includes(INTERRUPT))
|
|
1702
|
-
return undefined;
|
|
1703
|
-
// Protocol: route hosts decide whether one interrupt closes their app. Raw-input forwarding
|
|
1704
|
-
// is required because Pi handles Ctrl+C before the fullscreen overlay receives normal input.
|
|
1705
|
-
const consumedBySurface = surface.handleInput(INTERRUPT);
|
|
1706
|
-
if (surface.isClosed()) {
|
|
1707
|
-
armedAt = 0;
|
|
1708
|
-
closeSurface();
|
|
1709
|
-
return { consume: true };
|
|
1710
|
-
}
|
|
1711
|
-
if (consumedBySurface) {
|
|
1712
|
-
armedAt = 0;
|
|
1713
|
-
this.runtime.requestRender();
|
|
1714
|
-
return { consume: true };
|
|
1715
|
-
}
|
|
1716
|
-
const now = Date.now();
|
|
1717
|
-
if (armedAt !== 0 && now - armedAt <= INTERRUPT_CHORD_MS) {
|
|
1718
|
-
armedAt = 0;
|
|
1719
|
-
closeSurface();
|
|
1720
|
-
void this.shutdown();
|
|
1721
|
-
return { consume: true };
|
|
1722
|
-
}
|
|
1723
|
-
armedAt = now;
|
|
1724
|
-
this.runtime.requestRender();
|
|
1725
|
-
// Invariant: the presented screen owns the chord, so the pinned shell never sees a
|
|
1726
|
-
// stray interrupt while it is up.
|
|
1727
|
-
return { consume: true };
|
|
1728
|
-
});
|
|
1729
|
-
let removeSurfacePreInput = () => { };
|
|
1730
|
-
let rendered = false;
|
|
1731
|
-
let closed = false;
|
|
1732
|
-
const closeSurface = () => {
|
|
1733
|
-
if (closed)
|
|
1734
|
-
return;
|
|
1735
|
-
closed = true;
|
|
1736
|
-
removeSurfacePreInput();
|
|
1737
|
-
removeInterruptWatch();
|
|
1738
|
-
this.#setPointerReporting(false);
|
|
1739
|
-
this.#dialogHandle?.hide();
|
|
1740
|
-
this.#dialogHandle = undefined;
|
|
1741
|
-
this.#dialogId = undefined;
|
|
1742
|
-
this.#dialogSource = undefined;
|
|
1743
|
-
if (rendered) {
|
|
1744
|
-
try {
|
|
1745
|
-
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;
|
|
1746
1590
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
// at the pre-input boundary so dropdowns, value hover, and numeric +/- work,
|
|
1753
|
-
// and consume even unhandled reports so settings content is never selected.
|
|
1754
|
-
removeSurfacePreInput = this.runtime.addPreInputListener(data => {
|
|
1755
|
-
const { events, rest } = parseMouseInput(data);
|
|
1756
|
-
if (events.length === 0)
|
|
1757
|
-
return undefined;
|
|
1758
|
-
for (const event of events)
|
|
1759
|
-
surface.handleMouse(event);
|
|
1760
|
-
if (surface.isClosed())
|
|
1761
|
-
closeSurface();
|
|
1762
|
-
else
|
|
1763
|
-
this.runtime.requestRender();
|
|
1764
|
-
return rest.length === 0 ? { consume: true } : { data: rest };
|
|
1765
|
-
});
|
|
1766
|
-
const rows = () => Math.max(1, this.runtime.viewport().rows);
|
|
1767
|
-
const component = {
|
|
1768
|
-
render: (width) => {
|
|
1769
|
-
const frame = [...surface.render(Math.max(1, width), rows())];
|
|
1770
|
-
rendered = true;
|
|
1771
|
-
return frame;
|
|
1772
|
-
},
|
|
1773
|
-
handleInput: (data) => {
|
|
1774
|
-
const { events, rest } = parseMouseInput(data);
|
|
1775
|
-
for (const event of events)
|
|
1776
|
-
surface.handleMouse(event);
|
|
1777
|
-
if (rest.length > 0)
|
|
1778
|
-
surface.handleInput(rest);
|
|
1779
|
-
if (surface.isClosed()) {
|
|
1780
|
-
closeSurface();
|
|
1781
|
-
return;
|
|
1591
|
+
if (rendered) {
|
|
1592
|
+
try {
|
|
1593
|
+
void onClosed?.()?.catch(() => undefined);
|
|
1594
|
+
}
|
|
1595
|
+
catch { /* Rationale: a failed acknowledgement stays pending. */ }
|
|
1782
1596
|
}
|
|
1783
|
-
this.runtime.requestRender();
|
|
1784
1597
|
},
|
|
1785
|
-
invalidate: () => this.runtime.requestRender(),
|
|
1786
|
-
};
|
|
1787
|
-
surface.onRenderRequested(() => this.runtime.requestRender());
|
|
1788
|
-
surface.onExitRequested(() => {
|
|
1789
|
-
closeSurface();
|
|
1790
|
-
void this.shutdown();
|
|
1791
|
-
});
|
|
1792
|
-
this.#dialogHandle = this.runtime.showOverlay(component, {
|
|
1793
|
-
width: "100%",
|
|
1794
|
-
maxHeight: "100%",
|
|
1795
|
-
anchor: "top-left",
|
|
1796
|
-
inputCoordination: "owned",
|
|
1797
1598
|
});
|
|
1599
|
+
this.#dialogHandle = handle;
|
|
1798
1600
|
this.#dialogId = surface.id;
|
|
1799
1601
|
return { outcome: "completed", diagnostic: null };
|
|
1800
1602
|
}
|
|
1801
1603
|
#syncDialog(dialog) {
|
|
1802
|
-
if (!this.
|
|
1604
|
+
if (!this.#terminal.running)
|
|
1803
1605
|
return;
|
|
1804
1606
|
if (dialog === null) {
|
|
1805
1607
|
// Invariant: locally owned routes (notably /settings) are independent of backend
|
|
@@ -1830,7 +1632,7 @@ export class OwnedUiSessionShell {
|
|
|
1830
1632
|
this.#dialogSource = undefined;
|
|
1831
1633
|
},
|
|
1832
1634
|
});
|
|
1833
|
-
this.#dialogHandle = this.
|
|
1635
|
+
this.#dialogHandle = this.#terminal.showOverlay(component, {
|
|
1834
1636
|
width: "70%",
|
|
1835
1637
|
maxHeight: "80%",
|
|
1836
1638
|
anchor: "center",
|
|
@@ -1896,7 +1698,7 @@ export class OwnedUiSessionShell {
|
|
|
1896
1698
|
if (skill === undefined) {
|
|
1897
1699
|
const message = "Unknown skill: " + token;
|
|
1898
1700
|
this.root.appendWorkflowMessage({ kind: "error", message });
|
|
1899
|
-
this.
|
|
1701
|
+
this.#terminal.requestRender();
|
|
1900
1702
|
return { outcome: "failed", diagnostic: message };
|
|
1901
1703
|
}
|
|
1902
1704
|
return this.#submitSkillPrompt(skillPrompt(skill.name, separator < 0 ? "" : trimmed.slice(separator + 1)), text);
|
|
@@ -1915,7 +1717,7 @@ export class OwnedUiSessionShell {
|
|
|
1915
1717
|
#treeSummary() {
|
|
1916
1718
|
return new Promise(resolve => {
|
|
1917
1719
|
this.root.setInputSurface(createPiShellExtensionSelector("Summarize Branch?", ["No summary", "Summarize", "Summarize with custom prompt"], resolve, () => resolve(undefined)), true, "opaque");
|
|
1918
|
-
this.
|
|
1720
|
+
this.#terminal.requestRender();
|
|
1919
1721
|
});
|
|
1920
1722
|
}
|
|
1921
1723
|
async #completeTreeSelection(entryId, skipSummaryPrompt) {
|
|
@@ -1938,7 +1740,7 @@ export class OwnedUiSessionShell {
|
|
|
1938
1740
|
}
|
|
1939
1741
|
}
|
|
1940
1742
|
this.root.setInputSurface(null);
|
|
1941
|
-
this.
|
|
1743
|
+
this.#terminal.requestRender();
|
|
1942
1744
|
const result = await this.runWorkflow({
|
|
1943
1745
|
command: "tree",
|
|
1944
1746
|
argument: "",
|
|
@@ -1954,9 +1756,9 @@ export class OwnedUiSessionShell {
|
|
|
1954
1756
|
#startWorkflowLogin(request) {
|
|
1955
1757
|
this.#finishWorkflowLogin();
|
|
1956
1758
|
const dialog = createPiShellLoginDialog({
|
|
1957
|
-
getColumns: () => this.
|
|
1958
|
-
getRows: () => this.
|
|
1959
|
-
requestRender: () => this.
|
|
1759
|
+
getColumns: () => this.#terminal.viewport().columns,
|
|
1760
|
+
getRows: () => this.#terminal.viewport().rows,
|
|
1761
|
+
requestRender: () => this.#terminal.requestRender(),
|
|
1960
1762
|
}, request.providerId, success => {
|
|
1961
1763
|
if (!success)
|
|
1962
1764
|
this.#finishWorkflowLogin();
|
|
@@ -1968,7 +1770,7 @@ export class OwnedUiSessionShell {
|
|
|
1968
1770
|
message: `Log in to ${request.providerName}`,
|
|
1969
1771
|
});
|
|
1970
1772
|
this.#syncTerminalProgress(this.view());
|
|
1971
|
-
this.
|
|
1773
|
+
this.#terminal.requestRender();
|
|
1972
1774
|
}
|
|
1973
1775
|
#requestWorkflowInput(request) {
|
|
1974
1776
|
const dialog = this.#activeLoginDialog;
|
|
@@ -1981,7 +1783,7 @@ export class OwnedUiSessionShell {
|
|
|
1981
1783
|
const restoreDialog = () => {
|
|
1982
1784
|
if (this.#activeLoginDialog === dialog)
|
|
1983
1785
|
this.root.setInputSurface(dialog);
|
|
1984
|
-
this.
|
|
1786
|
+
this.#terminal.requestRender();
|
|
1985
1787
|
};
|
|
1986
1788
|
const selector = createPiShellExtensionSelector(request.message, labels, label => {
|
|
1987
1789
|
const id = options.find(option => option.label === label)?.id;
|
|
@@ -1992,13 +1794,13 @@ export class OwnedUiSessionShell {
|
|
|
1992
1794
|
resolve(null);
|
|
1993
1795
|
});
|
|
1994
1796
|
this.root.setInputSurface(selector, false);
|
|
1995
|
-
this.
|
|
1797
|
+
this.#terminal.requestRender();
|
|
1996
1798
|
});
|
|
1997
1799
|
}
|
|
1998
1800
|
const response = request.type === "manual-code"
|
|
1999
1801
|
? dialog.showManualInput(request.message)
|
|
2000
1802
|
: dialog.showPrompt(request.message, request.placeholder);
|
|
2001
|
-
this.
|
|
1803
|
+
this.#terminal.requestRender();
|
|
2002
1804
|
return response.then(value => value, () => null);
|
|
2003
1805
|
}
|
|
2004
1806
|
#notifyWorkflowLogin(event) {
|
|
@@ -2017,7 +1819,7 @@ export class OwnedUiSessionShell {
|
|
|
2017
1819
|
dialog.showWaiting(event.message);
|
|
2018
1820
|
else
|
|
2019
1821
|
dialog.showProgress(event.message);
|
|
2020
|
-
this.
|
|
1822
|
+
this.#terminal.requestRender();
|
|
2021
1823
|
}
|
|
2022
1824
|
#finishWorkflowLogin() {
|
|
2023
1825
|
if (!this.#activeLoginDialog)
|
|
@@ -2026,7 +1828,7 @@ export class OwnedUiSessionShell {
|
|
|
2026
1828
|
this.root.setInputSurface(null);
|
|
2027
1829
|
this.#programStatus.setBlocked("authentication", undefined);
|
|
2028
1830
|
this.#syncTerminalProgress(this.view());
|
|
2029
|
-
this.
|
|
1831
|
+
this.#terminal.requestRender();
|
|
2030
1832
|
}
|
|
2031
1833
|
async #execute(command, draft) {
|
|
2032
1834
|
if (draft === undefined)
|
|
@@ -2080,7 +1882,7 @@ export class OwnedUiSessionShell {
|
|
|
2080
1882
|
try {
|
|
2081
1883
|
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
|
|
2082
1884
|
?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
|
|
2083
|
-
this.
|
|
1885
|
+
this.#terminal.requestRender();
|
|
2084
1886
|
}
|
|
2085
1887
|
catch { /* Security: error presentation cannot create another rejected submission callback. */ }
|
|
2086
1888
|
}
|
|
@@ -2092,6 +1894,29 @@ export class OwnedUiSessionShell {
|
|
|
2092
1894
|
return `pi-shell-${prefix}-${this.#sequence}`;
|
|
2093
1895
|
}
|
|
2094
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
|
+
}
|
|
2095
1920
|
function isCoalescedStreamBlock(kind) {
|
|
2096
1921
|
return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
|
|
2097
1922
|
}
|
|
@@ -2134,9 +1959,4 @@ function workflowAdapterResult(result) {
|
|
|
2134
1959
|
return { outcome: "failed", diagnostic: result.message };
|
|
2135
1960
|
return { outcome: "rejected", diagnostic: result.message };
|
|
2136
1961
|
}
|
|
2137
|
-
const INTERRUPT = "\u0003";
|
|
2138
|
-
const INTERRUPT_CHORD_MS = 1_500;
|
|
2139
1962
|
const RELOAD_SURFACE_MIN_VISIBLE_MS = 400;
|
|
2140
|
-
// Rationale: the outro is not configurable; the switch only decides whether this plan plays.
|
|
2141
|
-
const QUIT_OUTRO_EFFECT = "fall";
|
|
2142
|
-
const QUIT_OUTRO_DURATION_MS = 800;
|