@timurproko/a1 0.2.7-dev.806 → 0.2.7-dev.809
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
- package/dist/app/session-shell/dialog-coordinator.js +133 -0
- package/dist/app/session-shell/dock-chrome.d.ts +34 -0
- package/dist/app/session-shell/dock-chrome.js +109 -0
- package/dist/app/session-shell/exit-notice.d.ts +1 -1
- package/dist/app/session-shell/exit-notice.js +1 -1
- package/dist/app/session-shell/extension-surface.d.ts +30 -0
- package/dist/app/session-shell/extension-surface.js +105 -0
- package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
- package/dist/app/session-shell/input-surface-switch.js +85 -0
- package/dist/app/session-shell/login-interaction.d.ts +27 -0
- package/dist/app/session-shell/login-interaction.js +91 -0
- package/dist/app/session-shell/prompt-submission.d.ts +48 -0
- package/dist/app/session-shell/prompt-submission.js +276 -0
- package/dist/app/session-shell/resident-tab-coordinator.d.ts +41 -0
- package/dist/app/session-shell/resident-tab-coordinator.js +120 -0
- package/dist/app/session-shell/session-concern.d.ts +22 -0
- package/dist/app/session-shell/session-concern.js +29 -0
- package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
- package/dist/app/session-shell/session-lifecycle.js +49 -0
- package/dist/app/session-shell/session-shell-root.d.ts +1 -1
- package/dist/app/session-shell/session-shell-root.js +182 -931
- package/dist/app/session-shell/session-shell.d.ts +2 -8
- package/dist/app/session-shell/session-shell.js +209 -897
- package/dist/app/session-shell/settings-bindings.d.ts +17 -0
- package/dist/app/session-shell/settings-bindings.js +91 -0
- package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
- package/dist/app/session-shell/shell-editor-factory.js +102 -0
- package/dist/app/session-shell/shell-host.d.ts +6 -0
- package/dist/app/session-shell/shell-host.js +26 -4
- package/dist/app/session-shell/shell-layout.d.ts +64 -0
- package/dist/app/session-shell/shell-layout.js +18 -0
- package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
- package/dist/app/session-shell/suggestion-surface.js +63 -0
- package/dist/app/session-shell/transcript-document.d.ts +78 -0
- package/dist/app/session-shell/transcript-document.js +365 -0
- package/dist/app/session-shell/viewport-layout.d.ts +31 -0
- package/dist/app/session-shell/viewport-layout.js +160 -0
- package/dist/app/session-shell/workflow-output.d.ts +33 -0
- package/dist/app/session-shell/workflow-output.js +174 -0
- package/dist/app/session-shell/workflow-router.d.ts +25 -0
- package/dist/app/session-shell/workflow-router.js +39 -0
- package/dist/composition/owned-ui.d.ts +6 -25
- package/dist/composition/owned-ui.js +48 -242
- package/dist/composition/resident-tab-bridge.d.ts +16 -0
- package/dist/composition/resident-tab-bridge.js +159 -0
- package/dist/composition/session-backend.d.ts +19 -0
- package/dist/composition/session-backend.js +1 -0
- package/dist/composition/session-runtime-registry.d.ts +20 -0
- package/dist/composition/session-runtime-registry.js +85 -0
- package/dist/composition/settings-route-host.d.ts +6 -1
- package/dist/composition/settings-route-host.js +7 -6
- package/dist/composition/startup-route.d.ts +32 -0
- package/dist/composition/startup-route.js +72 -0
- package/dist/features/owned-ui/settings-app.d.ts +4 -1
- package/dist/features/owned-ui/settings-app.js +9 -3
- package/dist/features/resident-tab/shortcut-declarations.js +1 -1
- package/dist/features/resident-tab/status-deriver.d.ts +4 -3
- package/dist/features/resident-tab/status-deriver.js +2 -5
- package/dist/foundation/release/bootstrap.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
- package/dist/foundation/resident-tabs/bridge-client.js +2 -1
- package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
- package/dist/foundation/resident-tabs/fixture-control.js +35 -0
- package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
- package/dist/foundation/resident-tabs/progress-observation.js +119 -0
- package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
- package/dist/foundation/terminal-cleanup/index.js +1 -1
- package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
- package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +4 -77
- package/dist/integrations/pi/engine/adapter.js +85 -393
- package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
- package/dist/integrations/pi/engine/command-dispatch.js +57 -0
- package/dist/integrations/pi/engine/host.js +2 -2
- package/dist/integrations/pi/engine/ports.d.ts +56 -0
- package/dist/integrations/pi/engine/ports.js +89 -0
- package/dist/integrations/pi/engine/session-runtime.js +5 -2
- package/dist/integrations/pi/engine/view-state.d.ts +42 -0
- package/dist/integrations/pi/engine/view-state.js +126 -0
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/product-identity.d.ts +2 -2
- package/dist/product-identity.js +2 -2
- package/dist/product-identity.json +6 -0
- package/dist/ui/settings/manager.d.ts +6 -0
- package/dist/ui/settings/manager.js +25 -0
- package/docs/architecture/boundaries.md +14 -0
- package/docs/architecture/resident-tabs.md +23 -18
- package/package.json +2 -1
|
@@ -1,35 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { backgroundSgrSpan, heldNativeHyperlinkStyle, hyperlinkSgrSpan, nativeHyperlinkStyle, overlaySpan, } from "../../ui/components/spans.js";
|
|
1
|
+
import { ImageAttachmentError } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
import { promptArrow } from "../../ui/components/prompt-input.js";
|
|
3
|
+
import { composeSubmittedPromptRows, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
|
|
4
|
+
import { backgroundSgrSpan, nativeHyperlinkStyle, } from "../../ui/components/spans.js";
|
|
6
5
|
import { progressStatusFrame, progressStatusText } from "../../ui/components/progress-status.js";
|
|
7
|
-
import { displayWidth, faint, stripAnsi } from "../../ui/components/text.js";
|
|
8
|
-
import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
|
|
9
6
|
import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
|
|
15
|
-
import { classifyPiTuiInput, } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
7
|
+
import { createPiShellTranscriptComponent, } from "../../integrations/pi/components/shell-presenters-transcript.js";
|
|
8
|
+
import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
|
|
9
|
+
import {} from "../../integrations/pi/components/shell-shared-facade.js";
|
|
10
|
+
import {} from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
16
11
|
import { PromptChipStore } from "./prompt-chips.js";
|
|
17
|
-
import { EditorHyperlinkBudget } from "./editor-hyperlink-budget.js";
|
|
18
12
|
import { SessionViewportController } from "./session-viewport-controller.js";
|
|
19
|
-
import {
|
|
13
|
+
import { createShellEditor } from "./shell-editor-factory.js";
|
|
14
|
+
import { ExtensionSurface } from "./extension-surface.js";
|
|
15
|
+
import { DockChrome } from "./dock-chrome.js";
|
|
16
|
+
import { WorkflowOutput } from "./workflow-output.js";
|
|
17
|
+
import { InputSurfaceSwitch } from "./input-surface-switch.js";
|
|
18
|
+
import { PinnedLayout } from "./shell-layout.js";
|
|
19
|
+
import { ViewportLayout } from "./viewport-layout.js";
|
|
20
|
+
import { nativeTranscriptLinkColor, TranscriptDocument } from "./transcript-document.js";
|
|
20
21
|
/** Composes backend session state into the owned transcript, viewport, editor, and dock presentation. */
|
|
21
22
|
export class OwnedUiSessionShellRoot {
|
|
22
23
|
editor;
|
|
23
24
|
header;
|
|
24
25
|
resources;
|
|
25
26
|
#cwd;
|
|
26
|
-
#
|
|
27
|
-
#blocksById = new Map();
|
|
28
|
-
#renderedRows = new Map();
|
|
29
|
-
// Performance: document layouts are shared by wheel, rail, jump, and selection frames.
|
|
30
|
-
#documentLayouts = new Map();
|
|
27
|
+
#document;
|
|
31
28
|
#themeUnsubscribe;
|
|
32
|
-
#transcriptOrder = [];
|
|
33
29
|
#view;
|
|
34
30
|
#status;
|
|
35
31
|
#footer;
|
|
@@ -37,15 +33,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
37
33
|
#extensionRenderers;
|
|
38
34
|
#imageAssets;
|
|
39
35
|
#promptChips;
|
|
40
|
-
#editorHyperlinks = new EditorHyperlinkBudget();
|
|
41
36
|
#pasteDiagnosticId = 0;
|
|
42
|
-
#terminalPasteDiagnosticRequest;
|
|
43
37
|
#customViewport;
|
|
44
38
|
#submittedPromptComposer;
|
|
45
39
|
#viewportController;
|
|
46
|
-
#viewportTheme;
|
|
47
|
-
#onViewportFrame;
|
|
48
|
-
#onInputSurfaceChanged;
|
|
49
40
|
#onPromptSuggestionEligibilityChanged;
|
|
50
41
|
#componentRuntime;
|
|
51
42
|
#toolsExpanded = false;
|
|
@@ -55,33 +46,11 @@ export class OwnedUiSessionShellRoot {
|
|
|
55
46
|
#showImages = true;
|
|
56
47
|
#imageWidthCells = 80;
|
|
57
48
|
#outputPad = 1;
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
#dockNotice;
|
|
64
|
-
// Invariant: bare A1 docks the release notice above the live status; closing it lasts for this session only.
|
|
65
|
-
#releaseNoticeDismissed = false;
|
|
66
|
-
#releaseCloseTarget;
|
|
67
|
-
#copyAcknowledgement;
|
|
68
|
-
#copyAcknowledgementTimer;
|
|
69
|
-
#inputSurface;
|
|
70
|
-
#inputSurfaceCoordination = "editor";
|
|
71
|
-
#dockInputReuseEnabled;
|
|
72
|
-
#dockInputCandidate = false;
|
|
73
|
-
// Invariant: receipt-time eligibility is evidence, never authorization for composition-time reuse.
|
|
74
|
-
#dockInputRevision;
|
|
75
|
-
#visibleViewportSnapshot;
|
|
76
|
-
#fullViewportCompositions = 0;
|
|
77
|
-
#dockOnlyViewportCompositions = 0;
|
|
78
|
-
#transcriptBlockRenders = 0;
|
|
79
|
-
#extensionHeader = null;
|
|
80
|
-
#extensionFooter = null;
|
|
81
|
-
#extensionWidgets = new Map();
|
|
82
|
-
#extensionStatuses = new Map();
|
|
83
|
-
#extensionWorkingMessage;
|
|
84
|
-
#extensionWorkingVisible = true;
|
|
49
|
+
#workflowOutput;
|
|
50
|
+
#input;
|
|
51
|
+
#layout;
|
|
52
|
+
#extensions = new ExtensionSurface();
|
|
53
|
+
#dockChrome;
|
|
85
54
|
#imageSubmissionsSending = 0;
|
|
86
55
|
constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
|
|
87
56
|
getMessageRenderer: () => undefined,
|
|
@@ -94,20 +63,13 @@ export class OwnedUiSessionShellRoot {
|
|
|
94
63
|
...(handlers.pasteDiagnostics === undefined ? {} : { onEvent: handlers.pasteDiagnostics }),
|
|
95
64
|
...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation }),
|
|
96
65
|
...(handlers.promptImageLimit === undefined ? {} : { imageLimit: handlers.promptImageLimit }) });
|
|
97
|
-
this.#submittedPromptComposer = this.#customViewport
|
|
98
|
-
? {
|
|
99
|
-
layout: (width, source) => submittedPromptLayout(width, source, this.#viewportController.config.timeFormat ?? "8:00 PM"),
|
|
100
|
-
compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) }, this.#viewportController.config.timeFormat ?? "8:00 PM").map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
|
|
101
|
-
}
|
|
102
|
-
: undefined;
|
|
66
|
+
this.#submittedPromptComposer = this.#customViewport ? this.#createSubmittedPromptComposer() : undefined;
|
|
103
67
|
this.#cwd = cwd;
|
|
68
|
+
this.#document = this.#createDocument(cwd);
|
|
104
69
|
this.#extensionRenderers = extensionRenderers;
|
|
105
70
|
this.#imageAssets = imageAssets;
|
|
106
71
|
this.#componentRuntime = handlers;
|
|
107
|
-
this.#onViewportFrame = handlers.onViewportFrame;
|
|
108
|
-
this.#onInputSurfaceChanged = handlers.onInputSurfaceChanged;
|
|
109
72
|
this.#onPromptSuggestionEligibilityChanged = handlers.onPromptSuggestionEligibilityChanged;
|
|
110
|
-
this.#dockInputReuseEnabled = handlers.enableDockInputReuse ?? true;
|
|
111
73
|
this.header = createPiShellHeader({
|
|
112
74
|
...startup,
|
|
113
75
|
...(this.#customViewport ? { getKeybindings: () => this.editor.keybindingConfig() } : {}),
|
|
@@ -118,81 +80,16 @@ export class OwnedUiSessionShellRoot {
|
|
|
118
80
|
frame: progressStatusFrame,
|
|
119
81
|
}, handlers);
|
|
120
82
|
this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
|
|
121
|
-
this.#footer = createPiShellFooter(this.#
|
|
122
|
-
this.editor =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
...(handlers.keybindings === undefined ? {} : { keybindings: handlers.keybindings }),
|
|
126
|
-
paintEditorSelection: (line, from, to, atomic) => backgroundSgrSpan(line, from, to, atomic ? "\u001b[7m" : "\u001b[27m\u001b[48;2;38;79;120m", atomic ? "\u001b[27m" : "\u001b[49m", piShellVisibleWidth),
|
|
127
|
-
transformPastedContent: content => {
|
|
128
|
-
try {
|
|
129
|
-
return this.#promptChips.transformPastedContent(content, this.editor.getText());
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
handlers.onPasteRejected?.(error);
|
|
133
|
-
return "";
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
...(this.#customViewport ? {
|
|
137
|
-
beginClipboardPaste: () => this.#promptChips.beginPaste(this.editor.getText(), handlers.captureClipboardPaste?.() ?? { kind: "provided", read: signal => handlers.readClipboardContent?.(signal) ?? Promise.resolve(null) }, error => handlers.onPasteRejected?.(error), () => handlers.requestRender()),
|
|
138
|
-
onPasteInput: (bytes, phase) => {
|
|
139
|
-
try {
|
|
140
|
-
if (phase === "framing")
|
|
141
|
-
this.#terminalPasteDiagnosticRequest = --this.#pasteDiagnosticId;
|
|
142
|
-
const request = this.#terminalPasteDiagnosticRequest ?? --this.#pasteDiagnosticId;
|
|
143
|
-
handlers.pasteDiagnostics?.({ request, phase, bytes, atMs: performance.now(), pending: 0, transport: "terminal",
|
|
144
|
-
...(phase === "settled" ? { outcome: "ready" } : {}) });
|
|
145
|
-
if (phase === "settled")
|
|
146
|
-
this.#terminalPasteDiagnosticRequest = undefined;
|
|
147
|
-
}
|
|
148
|
-
catch { /* Invariant: payload-free diagnostics cannot interfere with the input path. */ }
|
|
149
|
-
},
|
|
150
|
-
deferTextPaste: text => !canPreparePasteInline(text),
|
|
151
|
-
beginTextPaste: text => this.#promptChips.beginPaste(this.editor.getText(), { kind: "text", text }, error => handlers.onPasteRejected?.(error)),
|
|
152
|
-
} : {}),
|
|
153
|
-
editorAtomicRanges: line => this.#promptChips.atomicRanges(line),
|
|
154
|
-
editorHiddenRanges: line => this.#promptChips.hiddenRanges(line),
|
|
155
|
-
decorateEditorRow: (row, width, rowIndex) => {
|
|
156
|
-
if (rowIndex === 0)
|
|
157
|
-
this.#editorHyperlinks.reset();
|
|
158
|
-
const plain = stripAnsi(row);
|
|
159
|
-
const dimmed = this.#promptChips.unsentRanges(plain).reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[2m", "\u001b[22m", piShellVisibleWidth), row);
|
|
160
|
-
const ranges = this.#promptChips.hyperlinkRanges(plain);
|
|
161
|
-
if (ranges.length === 0 || !this.#editorHyperlinks.takeCleanup())
|
|
162
|
-
return dimmed;
|
|
163
|
-
const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(dimmed)))}`;
|
|
164
|
-
// Platform: VS15 is zero-column and default-ignorable. It breaks Windows Terminal's
|
|
165
|
-
// plain-text URL detector only in the held-button paint; semantic text stays exact.
|
|
166
|
-
const paintRow = this.#viewportController.editorPointerSelecting
|
|
167
|
-
? dimmed.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//")
|
|
168
|
-
: dimmed;
|
|
169
|
-
const decorated = this.#viewportController.editorPointerSelecting
|
|
170
|
-
? ranges.reduce((result, range) => backgroundSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), "\u001b[4:4m", "\u001b[24m", piShellVisibleWidth), paintRow)
|
|
171
|
-
: ranges.filter(range => this.#editorHyperlinks.take(range.target)).reduce((result, range) => hyperlinkSgrSpan(result, piShellVisibleWidth(plain.slice(0, range.start)), piShellVisibleWidth(plain.slice(0, range.end)), range.target, piShellVisibleWidth), dimmed);
|
|
172
|
-
// Platform: Windows Terminal can retain stale native dotted-link cells when a mutable
|
|
173
|
-
// prompt replaces a longer URL. Explicit non-link spaces overwrite that tail.
|
|
174
|
-
return `${decorated}${linkResetAndTail}`;
|
|
175
|
-
},
|
|
176
|
-
expandCopiedEditorText: text => this.#promptChips.expandCopiedText(text),
|
|
83
|
+
this.#footer = createPiShellFooter(this.#extensions.withStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel, handlers.footerSessionName ?? true);
|
|
84
|
+
this.editor = createShellEditor({
|
|
85
|
+
handlers,
|
|
86
|
+
customViewport: this.#customViewport,
|
|
177
87
|
cwd,
|
|
178
88
|
...(agentDir === undefined ? {} : { agentDir }),
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
measure: piShellVisibleWidth,
|
|
184
|
-
truncate: piShellTruncateToWidth,
|
|
185
|
-
}),
|
|
186
|
-
styleSuggestion: faint,
|
|
187
|
-
styleSuggestionCaret: caretCell,
|
|
188
|
-
},
|
|
189
|
-
} : {}),
|
|
190
|
-
onChange: text => {
|
|
191
|
-
handlers.onEditorChange?.(text);
|
|
192
|
-
// Concurrency: Pi clears before onSubmit; capture waiting references before pruning removed chips.
|
|
193
|
-
queueMicrotask(() => this.#promptChips.reconcileDraft(this.editor.getText()));
|
|
194
|
-
},
|
|
195
|
-
...(handlers.onPromptSuggestionAccepted === undefined ? {} : { onPromptSuggestionAccepted: handlers.onPromptSuggestionAccepted }),
|
|
89
|
+
promptChips: this.#promptChips,
|
|
90
|
+
editorPointerSelecting: () => this.#viewportController.editorPointerSelecting,
|
|
91
|
+
nextPasteDiagnosticRequest: () => --this.#pasteDiagnosticId,
|
|
92
|
+
toggleToolsExpanded: () => this.#setToolsExpanded(!this.#toolsExpanded),
|
|
196
93
|
});
|
|
197
94
|
this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned", this.#customViewport ? () => this.editor.keybindingConfig() : undefined);
|
|
198
95
|
this.#viewportController = new SessionViewportController({
|
|
@@ -204,23 +101,33 @@ export class OwnedUiSessionShellRoot {
|
|
|
204
101
|
...(handlers.pasteDiagnostics === undefined ? {} : { pasteDiagnostics: handlers.pasteDiagnostics }),
|
|
205
102
|
nextPasteDiagnosticRequest: () => --this.#pasteDiagnosticId,
|
|
206
103
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
104
|
+
this.#dockChrome = new DockChrome({
|
|
105
|
+
invalidateChrome: () => this.#invalidateChrome(),
|
|
106
|
+
requestRender: () => this.#componentRuntime.requestRender(),
|
|
107
|
+
viewport: this.#viewportController,
|
|
108
|
+
});
|
|
109
|
+
this.#workflowOutput = new WorkflowOutput({
|
|
110
|
+
customViewport: this.#customViewport,
|
|
111
|
+
document: this.#document,
|
|
112
|
+
dock: this.#dockChrome,
|
|
113
|
+
transcriptLength: () => this.#view.transcript.length,
|
|
114
|
+
outputPad: () => this.#outputPad,
|
|
115
|
+
hotkeysPresentation: () => this.hotkeysPresentation(),
|
|
116
|
+
componentRuntime: this.#componentRuntime,
|
|
117
|
+
invalidate: () => this.invalidate(),
|
|
118
|
+
});
|
|
217
119
|
this.editor.setThinkingLevel(view.thinkingLevel);
|
|
218
|
-
this.#
|
|
120
|
+
this.#input = new InputSurfaceSwitch(this.editor, {
|
|
121
|
+
dockInputReuse: this.#customViewport && (handlers.enableDockInputReuse ?? true),
|
|
122
|
+
viewport: this.#viewportController,
|
|
123
|
+
...(handlers.onInputSurfaceChanged === undefined ? {} : { onChanged: handlers.onInputSurfaceChanged }),
|
|
124
|
+
});
|
|
125
|
+
this.#layout = this.#createLayout(handlers.onViewportFrame);
|
|
219
126
|
for (const block of view.transcript) {
|
|
220
127
|
if (block.kind === "user")
|
|
221
128
|
this.editor.addToHistory(block.text);
|
|
222
129
|
}
|
|
223
|
-
this.#
|
|
130
|
+
this.#document.sync(view.transcript);
|
|
224
131
|
// Invariant: colours come from the active theme, so rendered rows outlive their revision only
|
|
225
132
|
// until the theme under them changes.
|
|
226
133
|
this.#themeUnsubscribe = onPiThemeChange(() => {
|
|
@@ -228,9 +135,51 @@ export class OwnedUiSessionShellRoot {
|
|
|
228
135
|
this.#componentRuntime.requestRender(true);
|
|
229
136
|
});
|
|
230
137
|
}
|
|
138
|
+
// Invariant: the document reads the shell's chrome lazily, so it may be built before the header and viewport.
|
|
139
|
+
#createDocument(cwd) {
|
|
140
|
+
return new TranscriptDocument({
|
|
141
|
+
customViewport: this.#customViewport,
|
|
142
|
+
cwd,
|
|
143
|
+
mount: block => this.#mountTranscript(block),
|
|
144
|
+
chrome: {
|
|
145
|
+
diagnostics: () => this.#view.diagnostics,
|
|
146
|
+
headerRows: width => this.#extensions.header !== null ? this.#extensions.header.render(width)
|
|
147
|
+
: this.#customViewport ? [] : this.header.render(width),
|
|
148
|
+
resourceRows: width => this.#customViewport ? [] : this.resources.render(width),
|
|
149
|
+
hasExtensionHeader: () => this.#extensions.header !== null,
|
|
150
|
+
thinkingVisible: () => this.#thinkingVisible,
|
|
151
|
+
timeFormat: () => this.#viewportController.config.timeFormat,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Invariant: one layout per root; the render path never consults the layout mode again.
|
|
156
|
+
#createLayout(onFrame) {
|
|
157
|
+
const ports = {
|
|
158
|
+
viewport: this.#viewportController,
|
|
159
|
+
input: this.#input,
|
|
160
|
+
editor: this.editor,
|
|
161
|
+
promptChips: this.#promptChips,
|
|
162
|
+
dockChrome: this.#dockChrome,
|
|
163
|
+
theme: VIEWPORT_THEME,
|
|
164
|
+
...(onFrame === undefined ? {} : { onFrame }),
|
|
165
|
+
rows: () => this.#componentRuntime.getRows(),
|
|
166
|
+
dock: width => this.#renderDockLayout(width),
|
|
167
|
+
document: width => this.#document.layout(width),
|
|
168
|
+
queued: width => this.#renderQueued(width),
|
|
169
|
+
liveStatus: width => this.#renderStatus(width),
|
|
170
|
+
diagnostics: () => this.#view.diagnostics,
|
|
171
|
+
};
|
|
172
|
+
return this.#customViewport ? new ViewportLayout(ports) : new PinnedLayout(ports);
|
|
173
|
+
}
|
|
174
|
+
#createSubmittedPromptComposer() {
|
|
175
|
+
return {
|
|
176
|
+
layout: (width, source) => submittedPromptLayout(width, source, this.#viewportController.config.timeFormat ?? "8:00 PM"),
|
|
177
|
+
compose: (rows, width, source, style) => composeSubmittedPromptRows(rows, width, source, { ...style, prefix: text => promptArrow(text, piTheme()) }, this.#viewportController.config.timeFormat ?? "8:00 PM").map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
231
180
|
setEditorPaddingX(padding) {
|
|
232
181
|
this.editor.setPaddingX(padding);
|
|
233
|
-
this.#
|
|
182
|
+
this.#document.invalidate("editor-padding");
|
|
234
183
|
}
|
|
235
184
|
setAutocompleteMaxVisible(maxVisible) {
|
|
236
185
|
this.editor.setAutocompleteMaxVisible(maxVisible);
|
|
@@ -240,7 +189,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
240
189
|
return;
|
|
241
190
|
this.#outputPad = padding;
|
|
242
191
|
this.#status.setOutputPad(padding);
|
|
243
|
-
for (const component of this.#
|
|
192
|
+
for (const component of this.#document.components())
|
|
244
193
|
component.setOutputPad(padding);
|
|
245
194
|
this.#invalidateTranscriptPresentation();
|
|
246
195
|
}
|
|
@@ -248,7 +197,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
248
197
|
if (this.#thinkingVisible === !hidden)
|
|
249
198
|
return;
|
|
250
199
|
this.#thinkingVisible = !hidden;
|
|
251
|
-
for (const component of this.#
|
|
200
|
+
for (const component of this.#document.components())
|
|
252
201
|
component.setHideThinkingBlock(hidden);
|
|
253
202
|
this.#invalidateTranscriptPresentation();
|
|
254
203
|
}
|
|
@@ -256,7 +205,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
256
205
|
if (this.#mermaidRenderingMode === mode)
|
|
257
206
|
return;
|
|
258
207
|
this.#mermaidRenderingMode = mode;
|
|
259
|
-
for (const component of this.#
|
|
208
|
+
for (const component of this.#document.components())
|
|
260
209
|
component.setMermaidRenderingMode(mode);
|
|
261
210
|
this.#invalidateTranscriptPresentation();
|
|
262
211
|
}
|
|
@@ -265,7 +214,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
265
214
|
return;
|
|
266
215
|
this.#showImages = showImages;
|
|
267
216
|
this.#imageWidthCells = imageWidthCells;
|
|
268
|
-
for (const component of this.#
|
|
217
|
+
for (const component of this.#document.components())
|
|
269
218
|
component.setImagePresentation(showImages, imageWidthCells);
|
|
270
219
|
this.#invalidateTranscriptPresentation();
|
|
271
220
|
}
|
|
@@ -285,7 +234,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
285
234
|
return this.#promptChips.prepareSubmission(text);
|
|
286
235
|
}
|
|
287
236
|
reconcilePromptImageLimitNotice(text = this.editor.getText()) {
|
|
288
|
-
if (this.#
|
|
237
|
+
if (this.#dockChrome.notice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
|
|
289
238
|
return false;
|
|
290
239
|
this.dismissNotice();
|
|
291
240
|
return true;
|
|
@@ -331,22 +280,22 @@ export class OwnedUiSessionShellRoot {
|
|
|
331
280
|
const becameReady = this.#view.lifecycle !== "ready" && view.lifecycle === "ready";
|
|
332
281
|
if (view.diagnostics.length !== this.#view.diagnostics.length
|
|
333
282
|
|| view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
|
|
334
|
-
this.#
|
|
283
|
+
this.#document.invalidate("diagnostics");
|
|
335
284
|
}
|
|
336
285
|
const promptAccepted = this.#view.lifecycle !== "busy" && view.lifecycle === "busy";
|
|
337
286
|
this.#view = view;
|
|
338
287
|
// Invariant: the idle new-session confirmation yields to the first truthful live status;
|
|
339
288
|
// ordinary dock notices deliberately remain visible below Working during a run.
|
|
340
|
-
if (promptAccepted && this.#
|
|
341
|
-
this.#
|
|
289
|
+
if (promptAccepted && this.#dockChrome.notice?.kind === "new-session")
|
|
290
|
+
this.#dockChrome.forgetNotice();
|
|
342
291
|
if (promptAccepted && this.#imageSubmissionsSending > 0) {
|
|
343
292
|
this.#imageSubmissionsSending = 0;
|
|
344
293
|
this.#syncWorkingOverride();
|
|
345
294
|
}
|
|
346
295
|
this.#status.update(view);
|
|
347
|
-
this.#footer.update(this.#
|
|
296
|
+
this.#footer.update(this.#extensions.withStatuses(view));
|
|
348
297
|
this.#queued.update(view.editor.queuedSubmissions);
|
|
349
|
-
this.#
|
|
298
|
+
this.#document.sync(view.transcript);
|
|
350
299
|
this.editor.setSubmitEnabled(view.lifecycle !== "stopping" && view.lifecycle !== "stopped" && view.lifecycle !== "failed");
|
|
351
300
|
this.editor.setThinkingLevel(view.thinkingLevel);
|
|
352
301
|
// Performance: semantic updates already invalidate affected transcript blocks. Chrome is a separate authority.
|
|
@@ -360,24 +309,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
360
309
|
* walk of the whole transcript.
|
|
361
310
|
*/
|
|
362
311
|
applyTranscriptBlock(block) {
|
|
363
|
-
|
|
364
|
-
// Invariant: a full-view/final presentation may preempt queued partials. Never revive an older revision.
|
|
365
|
-
if (current !== undefined && !acceptsTranscriptUpdate(current, block))
|
|
366
|
-
return;
|
|
367
|
-
this.#blocksById.set(block.id, block);
|
|
368
|
-
const component = this.#transcript.get(block.id);
|
|
369
|
-
if (component === undefined) {
|
|
370
|
-
const created = this.#mountTranscript(block);
|
|
371
|
-
this.#transcript.set(block.id, created);
|
|
372
|
-
this.#transcriptOrder.push(block.id);
|
|
373
|
-
}
|
|
374
|
-
else if (component.revision !== block.revision) {
|
|
375
|
-
component.update(block);
|
|
376
|
-
}
|
|
377
|
-
// Performance: one chunk touches one block, and the updated component tracks its own dirtiness.
|
|
378
|
-
// Invalidating the whole shell here would re-wrap the entire transcript per chunk.
|
|
379
|
-
this.#renderedRows.delete(block.id);
|
|
380
|
-
this.#documentLayouts.clear();
|
|
312
|
+
this.#document.apply(block);
|
|
381
313
|
}
|
|
382
314
|
#mountTranscript(block) {
|
|
383
315
|
// Invariant: the notice answers the reader's last command, so only their next prompt or
|
|
@@ -385,148 +317,18 @@ export class OwnedUiSessionShellRoot {
|
|
|
385
317
|
if (block.kind === "user" || block.kind === "bash")
|
|
386
318
|
this.dismissNotice();
|
|
387
319
|
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets, { ...this.#componentRuntime, changed: () => {
|
|
388
|
-
if (this.#
|
|
320
|
+
if (this.#document.component(block.id) !== created)
|
|
389
321
|
return;
|
|
390
|
-
this.#
|
|
391
|
-
this.#
|
|
392
|
-
this.#
|
|
393
|
-
this.#dockInputCandidate = false;
|
|
394
|
-
this.#dockInputRevision = undefined;
|
|
322
|
+
this.#document.blockChanged(block.id);
|
|
323
|
+
this.#layout.invalidate();
|
|
324
|
+
this.#input.clearDockInputCandidate();
|
|
395
325
|
this.#componentRuntime.requestRender();
|
|
396
326
|
} });
|
|
397
327
|
created.setExpanded(this.#toolsExpanded);
|
|
398
328
|
return created;
|
|
399
329
|
}
|
|
400
330
|
render(width) {
|
|
401
|
-
|
|
402
|
-
this.#viewportController.setEditorPointerFrame(undefined);
|
|
403
|
-
return [...this.#renderDocument(width), ...this.#renderDock(width)];
|
|
404
|
-
}
|
|
405
|
-
const height = Math.max(0, this.#componentRuntime.getRows());
|
|
406
|
-
const dock = this.#renderDockLayout(width);
|
|
407
|
-
const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
|
|
408
|
-
// Invariant: auto/always own one stable final-column gutter. Every scrollable
|
|
409
|
-
// surface ends before it; the dock remains full-width and hidden mode returns it.
|
|
410
|
-
const documentWidth = this.#viewportController.config.scrollbarAppearance === "hidden"
|
|
411
|
-
? width
|
|
412
|
-
: Math.max(1, width - 1);
|
|
413
|
-
const document = this.#renderDocumentLayout(documentWidth);
|
|
414
|
-
const steeringRows = this.#renderQueued(documentWidth);
|
|
415
|
-
const releaseBanner = this.#renderReleaseBanner(documentWidth);
|
|
416
|
-
// Rationale: the release notice rides the bottom-aligned tail directly above Working, so it
|
|
417
|
-
// never displaces transcript content and scrolls away with the live status.
|
|
418
|
-
// Invariant: when idle the banner's last row sits on the line Working would occupy.
|
|
419
|
-
const statusRows = [...releaseBanner?.rows ?? [], ...this.#renderStatus(documentWidth)];
|
|
420
|
-
const transientSignature = transientRowsSignature(steeringRows, statusRows);
|
|
421
|
-
const snapshot = this.#visibleViewportSnapshot;
|
|
422
|
-
// Invariant: paint-only feedback may cover either viewport or dock cells, so
|
|
423
|
-
// ordinary dock-only reuse cannot safely omit its overlay composition.
|
|
424
|
-
const dockInputCandidate = this.#dockInputCandidate && copyAcknowledgement === undefined;
|
|
425
|
-
// Concurrency: input may change after a keyboard receipt but before its paint.
|
|
426
|
-
// Capture only the live inputs this composition will actually represent.
|
|
427
|
-
const inputSurface = this.#inputSurface;
|
|
428
|
-
const dockRows = dock.rows;
|
|
429
|
-
const selectableDocumentRowCount = document.rows.length;
|
|
430
|
-
const dockStartRow = height - dockRows.length + 1;
|
|
431
|
-
const editorOffset = dock.editorOffset;
|
|
432
|
-
// Rationale: rendered chip tags are lossy presentation (truncated labels), so a frame drag over
|
|
433
|
-
// a draft holding chips stays visual-only; editor-owned word/line selection still copies the
|
|
434
|
-
// expanded chip text through its semantic route.
|
|
435
|
-
const draft = this.usesDefaultInputSurface() ? this.editor.getText() : "";
|
|
436
|
-
const promptCopyRows = draft.length > 0
|
|
437
|
-
&& !draft.split("\n").some(line => this.#promptChips.atomicRanges(line).length > 0)
|
|
438
|
-
? Math.max(0, dock.inputRows - 2)
|
|
439
|
-
: 0;
|
|
440
|
-
const copyableDockRows = [
|
|
441
|
-
...dock.copyableMessageRows,
|
|
442
|
-
...Array.from({ length: promptCopyRows }, (_value, index) => ({
|
|
443
|
-
row: editorOffset + index + 1,
|
|
444
|
-
prompt: index === 0 ? "first" : "continuation",
|
|
445
|
-
})),
|
|
446
|
-
...Array.from({ length: dock.footerRows }, (_value, index) => ({
|
|
447
|
-
row: dockRows.length - dock.footerRows + index,
|
|
448
|
-
})),
|
|
449
|
-
];
|
|
450
|
-
this.#viewportController.setInputSurfaceFrame(this.usesDefaultInputSurface() ? undefined : {
|
|
451
|
-
component: this.#inputSurface,
|
|
452
|
-
columnStart: 1,
|
|
453
|
-
columnEnd: width,
|
|
454
|
-
rowStart: Math.max(1, dockStartRow + editorOffset),
|
|
455
|
-
rowEnd: Math.min(height, dockStartRow + editorOffset + dock.inputRows - 1),
|
|
456
|
-
});
|
|
457
|
-
this.#viewportController.setEditorPointerFrame(this.usesDefaultInputSurface()
|
|
458
|
-
? {
|
|
459
|
-
rowStart: dockStartRow + editorOffset,
|
|
460
|
-
rowEnd: dockStartRow + editorOffset + dock.inputRows - 1,
|
|
461
|
-
}
|
|
462
|
-
: undefined);
|
|
463
|
-
const viewportRevision = this.#viewportController.presentationRevision;
|
|
464
|
-
const selectionRevision = this.#viewportController.selectionRevision;
|
|
465
|
-
const pointerPosition = this.#viewportController.pointerPosition;
|
|
466
|
-
this.#dockInputCandidate = false;
|
|
467
|
-
let frame = dockInputCandidate
|
|
468
|
-
&& snapshot !== undefined
|
|
469
|
-
&& snapshot.width === width
|
|
470
|
-
&& snapshot.height === height
|
|
471
|
-
&& snapshot.documentRows === document.rows
|
|
472
|
-
&& snapshot.transientSignature === transientSignature
|
|
473
|
-
&& snapshot.promptAnchors === document.promptAnchors
|
|
474
|
-
&& snapshot.dockLength === dockRows.length
|
|
475
|
-
&& snapshot.inputSurface === inputSurface
|
|
476
|
-
&& snapshot.viewportRevision === viewportRevision
|
|
477
|
-
&& snapshot.selectionRevision === selectionRevision
|
|
478
|
-
? this.#viewportController.composeDockOnly(dockRows, width, height, copyableDockRows)
|
|
479
|
-
: null;
|
|
480
|
-
if (frame === null) {
|
|
481
|
-
frame = this.#viewportController.compose({
|
|
482
|
-
// Performance: do not copy the complete document on unchanged dock-only input.
|
|
483
|
-
// Steering and Working retain their one non-persistent viewport-tail ownership.
|
|
484
|
-
documentRows: [...document.rows, ...steeringRows, ...statusRows],
|
|
485
|
-
...(this.#viewportController.transcriptPointerSelecting
|
|
486
|
-
? { paintDocumentRow: heldNativeHyperlinkStyle }
|
|
487
|
-
: {}),
|
|
488
|
-
// Invariant: this boundary tracks persistent transcript rows for streaming and
|
|
489
|
-
// geometry; transient Steering and Working rows remain complete-frame selectable.
|
|
490
|
-
selectableDocumentRowCount,
|
|
491
|
-
...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
|
|
492
|
-
bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
|
|
493
|
-
dockRows,
|
|
494
|
-
copyableDockRows,
|
|
495
|
-
promptAnchors: document.promptAnchors,
|
|
496
|
-
width,
|
|
497
|
-
height,
|
|
498
|
-
// Invariant: the control belongs immediately above the complete dock and
|
|
499
|
-
// floats over transient viewport content rather than consuming a dock row.
|
|
500
|
-
bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
|
|
501
|
-
...(copyAcknowledgement === undefined || height <= 0 ? {} : {
|
|
502
|
-
frameOverlay: {
|
|
503
|
-
row: Math.max(1, Math.min(height, dockStartRow + dock.inputOffset - 1)),
|
|
504
|
-
text: copyAcknowledgement,
|
|
505
|
-
},
|
|
506
|
-
}),
|
|
507
|
-
theme: this.#viewportTheme,
|
|
508
|
-
});
|
|
509
|
-
this.#fullViewportCompositions += 1;
|
|
510
|
-
}
|
|
511
|
-
else
|
|
512
|
-
this.#dockOnlyViewportCompositions += 1;
|
|
513
|
-
this.#locateReleaseClose(frame, releaseBanner, document.rows.length + steeringRows.length + statusRows.length, statusRows.length);
|
|
514
|
-
this.#visibleViewportSnapshot = {
|
|
515
|
-
width,
|
|
516
|
-
height,
|
|
517
|
-
documentRows: document.rows,
|
|
518
|
-
transientSignature,
|
|
519
|
-
promptAnchors: document.promptAnchors,
|
|
520
|
-
dockLength: dockRows.length,
|
|
521
|
-
inputSurface,
|
|
522
|
-
// Invariant: a callback during composition cannot bless older rows with its
|
|
523
|
-
// newer revision. Its ordinary pending render will publish the newer state.
|
|
524
|
-
viewportRevision,
|
|
525
|
-
selectionRevision: frame.descriptor.selectionRevision,
|
|
526
|
-
pointerPosition,
|
|
527
|
-
};
|
|
528
|
-
this.#onViewportFrame?.(frame);
|
|
529
|
-
return frame.rows;
|
|
331
|
+
return this.#layout.render(width);
|
|
530
332
|
}
|
|
531
333
|
viewportFrameDescriptor() {
|
|
532
334
|
return this.#viewportController.frame?.descriptor ?? null;
|
|
@@ -534,10 +336,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
534
336
|
/** Read-only, payload-free evidence; observing a pending frame must never render or repair it. */
|
|
535
337
|
viewportPresentationEvidence() {
|
|
536
338
|
const frame = this.#viewportController.frame;
|
|
537
|
-
const snapshot = this.#
|
|
339
|
+
const snapshot = this.#layout.composed();
|
|
538
340
|
return {
|
|
539
|
-
candidate: this.#dockInputCandidate,
|
|
540
|
-
candidateRevision: this.#dockInputCandidate ? this.#dockInputRevision ?? null : null,
|
|
341
|
+
candidate: this.#input.dockInputCandidate,
|
|
342
|
+
candidateRevision: this.#input.dockInputCandidate ? this.#input.dockInputRevision ?? null : null,
|
|
541
343
|
currentRevision: this.#viewportController.presentationRevision,
|
|
542
344
|
composedRevision: snapshot?.viewportRevision ?? null,
|
|
543
345
|
currentPointer: this.#viewportController.pointerPosition ?? null,
|
|
@@ -559,29 +361,15 @@ export class OwnedUiSessionShellRoot {
|
|
|
559
361
|
}
|
|
560
362
|
setViewportConfig(config) {
|
|
561
363
|
if (this.#viewportController.setConfig(config)) {
|
|
562
|
-
this.#
|
|
563
|
-
this.#
|
|
564
|
-
this.#visibleViewportSnapshot = undefined;
|
|
364
|
+
this.#document.invalidate("viewport-config");
|
|
365
|
+
this.#layout.invalidate();
|
|
565
366
|
}
|
|
566
367
|
}
|
|
567
368
|
setFullscreenCopyOnSelect(enabled) {
|
|
568
369
|
this.#viewportController.setCopyOnSelect(enabled);
|
|
569
370
|
}
|
|
570
371
|
showCopyAcknowledgement(message) {
|
|
571
|
-
this.#
|
|
572
|
-
this.#copyAcknowledgement = message;
|
|
573
|
-
const timer = setTimeout(() => {
|
|
574
|
-
if (this.#copyAcknowledgementTimer !== timer)
|
|
575
|
-
return;
|
|
576
|
-
this.#copyAcknowledgementTimer = undefined;
|
|
577
|
-
this.#copyAcknowledgement = undefined;
|
|
578
|
-
this.#invalidateChrome();
|
|
579
|
-
this.#componentRuntime.requestRender();
|
|
580
|
-
}, COPY_ACKNOWLEDGEMENT_DURATION_MS);
|
|
581
|
-
timer.unref?.();
|
|
582
|
-
this.#copyAcknowledgementTimer = timer;
|
|
583
|
-
this.#invalidateChrome();
|
|
584
|
-
this.#componentRuntime.requestRender();
|
|
372
|
+
this.#dockChrome.showCopyAcknowledgement(message);
|
|
585
373
|
}
|
|
586
374
|
resumeViewportFollowing() {
|
|
587
375
|
this.#viewportController.resumeFollowing();
|
|
@@ -591,13 +379,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
591
379
|
}
|
|
592
380
|
/** A new session binding owns new mounts even when it reuses semantic invocation ids. */
|
|
593
381
|
resetTranscript() {
|
|
594
|
-
|
|
595
|
-
component.dispose?.();
|
|
596
|
-
this.#transcript.clear();
|
|
597
|
-
this.#blocksById.clear();
|
|
598
|
-
this.#transcriptOrder = [];
|
|
599
|
-
this.#renderedRows.clear();
|
|
600
|
-
this.#documentLayouts.clear();
|
|
382
|
+
this.#document.reset();
|
|
601
383
|
}
|
|
602
384
|
resetViewport() {
|
|
603
385
|
this.#viewportController.reset();
|
|
@@ -615,20 +397,17 @@ export class OwnedUiSessionShellRoot {
|
|
|
615
397
|
handleViewportPreInput(data, allowWheel = true, now = Date.now(), editorActive = this.usesDefaultInputSurface()) {
|
|
616
398
|
return this.#viewportController.handlePreInput(data, allowWheel, now, editorActive);
|
|
617
399
|
}
|
|
618
|
-
#renderDock(width) {
|
|
619
|
-
return this.#renderDockLayout(width).rows;
|
|
620
|
-
}
|
|
621
400
|
#renderDockLayout(width) {
|
|
622
401
|
const queued = this.#customViewport ? [] : this.#renderQueued(width);
|
|
623
402
|
const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
|
|
624
|
-
const noticeRows = this.#
|
|
403
|
+
const noticeRows = this.#dockChrome.renderNotice(width, this.#outputPad);
|
|
625
404
|
const transientRows = [...queued, ...statusRows, ...noticeRows];
|
|
626
405
|
const messageStart = queued.length;
|
|
627
406
|
const copyableMessageRows = Array.from({ length: statusRows.length + noticeRows.length }, (_value, index) => ({ row: messageStart + index }));
|
|
628
|
-
const aboveWidgets = this.#renderWidgets("aboveEditor", width);
|
|
629
|
-
const input = this.#
|
|
407
|
+
const aboveWidgets = this.#extensions.renderWidgets("aboveEditor", width);
|
|
408
|
+
const input = this.#input.surface.render(width);
|
|
630
409
|
const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
|
|
631
|
-
const belowWidgets = this.#renderWidgets("belowEditor", width);
|
|
410
|
+
const belowWidgets = this.#extensions.renderWidgets("belowEditor", width);
|
|
632
411
|
const footer = this.#renderFooter(width);
|
|
633
412
|
const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
|
|
634
413
|
const inputOffset = transientRows.length + aboveWidgets.length;
|
|
@@ -641,66 +420,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
641
420
|
copyableMessageRows,
|
|
642
421
|
};
|
|
643
422
|
}
|
|
644
|
-
#renderCopyAcknowledgement(width) {
|
|
645
|
-
if (this.#copyAcknowledgement === undefined || width <= 0)
|
|
646
|
-
return undefined;
|
|
647
|
-
return piTheme().fg("accent", piShellTruncateToWidth(this.#copyAcknowledgement, width));
|
|
648
|
-
}
|
|
649
|
-
#clearCopyAcknowledgement(requestRender) {
|
|
650
|
-
if (this.#copyAcknowledgementTimer !== undefined)
|
|
651
|
-
clearTimeout(this.#copyAcknowledgementTimer);
|
|
652
|
-
this.#copyAcknowledgementTimer = undefined;
|
|
653
|
-
if (this.#copyAcknowledgement === undefined)
|
|
654
|
-
return;
|
|
655
|
-
this.#copyAcknowledgement = undefined;
|
|
656
|
-
this.#invalidateChrome();
|
|
657
|
-
if (requestRender)
|
|
658
|
-
this.#componentRuntime.requestRender();
|
|
659
|
-
}
|
|
660
|
-
#renderReleaseBanner(width) {
|
|
661
|
-
if (this.#releaseNoticeDismissed)
|
|
662
|
-
return undefined;
|
|
663
|
-
const diagnostic = this.#view.diagnostics.findLast(item => item.code === "release-update");
|
|
664
|
-
const release = diagnostic === undefined ? null : parseReleaseUpdateDiagnostic(diagnostic.message);
|
|
665
|
-
if (release === null)
|
|
666
|
-
return undefined;
|
|
667
|
-
return renderPiShellReleaseUpdateBanner(release, width, this.#viewportController.tailControlHovered(this.#releaseCloseTarget));
|
|
668
|
-
}
|
|
669
|
-
#locateReleaseClose(frame, banner, tailEnd, tailLength) {
|
|
670
|
-
// Invariant: the tail is the document suffix, so the control's document row is fixed relative to its
|
|
671
|
-
// end; alignment gap rows precede it and the viewport scroll decides whether it is on screen.
|
|
672
|
-
const bannerStart = tailEnd + frame.descriptor.transientAlignmentGapRows - tailLength - frame.scrollTop + 1;
|
|
673
|
-
const rowStart = banner === undefined ? 0 : Math.max(1, bannerStart + banner.close.rowStart);
|
|
674
|
-
const rowEnd = banner === undefined ? 0 : Math.min(frame.hits.viewportHeight, bannerStart + banner.close.rowEnd);
|
|
675
|
-
this.#releaseCloseTarget = banner !== undefined && rowStart <= rowEnd
|
|
676
|
-
? { rowStart, rowEnd, columnStart: banner.close.columnStart, columnEnd: banner.close.columnEnd }
|
|
677
|
-
: undefined;
|
|
678
|
-
this.#viewportController.setTailControl(this.#releaseCloseTarget === undefined ? undefined : {
|
|
679
|
-
...this.#releaseCloseTarget,
|
|
680
|
-
activate: () => {
|
|
681
|
-
this.#releaseNoticeDismissed = true;
|
|
682
|
-
this.#releaseCloseTarget = undefined;
|
|
683
|
-
},
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
#renderDockNotice(width) {
|
|
687
|
-
const notice = this.#dockNotice;
|
|
688
|
-
if (notice === undefined)
|
|
689
|
-
return [];
|
|
690
|
-
if (notice.kind === "status") {
|
|
691
|
-
// Compatibility: Pi pads its status text by one cell regardless of the output pad setting.
|
|
692
|
-
return ["", ...renderPiShellStatusText(notice.message, width, PINNED_PI_LAYOUT.outputPad)];
|
|
693
|
-
}
|
|
694
|
-
return renderPiShellCommandMessage({
|
|
695
|
-
kind: notice.kind === "new-session" ? "accent" : notice.kind,
|
|
696
|
-
message: notice.message,
|
|
697
|
-
}, width, this.#outputPad);
|
|
698
|
-
}
|
|
699
423
|
dismissNotice() {
|
|
700
|
-
|
|
701
|
-
return;
|
|
702
|
-
this.#dockNotice = undefined;
|
|
703
|
-
this.#invalidateChrome();
|
|
424
|
+
this.#dockChrome.dismissNotice();
|
|
704
425
|
}
|
|
705
426
|
#renderQueued(width) {
|
|
706
427
|
return this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
|
|
@@ -710,287 +431,30 @@ export class OwnedUiSessionShellRoot {
|
|
|
710
431
|
return {
|
|
711
432
|
document: layoutPort(width => this.#renderDocument(width), () => this.invalidate()),
|
|
712
433
|
queued: layoutPort(width => this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width), () => this.#queued.invalidate()),
|
|
713
|
-
aboveWidgets: layoutPort(width => this.#renderWidgets("aboveEditor", width), () => this.#
|
|
434
|
+
aboveWidgets: layoutPort(width => this.#extensions.renderWidgets("aboveEditor", width), () => this.#extensions.invalidate()),
|
|
714
435
|
status: layoutPort(width => this.#renderStatus(width), () => this.#status.invalidate()),
|
|
715
|
-
editor: layoutPort(width => this.#
|
|
716
|
-
belowWidgets: layoutPort(width => this.#renderWidgets("belowEditor", width), () => this.#
|
|
717
|
-
footer: layoutPort(width => this.#renderFooter(width), () => (this.#
|
|
436
|
+
editor: layoutPort(width => this.#input.surface.render(width), () => this.#input.surface.invalidate(), data => this.#input.surface.handleInput?.(data)),
|
|
437
|
+
belowWidgets: layoutPort(width => this.#extensions.renderWidgets("belowEditor", width), () => this.#extensions.invalidate()),
|
|
438
|
+
footer: layoutPort(width => this.#renderFooter(width), () => (this.#extensions.footer ?? this.#footer).invalidate()),
|
|
718
439
|
};
|
|
719
440
|
}
|
|
720
441
|
#renderDocument(width) {
|
|
721
|
-
return this.#
|
|
722
|
-
}
|
|
723
|
-
#renderDocumentLayout(width) {
|
|
724
|
-
// Performance: extension headers may animate independently of transcript revisions, so
|
|
725
|
-
// only the stable built-in document participates in this frame cache.
|
|
726
|
-
const cached = this.#extensionHeader === null ? this.#documentLayouts.get(width) : undefined;
|
|
727
|
-
if (cached !== undefined)
|
|
728
|
-
return cached;
|
|
729
|
-
const diagnostics = this.#view.diagnostics;
|
|
730
|
-
const startupRows = diagnostics
|
|
731
|
-
// Compatibility: the pinned route keeps trust failures with Pi-style startup diagnostics;
|
|
732
|
-
// bare A1 presents them in its prompt-adjacent notice dock instead.
|
|
733
|
-
.filter(diagnostic => diagnostic.code === "engine-startup"
|
|
734
|
-
|| !this.#customViewport && diagnostic.code === "project-trust")
|
|
735
|
-
.flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
|
|
736
|
-
const resourceRows = this.#customViewport ? [] : [...this.resources.render(width)];
|
|
737
|
-
if (resourceRows.at(-1) === "")
|
|
738
|
-
resourceRows.pop();
|
|
739
|
-
const headerRows = this.#extensionHeader !== null
|
|
740
|
-
? this.#extensionHeader.render(width)
|
|
741
|
-
: this.#customViewport ? [] : this.header.render(width);
|
|
742
|
-
const rows = [
|
|
743
|
-
...startupRows,
|
|
744
|
-
...headerRows,
|
|
745
|
-
...resourceRows,
|
|
746
|
-
];
|
|
747
|
-
const promptAnchors = [];
|
|
748
|
-
// Rationale: a block that is still streaming re-renders every row it owns, so the frame
|
|
749
|
-
// reports where its rows begin. Damage-aware painting uses that to tell a live block's
|
|
750
|
-
// own reflow apart from a repaint that reaches settled rows.
|
|
751
|
-
let liveTailStartRow;
|
|
752
|
-
for (let index = 0; index < this.#transcriptOrder.length; index += 1) {
|
|
753
|
-
const id = this.#transcriptOrder[index];
|
|
754
|
-
const block = this.#blocksById.get(id);
|
|
755
|
-
if (!this.#thinkingVisible && block?.kind === "thinking")
|
|
756
|
-
continue;
|
|
757
|
-
const promptLike = block !== undefined
|
|
758
|
-
&& (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block));
|
|
759
|
-
// Invariant: the custom viewport has already removed its rail gutter from this width.
|
|
760
|
-
// Prompt-like and ordinary blocks therefore share one right content edge.
|
|
761
|
-
const blockWidth = width;
|
|
762
|
-
const blockRows = this.#blockRows(id, block, blockWidth);
|
|
763
|
-
if (promptLike) {
|
|
764
|
-
// Compatibility: the first natural prompt gets one breathing row at the document top.
|
|
765
|
-
// Once scrolling advances, the prompt itself reaches row zero and then
|
|
766
|
-
// becomes sticky there, so the spacer is never pinned with it.
|
|
767
|
-
if (this.#customViewport && index === 0)
|
|
768
|
-
rows.push("");
|
|
769
|
-
else if (index > 0)
|
|
770
|
-
rows.push("");
|
|
771
|
-
}
|
|
772
|
-
const firstRow = rows.length;
|
|
773
|
-
if (liveTailStartRow === undefined && block !== undefined && block.status !== "finalized") {
|
|
774
|
-
liveTailStartRow = firstRow;
|
|
775
|
-
}
|
|
776
|
-
rows.push(...blockRows);
|
|
777
|
-
if (promptLike && blockRows[0] !== undefined) {
|
|
778
|
-
const source = pinnedPromptSource(block, blockRows[0], blockWidth, this.#viewportController.config.timeFormat);
|
|
779
|
-
promptAnchors.push({
|
|
780
|
-
id: block.id,
|
|
781
|
-
firstRow,
|
|
782
|
-
lastRow: Math.max(firstRow, rows.length - 1),
|
|
783
|
-
sourceRow: source.row,
|
|
784
|
-
copySuffixWidth: source.copySuffixWidth,
|
|
785
|
-
prominentSourceRow: blockRows[0],
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
// Invariant: the A1 release notice precedes the extension-package notice, matching pinned Pi's order.
|
|
790
|
-
// Bare A1 docks it in the viewport tail instead of the document.
|
|
791
|
-
rows.push(...diagnostics
|
|
792
|
-
.filter(diagnostic => !this.#customViewport && diagnostic.code === "release-update")
|
|
793
|
-
.flatMap(diagnostic => {
|
|
794
|
-
const release = parseReleaseUpdateDiagnostic(diagnostic.message);
|
|
795
|
-
return release === null ? [] : renderPiShellReleaseUpdateNotice(release, width);
|
|
796
|
-
}));
|
|
797
|
-
rows.push(...diagnostics
|
|
798
|
-
.filter(diagnostic => diagnostic.code === "package-updates")
|
|
799
|
-
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
|
|
800
|
-
rows.push(...diagnostics
|
|
801
|
-
.filter(diagnostic => diagnostic.code === "changelog-collapsed" || diagnostic.code === "changelog-expanded")
|
|
802
|
-
// Rationale: bare A1 translates either startup diagnostic into transient dock chrome;
|
|
803
|
-
// the pinned layout retains Pi's collapsed hint or expanded document.
|
|
804
|
-
.flatMap(diagnostic => this.#customViewport
|
|
805
|
-
? []
|
|
806
|
-
: diagnostic.code === "changelog-collapsed"
|
|
807
|
-
? createPiShellCollapsedChangelog().render(width)
|
|
808
|
-
: createPiShellChangelog(diagnostic.message).render(width)));
|
|
809
|
-
rows.push(...diagnostics
|
|
810
|
-
.filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "project-trust"
|
|
811
|
-
&& diagnostic.code !== "release-update"
|
|
812
|
-
&& diagnostic.code !== "package-updates" && diagnostic.code !== "changelog-collapsed"
|
|
813
|
-
&& diagnostic.code !== "changelog-expanded")
|
|
814
|
-
.slice(-3)
|
|
815
|
-
.flatMap(diagnostic => renderPiShellTranscriptBlock({
|
|
816
|
-
id: `diagnostic-${diagnostic.sequence}`,
|
|
817
|
-
kind: diagnostic.severity === "error" ? "error" : "system",
|
|
818
|
-
status: "finalized",
|
|
819
|
-
revision: diagnostic.sequence,
|
|
820
|
-
title: diagnostic.code,
|
|
821
|
-
text: diagnostic.message,
|
|
822
|
-
payload: {},
|
|
823
|
-
}, width, this.#cwd)));
|
|
824
|
-
const layout = { rows: Object.freeze(rows), promptAnchors: Object.freeze(promptAnchors), liveTailStartRow };
|
|
825
|
-
if (this.#extensionHeader === null) {
|
|
826
|
-
this.#documentLayouts.set(width, layout);
|
|
827
|
-
// Performance: the custom viewport commonly probes full width and reserved-rail width.
|
|
828
|
-
while (this.#documentLayouts.size > 2)
|
|
829
|
-
this.#documentLayouts.delete(this.#documentLayouts.keys().next().value);
|
|
830
|
-
}
|
|
831
|
-
return layout;
|
|
832
|
-
}
|
|
833
|
-
// Performance: finalized blocks cache by revision and width; live blocks always render.
|
|
834
|
-
#blockRows(id, block, width) {
|
|
835
|
-
const component = this.#transcript.get(id);
|
|
836
|
-
if (component === undefined)
|
|
837
|
-
return [];
|
|
838
|
-
const render = () => {
|
|
839
|
-
this.#transcriptBlockRenders += 1;
|
|
840
|
-
const rows = component.render(width);
|
|
841
|
-
if (!this.#customViewport || block?.kind === "user")
|
|
842
|
-
return rows;
|
|
843
|
-
return rows.map(row => nativeHyperlinkStyle(row, nativeTranscriptLinkColor));
|
|
844
|
-
};
|
|
845
|
-
if (block === undefined || block.status !== "finalized")
|
|
846
|
-
return render();
|
|
847
|
-
let byWidth = this.#renderedRows.get(id);
|
|
848
|
-
const cached = byWidth?.get(width);
|
|
849
|
-
const presentationRevision = component.presentationRevision ?? 0;
|
|
850
|
-
if (cached?.revision === block.revision && cached.presentationRevision === presentationRevision)
|
|
851
|
-
return cached.rows;
|
|
852
|
-
const rows = render();
|
|
853
|
-
if (byWidth === undefined) {
|
|
854
|
-
byWidth = new Map();
|
|
855
|
-
this.#renderedRows.set(id, byWidth);
|
|
856
|
-
}
|
|
857
|
-
byWidth.set(width, { revision: block.revision, presentationRevision, rows });
|
|
858
|
-
// Invariant: bare A1 probes full width before reserving the overflowing rail column;
|
|
859
|
-
// retain both widths without turning resize history into an unbounded cache.
|
|
860
|
-
while (byWidth.size > 2)
|
|
861
|
-
byWidth.delete(byWidth.keys().next().value);
|
|
862
|
-
return rows;
|
|
442
|
+
return this.#document.layout(width).rows;
|
|
863
443
|
}
|
|
864
444
|
transcriptComponent(id) {
|
|
865
|
-
return this.#
|
|
445
|
+
return this.#document.component(id);
|
|
866
446
|
}
|
|
867
447
|
exitTranscript(width) {
|
|
868
|
-
|
|
869
|
-
for (const id of this.#transcriptOrder) {
|
|
870
|
-
const block = this.#blocksById.get(id);
|
|
871
|
-
if (block === undefined || (!this.#thinkingVisible && block.kind === "thinking"))
|
|
872
|
-
continue;
|
|
873
|
-
if (rows.length > 0 && (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block)))
|
|
874
|
-
rows.push("");
|
|
875
|
-
rows.push(...this.#blockRows(id, block, width).map(sanitizeExitTranscriptRow));
|
|
876
|
-
}
|
|
877
|
-
while (rows.at(-1) === "")
|
|
878
|
-
rows.pop();
|
|
879
|
-
return rows.join("\n");
|
|
448
|
+
return this.#document.exitTranscript(width);
|
|
880
449
|
}
|
|
881
450
|
appendWorkflowStatus(message) {
|
|
882
|
-
|
|
883
|
-
// reader's eye already is, instead of a transcript row that opens an empty session at the
|
|
884
|
-
// top-left or sinks into a long feed. The pinned route keeps Pi's chat placement.
|
|
885
|
-
if (this.#customViewport) {
|
|
886
|
-
this.#dockNotice = { kind: "status", message };
|
|
887
|
-
this.#invalidateChrome();
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
const previousId = this.#lastWorkflowStatusId;
|
|
891
|
-
if (previousId !== undefined
|
|
892
|
-
&& this.#transcriptOrder.at(-1) === previousId
|
|
893
|
-
&& this.#workflowStatusAnchors.get(previousId) === this.#view.transcript.length) {
|
|
894
|
-
this.#workflowStatusMessages.set(previousId, message);
|
|
895
|
-
this.#documentLayouts.clear();
|
|
896
|
-
this.invalidate();
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
const id = this.#appendAnchoredWorkflowComponent(width => [
|
|
900
|
-
"",
|
|
901
|
-
...renderPiShellStatusText(this.#workflowStatusMessages.get(id) ?? message, width),
|
|
902
|
-
]);
|
|
903
|
-
this.#workflowStatusMessages.set(id, message);
|
|
904
|
-
this.#lastWorkflowStatusId = id;
|
|
451
|
+
this.#workflowOutput.appendStatus(message);
|
|
905
452
|
}
|
|
906
453
|
appendWorkflowMessage(message, errorCode) {
|
|
907
|
-
|
|
908
|
-
this.appendWorkflowStatus(message.message);
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
if (this.#customViewport && (message.kind === "warning" || message.kind === "error")) {
|
|
912
|
-
this.#dockNotice = {
|
|
913
|
-
kind: errorCode === "image-count" ? "warning" : message.kind,
|
|
914
|
-
message: message.message,
|
|
915
|
-
...(errorCode === undefined ? {} : { errorCode }),
|
|
916
|
-
};
|
|
917
|
-
this.#invalidateChrome();
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
this.#lastWorkflowStatusId = undefined;
|
|
921
|
-
const presentation = { kind: message.kind, message: message.message };
|
|
922
|
-
this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
|
|
454
|
+
this.#workflowOutput.appendMessage(message, errorCode);
|
|
923
455
|
}
|
|
924
456
|
appendWorkflowResult(result, errorCode) {
|
|
925
|
-
|
|
926
|
-
for (const message of result.messages)
|
|
927
|
-
this.appendWorkflowMessage(message, errorCode);
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
if (result.messageKind === "silent" || (result.outcome === "cancelled" && result.messageKind === undefined))
|
|
931
|
-
return;
|
|
932
|
-
if (result.command === "reload" && result.outcome === "completed") {
|
|
933
|
-
this.appendWorkflowStatus(result.message);
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
if (result.command === "session" && result.outcome === "completed" && result.presentation?.kind === "session-info") {
|
|
937
|
-
this.#lastWorkflowStatusId = undefined;
|
|
938
|
-
const sessionInfo = createPiShellSessionInfo(result.presentation);
|
|
939
|
-
this.#appendAnchoredWorkflowComponent(width => sessionInfo.render(width), () => sessionInfo.dispose?.());
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
if (result.command === "hotkeys" && result.outcome === "completed") {
|
|
943
|
-
this.#lastWorkflowStatusId = undefined;
|
|
944
|
-
const presentation = this.hotkeysPresentation();
|
|
945
|
-
const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile, presentation.residentTabs);
|
|
946
|
-
this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
if (result.command === "changelog" && result.outcome === "completed") {
|
|
950
|
-
this.#lastWorkflowStatusId = undefined;
|
|
951
|
-
const changelog = createPiShellChangelog(result.detail ?? "No changelog entries found.");
|
|
952
|
-
this.#appendAnchoredWorkflowComponent(width => changelog.render(width), () => changelog.dispose?.());
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
if (result.outcome === "failed") {
|
|
956
|
-
this.appendWorkflowMessage({
|
|
957
|
-
kind: result.messageKind === "warning" ? "warning" : "error",
|
|
958
|
-
message: result.message,
|
|
959
|
-
}, errorCode);
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
|
|
963
|
-
return;
|
|
964
|
-
if (result.outcome === "completed" && result.command === "new" && this.#customViewport) {
|
|
965
|
-
// Rationale: bare A1 keeps the replacement session visually empty while preserving Pi's accent notice shape.
|
|
966
|
-
this.#dockNotice = { kind: "new-session", message: result.message };
|
|
967
|
-
this.#invalidateChrome();
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
if (result.outcome === "completed" && (result.command === "new" || result.command === "name" || result.command === "debug")) {
|
|
971
|
-
this.#lastWorkflowStatusId = undefined;
|
|
972
|
-
const presentation = { kind: result.command, message: result.message, ...(result.detail === undefined ? {} : { detail: result.detail }) };
|
|
973
|
-
this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (result.command === "arminsayshi" && result.outcome === "completed") {
|
|
977
|
-
this.#lastWorkflowStatusId = undefined;
|
|
978
|
-
const armin = createPiShellArmin(this.#componentRuntime);
|
|
979
|
-
this.#appendAnchoredWorkflowComponent(width => ["", ...armin.render(width)], () => armin.dispose?.());
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
if (result.command === "dementedelves" && result.outcome === "completed") {
|
|
983
|
-
this.#lastWorkflowStatusId = undefined;
|
|
984
|
-
const announcement = createPiShellEarendilAnnouncement();
|
|
985
|
-
this.#appendAnchoredWorkflowComponent(width => ["", ...announcement.render(width)], () => announcement.dispose?.());
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
// Rationale: the pinned engine links both share URLs; the workflow result carries the plain URLs.
|
|
989
|
-
const message = result.command !== "share" ? result.message
|
|
990
|
-
: result.detail ? `${linkShareUrl(result.message)}\nGist: ${piShellHyperlink(result.detail)}`
|
|
991
|
-
// Protocol: Radius shares carry only the artifact URL.
|
|
992
|
-
: linkShareUrl(result.message);
|
|
993
|
-
this.appendWorkflowStatus(message);
|
|
457
|
+
this.#workflowOutput.appendResult(result, errorCode);
|
|
994
458
|
}
|
|
995
459
|
toggleThinkingVisibility() {
|
|
996
460
|
this.setHideThinkingBlock(this.#thinkingVisible);
|
|
@@ -1002,39 +466,25 @@ export class OwnedUiSessionShellRoot {
|
|
|
1002
466
|
this.#setToolsExpanded(expanded);
|
|
1003
467
|
}
|
|
1004
468
|
setExtensionWidget(key, component, placement) {
|
|
1005
|
-
|
|
1006
|
-
if (component === null)
|
|
1007
|
-
this.#extensionWidgets.delete(key);
|
|
1008
|
-
else
|
|
1009
|
-
this.#extensionWidgets.set(key, { component, placement });
|
|
1010
|
-
if (previous !== undefined && previous !== component)
|
|
1011
|
-
previous.dispose?.();
|
|
469
|
+
this.#extensions.placeWidget(key, component, placement);
|
|
1012
470
|
this.invalidate();
|
|
1013
471
|
}
|
|
1014
472
|
setExtensionHeader(component) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
this.#extensionHeader = component;
|
|
1018
|
-
this.#documentLayouts.clear();
|
|
473
|
+
this.#extensions.replaceHeader(component);
|
|
474
|
+
this.#document.invalidate("extension-header");
|
|
1019
475
|
this.invalidate();
|
|
1020
476
|
}
|
|
1021
477
|
setExtensionFooter(component) {
|
|
1022
|
-
|
|
1023
|
-
this.#extensionFooter?.dispose?.();
|
|
1024
|
-
this.#extensionFooter = component;
|
|
478
|
+
this.#extensions.replaceFooter(component);
|
|
1025
479
|
this.invalidate();
|
|
1026
480
|
}
|
|
1027
481
|
setExtensionStatus(key, text) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
else
|
|
1031
|
-
this.#extensionStatuses.set(key, text);
|
|
1032
|
-
this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
|
|
482
|
+
this.#extensions.setStatus(key, text);
|
|
483
|
+
this.#footer.update(this.#extensions.withStatuses(this.#view));
|
|
1033
484
|
this.invalidate();
|
|
1034
485
|
}
|
|
1035
|
-
setExtensionWorking(message, visible = this.#
|
|
1036
|
-
this.#
|
|
1037
|
-
this.#extensionWorkingVisible = visible;
|
|
486
|
+
setExtensionWorking(message, visible = this.#extensions.working.visible) {
|
|
487
|
+
this.#extensions.setWorking(message, visible);
|
|
1038
488
|
this.#syncWorkingOverride();
|
|
1039
489
|
this.invalidate();
|
|
1040
490
|
}
|
|
@@ -1056,7 +506,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
1056
506
|
const sending = this.#imageSubmissionsSending > 0;
|
|
1057
507
|
this.#status.setWorkingOverride(sending
|
|
1058
508
|
? "Sending…"
|
|
1059
|
-
: this.#
|
|
509
|
+
: this.#extensions.working.visible ? this.#extensions.working.message : undefined, sending);
|
|
1060
510
|
}
|
|
1061
511
|
hotkeysPresentation() {
|
|
1062
512
|
return {
|
|
@@ -1066,34 +516,17 @@ export class OwnedUiSessionShellRoot {
|
|
|
1066
516
|
};
|
|
1067
517
|
}
|
|
1068
518
|
resetWorkflowPresentation() {
|
|
1069
|
-
this.#
|
|
1070
|
-
this.#clearCopyAcknowledgement(false);
|
|
1071
|
-
|
|
1072
|
-
this.#transcript.get(id)?.dispose?.();
|
|
1073
|
-
this.#transcript.delete(id);
|
|
1074
|
-
}
|
|
1075
|
-
this.#workflowStatusAnchors.clear();
|
|
1076
|
-
this.#workflowStatusMessages.clear();
|
|
1077
|
-
this.#transcriptOrder = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
|
|
1078
|
-
this.#lastWorkflowStatusId = undefined;
|
|
1079
|
-
this.#documentLayouts.clear();
|
|
519
|
+
this.#dockChrome.forgetNotice();
|
|
520
|
+
this.#dockChrome.clearCopyAcknowledgement(false);
|
|
521
|
+
this.#workflowOutput.reset();
|
|
1080
522
|
this.invalidate();
|
|
1081
523
|
}
|
|
1082
524
|
resetExtensionUi() {
|
|
1083
|
-
this.#
|
|
1084
|
-
this.#extensionFooter?.dispose?.();
|
|
1085
|
-
for (const { component } of this.#extensionWidgets.values())
|
|
1086
|
-
component.dispose?.();
|
|
1087
|
-
this.#extensionHeader = null;
|
|
1088
|
-
this.#extensionFooter = null;
|
|
1089
|
-
this.#extensionWidgets.clear();
|
|
1090
|
-
this.#extensionStatuses.clear();
|
|
1091
|
-
this.#extensionWorkingMessage = undefined;
|
|
525
|
+
this.#extensions.reset();
|
|
1092
526
|
this.#syncWorkingOverride();
|
|
1093
|
-
this.#footer.update(this.#
|
|
527
|
+
this.#footer.update(this.#extensions.withStatuses(this.#view));
|
|
1094
528
|
// Invariant: an extension renderer may have drawn transcript blocks that are now unrendered by it.
|
|
1095
|
-
this.#
|
|
1096
|
-
this.#documentLayouts.clear();
|
|
529
|
+
this.#document.invalidate("extension-renderers");
|
|
1097
530
|
this.invalidate();
|
|
1098
531
|
}
|
|
1099
532
|
addExtensionNotification(message, type) {
|
|
@@ -1106,268 +539,93 @@ export class OwnedUiSessionShellRoot {
|
|
|
1106
539
|
extensionFooterData() {
|
|
1107
540
|
return {
|
|
1108
541
|
getGitBranch: () => this.#view.status.footer?.branch ?? null,
|
|
1109
|
-
getExtensionStatuses: () =>
|
|
542
|
+
getExtensionStatuses: () => this.#extensions.statuses(),
|
|
1110
543
|
getAvailableProviderCount: () => this.#view.status.footer?.availableProviderCount ?? 1,
|
|
1111
544
|
onBranchChange: () => () => { },
|
|
1112
545
|
};
|
|
1113
546
|
}
|
|
1114
547
|
usesDefaultInputSurface() {
|
|
1115
|
-
return this.#
|
|
548
|
+
return this.#input.usesEditor;
|
|
1116
549
|
}
|
|
1117
550
|
inputCoordinationSurface() {
|
|
1118
|
-
return this.#
|
|
551
|
+
return this.#input.coordination;
|
|
1119
552
|
}
|
|
1120
553
|
viewportCompositionEvidence() {
|
|
1121
|
-
return
|
|
554
|
+
return this.#layout.compositions();
|
|
1122
555
|
}
|
|
1123
556
|
transcriptRenderCount() {
|
|
1124
|
-
return this.#
|
|
557
|
+
return this.#document.renderCount;
|
|
1125
558
|
}
|
|
1126
559
|
setFooterLevel(visible) { this.#footerLevel = visible; }
|
|
1127
560
|
setInputSurface(component, disposePrevious = true, coordination = "owned") {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (next === this.#inputSurface) {
|
|
1131
|
-
this.#inputSurfaceCoordination = nextCoordination;
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
this.#dockInputCandidate = false;
|
|
1135
|
-
this.#viewportController.invalidateInputSurface();
|
|
1136
|
-
this.#inputSurface.setFocused?.(false);
|
|
1137
|
-
if (disposePrevious && this.#inputSurface !== this.editor)
|
|
1138
|
-
this.#inputSurface.dispose?.();
|
|
1139
|
-
this.#inputSurface = next;
|
|
1140
|
-
this.#inputSurfaceCoordination = nextCoordination;
|
|
1141
|
-
this.#onInputSurfaceChanged?.();
|
|
1142
|
-
this.#inputSurface.setFocused?.(true);
|
|
1143
|
-
this.invalidate();
|
|
561
|
+
if (this.#input.set(component, disposePrevious, coordination))
|
|
562
|
+
this.invalidate();
|
|
1144
563
|
}
|
|
1145
564
|
handleInput(data) {
|
|
1146
|
-
this.#
|
|
1147
|
-
&& classifyPiTuiInput(data, this.#inputSurfaceCoordination) === "safe";
|
|
1148
|
-
this.#dockInputRevision = this.#dockInputCandidate ? this.#viewportController.presentationRevision : undefined;
|
|
1149
|
-
try {
|
|
1150
|
-
this.#inputSurface.handleInput?.(data);
|
|
1151
|
-
}
|
|
1152
|
-
catch (error) {
|
|
1153
|
-
this.#dockInputCandidate = false;
|
|
1154
|
-
throw error;
|
|
1155
|
-
}
|
|
565
|
+
this.#input.handleInput(data);
|
|
1156
566
|
}
|
|
1157
567
|
invalidate() {
|
|
1158
|
-
this.#
|
|
1159
|
-
this.#documentLayouts.clear();
|
|
1160
|
-
for (const component of this.#transcript.values())
|
|
1161
|
-
component.invalidate();
|
|
568
|
+
this.#document.invalidate("repaint");
|
|
1162
569
|
this.#invalidateChrome();
|
|
1163
570
|
}
|
|
1164
571
|
#invalidateChrome() {
|
|
1165
572
|
this.header.invalidate();
|
|
1166
573
|
this.resources.invalidate();
|
|
1167
574
|
this.editor.invalidate();
|
|
1168
|
-
if (this.#
|
|
1169
|
-
this.#
|
|
1170
|
-
this.#
|
|
575
|
+
if (!this.#input.usesEditor)
|
|
576
|
+
this.#input.surface.invalidate();
|
|
577
|
+
this.#extensions.invalidate();
|
|
1171
578
|
this.#status.invalidate();
|
|
1172
579
|
this.#footer.invalidate();
|
|
1173
580
|
this.#queued.invalidate();
|
|
1174
581
|
}
|
|
1175
582
|
setFocused(focused) {
|
|
1176
|
-
this.#
|
|
583
|
+
this.#input.surface.setFocused?.(focused);
|
|
1177
584
|
if (focused)
|
|
1178
585
|
this.#onPromptSuggestionEligibilityChanged?.();
|
|
1179
586
|
}
|
|
1180
587
|
dispose() {
|
|
1181
|
-
this.#clearCopyAcknowledgement(false);
|
|
588
|
+
this.#dockChrome.clearCopyAcknowledgement(false);
|
|
1182
589
|
this.clearViewportPointerState();
|
|
1183
590
|
this.#themeUnsubscribe();
|
|
1184
591
|
this.header.dispose?.();
|
|
1185
592
|
this.resources.dispose?.();
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
this.#
|
|
1189
|
-
this.#
|
|
1190
|
-
this.#documentLayouts.clear();
|
|
1191
|
-
this.#visibleViewportSnapshot = undefined;
|
|
1192
|
-
this.#dockInputCandidate = false;
|
|
1193
|
-
this.#dockInputRevision = undefined;
|
|
1194
|
-
if (this.#inputSurface !== this.editor)
|
|
1195
|
-
this.#inputSurface.dispose?.();
|
|
1196
|
-
this.#extensionHeader?.dispose?.();
|
|
1197
|
-
this.#extensionFooter?.dispose?.();
|
|
1198
|
-
for (const { component } of this.#extensionWidgets.values())
|
|
1199
|
-
component.dispose?.();
|
|
1200
|
-
this.#extensionWidgets.clear();
|
|
593
|
+
this.#document.dispose();
|
|
594
|
+
this.#layout.invalidate();
|
|
595
|
+
this.#input.dispose();
|
|
596
|
+
this.#extensions.dispose();
|
|
1201
597
|
this.editor.dispose?.();
|
|
1202
598
|
this.#status.dispose?.();
|
|
1203
599
|
this.#footer.dispose?.();
|
|
1204
600
|
this.#queued.dispose?.();
|
|
1205
601
|
}
|
|
1206
602
|
#invalidateTranscriptPresentation() {
|
|
1207
|
-
this.#
|
|
1208
|
-
this.#documentLayouts.clear();
|
|
603
|
+
this.#document.invalidate("block-presentation");
|
|
1209
604
|
this.invalidate();
|
|
1210
605
|
}
|
|
1211
|
-
#syncTranscript(blocks) {
|
|
1212
|
-
const previousIds = this.#transcriptOrder.filter(id => !id.startsWith("workflow-status-"));
|
|
1213
|
-
const transcriptChanged = previousIds.length !== blocks.length || blocks.some((block, index) => {
|
|
1214
|
-
const previous = this.#blocksById.get(block.id);
|
|
1215
|
-
return previousIds[index] !== block.id || previous?.revision !== block.revision;
|
|
1216
|
-
});
|
|
1217
|
-
if (transcriptChanged)
|
|
1218
|
-
this.#documentLayouts.clear();
|
|
1219
|
-
this.#blocksById.clear();
|
|
1220
|
-
for (const block of blocks)
|
|
1221
|
-
this.#blocksById.set(block.id, block);
|
|
1222
|
-
const nextIds = new Set(blocks.map(block => block.id));
|
|
1223
|
-
for (const [id, component] of this.#transcript) {
|
|
1224
|
-
if (id.startsWith("workflow-status-") || nextIds.has(id))
|
|
1225
|
-
continue;
|
|
1226
|
-
component.dispose?.();
|
|
1227
|
-
this.#transcript.delete(id);
|
|
1228
|
-
this.#renderedRows.delete(id);
|
|
1229
|
-
}
|
|
1230
|
-
for (const block of blocks) {
|
|
1231
|
-
const component = this.#transcript.get(block.id);
|
|
1232
|
-
if (component === undefined) {
|
|
1233
|
-
const created = this.#mountTranscript(block);
|
|
1234
|
-
this.#transcript.set(block.id, created);
|
|
1235
|
-
}
|
|
1236
|
-
else if (component.revision !== block.revision) {
|
|
1237
|
-
component.update(block);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
const statusIds = [...this.#workflowStatusAnchors.keys()];
|
|
1241
|
-
const order = [];
|
|
1242
|
-
for (let index = 0; index <= blocks.length; index += 1) {
|
|
1243
|
-
for (const statusId of statusIds) {
|
|
1244
|
-
if (this.#workflowStatusAnchors.get(statusId) === index)
|
|
1245
|
-
order.push(statusId);
|
|
1246
|
-
}
|
|
1247
|
-
const block = blocks[index];
|
|
1248
|
-
if (block !== undefined)
|
|
1249
|
-
order.push(block.id);
|
|
1250
|
-
}
|
|
1251
|
-
const placed = new Set(order);
|
|
1252
|
-
for (const statusId of statusIds) {
|
|
1253
|
-
if (!placed.has(statusId))
|
|
1254
|
-
order.push(statusId);
|
|
1255
|
-
}
|
|
1256
|
-
this.#transcriptOrder = order;
|
|
1257
|
-
}
|
|
1258
|
-
#appendAnchoredWorkflowComponent(render, dispose) {
|
|
1259
|
-
// Invariant: transcript-bound workflow output supersedes a pending acknowledgement.
|
|
1260
|
-
this.dismissNotice();
|
|
1261
|
-
this.#workflowTranscriptSequence += 1;
|
|
1262
|
-
const id = `workflow-status-${this.#workflowTranscriptSequence}`;
|
|
1263
|
-
const component = {
|
|
1264
|
-
id,
|
|
1265
|
-
revision: 1,
|
|
1266
|
-
render,
|
|
1267
|
-
handleInput() { },
|
|
1268
|
-
invalidate() { },
|
|
1269
|
-
update() { },
|
|
1270
|
-
setExpanded() { },
|
|
1271
|
-
setOutputPad() { },
|
|
1272
|
-
setHideThinkingBlock() { },
|
|
1273
|
-
setMermaidRenderingMode() { },
|
|
1274
|
-
setImagePresentation() { },
|
|
1275
|
-
...(dispose === undefined ? {} : { dispose }),
|
|
1276
|
-
};
|
|
1277
|
-
this.#transcript.set(id, component);
|
|
1278
|
-
this.#workflowStatusAnchors.set(id, this.#view.transcript.length);
|
|
1279
|
-
this.#transcriptOrder.push(id);
|
|
1280
|
-
this.#documentLayouts.clear();
|
|
1281
|
-
this.invalidate();
|
|
1282
|
-
return id;
|
|
1283
|
-
}
|
|
1284
|
-
#renderWidgets(placement, width) {
|
|
1285
|
-
const rows = [...this.#extensionWidgets.values()]
|
|
1286
|
-
.filter(widget => widget.placement === placement)
|
|
1287
|
-
.flatMap(widget => widget.component.render(width));
|
|
1288
|
-
return placement === "aboveEditor" ? ["", ...rows] : rows;
|
|
1289
|
-
}
|
|
1290
606
|
#renderStatus(width) {
|
|
1291
607
|
return this.#customViewport ? this.#status.renderLive(width) : this.#status.render(width);
|
|
1292
608
|
}
|
|
1293
609
|
#renderFooter(width) {
|
|
1294
|
-
return (this.#
|
|
1295
|
-
}
|
|
1296
|
-
#viewWithExtensionStatuses(view) {
|
|
1297
|
-
const footer = view.status.footer;
|
|
1298
|
-
const statuses = new Map(footer?.extensionStatuses ?? []);
|
|
1299
|
-
for (const [key, text] of this.#extensionStatuses)
|
|
1300
|
-
statuses.set(key, text);
|
|
1301
|
-
return {
|
|
1302
|
-
...view,
|
|
1303
|
-
status: {
|
|
1304
|
-
...view.status,
|
|
1305
|
-
footer: {
|
|
1306
|
-
...footer,
|
|
1307
|
-
branch: footer?.branch ?? null,
|
|
1308
|
-
sessionName: footer?.sessionName ?? null,
|
|
1309
|
-
availableProviderCount: footer?.availableProviderCount ?? 1,
|
|
1310
|
-
extensionStatuses: [...statuses],
|
|
1311
|
-
},
|
|
1312
|
-
},
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
#invalidateExtensions() {
|
|
1316
|
-
this.#extensionHeader?.invalidate();
|
|
1317
|
-
this.#extensionFooter?.invalidate();
|
|
1318
|
-
for (const { component } of this.#extensionWidgets.values())
|
|
1319
|
-
component.invalidate();
|
|
610
|
+
return (this.#extensions.footer ?? this.#footer).render(width);
|
|
1320
611
|
}
|
|
1321
612
|
#setToolsExpanded(expanded) {
|
|
1322
613
|
this.#toolsExpanded = expanded;
|
|
1323
614
|
this.header.setExpanded(expanded);
|
|
1324
615
|
this.resources.setExpanded(expanded);
|
|
1325
|
-
for (const component of this.#
|
|
616
|
+
for (const component of this.#document.components())
|
|
1326
617
|
component.setExpanded(expanded);
|
|
1327
618
|
// Invariant: expansion changes what a block draws without changing its revision.
|
|
1328
|
-
this.#
|
|
1329
|
-
this.#documentLayouts.clear();
|
|
619
|
+
this.#document.invalidate("tools-expanded");
|
|
1330
620
|
this.invalidate();
|
|
1331
621
|
}
|
|
1332
622
|
}
|
|
1333
|
-
const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
|
|
1334
623
|
const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
|
|
1335
|
-
const KITTY_IMAGE_CONTROL = /\u001b_G[\s\S]*?\u001b\\/g;
|
|
1336
|
-
const ITERM_IMAGE_CONTROL = /\u001b]1337;File=[^\u0007]*(?:\u0007|\u001b\\)/g;
|
|
1337
|
-
/** Keep semantic SGR/OSC styling while excluding non-replayable image payloads. */
|
|
1338
|
-
function sanitizeExitTranscriptRow(row) {
|
|
1339
|
-
return row.replace(KITTY_IMAGE_CONTROL, "").replace(ITERM_IMAGE_CONTROL, "");
|
|
1340
|
-
}
|
|
1341
624
|
const TERMINAL_BACKGROUND = /\u001b\[(?:4[0-9]|10[0-7]|48(?:[;:][0-9;:]*)?)m/g;
|
|
1342
|
-
/** Web URLs use link blue; file targets retain A1's cyan interactive accent. */
|
|
1343
|
-
function nativeTranscriptLinkColor(text, target) {
|
|
1344
|
-
return piTheme().fg(/^file:/iu.test(target) ? "accent" : "mdLink", text);
|
|
1345
|
-
}
|
|
1346
625
|
/** Repaint a source prompt with viewport chrome while preserving text, links, and foreground roles. */
|
|
1347
626
|
function withoutTerminalBackground(text) {
|
|
1348
627
|
return text.replace(TERMINAL_BACKGROUND, "");
|
|
1349
628
|
}
|
|
1350
|
-
/** Preserve the stable metadata-grey foreground for every pinned timestamp state. */
|
|
1351
|
-
function pinnedPromptSource(block, sourceRow, width, timeFormat) {
|
|
1352
|
-
const unchanged = { row: sourceRow, copySuffixWidth: 0 };
|
|
1353
|
-
if (typeof block.payload !== "object" || block.payload === null)
|
|
1354
|
-
return unchanged;
|
|
1355
|
-
const value = block.payload.timestamp;
|
|
1356
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1357
|
-
return unchanged;
|
|
1358
|
-
const format = timeFormat ?? "8:00 PM";
|
|
1359
|
-
const timestamp = formatSubmittedPromptTime(value, format);
|
|
1360
|
-
if (timestamp === null || submittedPromptLayout(width, value, format).timestamp === null)
|
|
1361
|
-
return unchanged;
|
|
1362
|
-
const rowWidth = displayWidth(sourceRow);
|
|
1363
|
-
const timestampWidth = displayWidth(timestamp);
|
|
1364
|
-
if (rowWidth < timestampWidth)
|
|
1365
|
-
return unchanged;
|
|
1366
|
-
return {
|
|
1367
|
-
row: overlaySpan(sourceRow, rowWidth - timestampWidth, rowWidth, paintPiSubmittedPromptTimestamp(timestamp)),
|
|
1368
|
-
copySuffixWidth: timestampWidth,
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
629
|
/**
|
|
1372
630
|
* The pinned-profile screen: a followed transcript scroll above the dock. The runtime mounts one such layout,
|
|
1373
631
|
* so its single `transcript` scroll view is shared by whichever session the parts currently resolve to.
|
|
@@ -1577,20 +835,13 @@ function isPackageExtensionSource(sourceInfo) {
|
|
|
1577
835
|
const source = sourceInfo?.source ?? "";
|
|
1578
836
|
return source.startsWith("npm:") || source.startsWith("git:");
|
|
1579
837
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
return { version: match[1], command: match[2], changelogUrl: url };
|
|
1591
|
-
}
|
|
1592
|
-
/** "Share URL: <url>" with the URL made clickable; other share wordings pass through unchanged. */
|
|
1593
|
-
function linkShareUrl(message) {
|
|
1594
|
-
const match = /^Share URL: (\S+)$/.exec(message);
|
|
1595
|
-
return match === null ? message : `Share URL: ${piShellHyperlink(match[1])}`;
|
|
1596
|
-
}
|
|
838
|
+
// Performance: stable painter identities let the neutral viewport retain row-level
|
|
839
|
+
// selection variants while each callback still resolves the live Pi theme.
|
|
840
|
+
const VIEWPORT_THEME = {
|
|
841
|
+
track: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("dim", text)}`,
|
|
842
|
+
thumb: text => `${SCROLLBAR_CELL_RESET}${piTheme().fg("accent", text)}`,
|
|
843
|
+
sticky: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", withoutTerminalBackground(text))),
|
|
844
|
+
quietSticky: text => `\u001b[2m${text.replace(/\u001b\[(?:0|22)m/g, "$&\u001b[2m")}\u001b[22m`,
|
|
845
|
+
bottomControl: (text, hovered) => piTheme().bg(hovered ? "selectedBg" : "toolPendingBg", piTheme().fg("text", text)),
|
|
846
|
+
selection: (line, from, to) => backgroundSgrSpan(line, from, to, "\u001b[48;2;38;79;120m", "\u001b[49m"),
|
|
847
|
+
};
|