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