@timurproko/a1 0.2.2-dev.611 → 0.2.2
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/README.md +16 -8
- package/bin/pinned-pi-public.js +11 -4
- package/dist/app/session-shell/index.d.ts +1 -1
- package/dist/app/session-shell/paste-executor.js +6 -1
- package/dist/app/session-shell/prompt-chips.d.ts +8 -0
- package/dist/app/session-shell/prompt-chips.js +58 -13
- package/dist/app/session-shell/session-shell-root.d.ts +21 -3
- package/dist/app/session-shell/session-shell-root.js +110 -17
- package/dist/app/session-shell/session-shell.d.ts +1 -1
- package/dist/app/session-shell/session-shell.js +76 -31
- package/dist/app/session-shell/session-viewport-controller.d.ts +13 -0
- package/dist/app/session-shell/session-viewport-controller.js +28 -1
- package/dist/cli/version-stats.js +4 -7
- package/dist/composition/owned-ui.d.ts +7 -0
- package/dist/composition/owned-ui.js +71 -3
- package/dist/contracts/owned-ui/image-attachments.d.ts +7 -4
- package/dist/contracts/owned-ui/image-attachments.js +20 -7
- package/dist/contracts/owned-ui/index.d.ts +1 -1
- package/dist/contracts/owned-ui/index.js +1 -1
- package/dist/contracts/owned-ui/prompt-chips.d.ts +2 -2
- package/dist/contracts/owned-ui/prompt-chips.js +9 -6
- package/dist/features/owned-ui/project-trust-prompt.js +39 -3
- package/dist/features/owned-ui/release-note-state.d.ts +13 -0
- package/dist/features/owned-ui/release-note-state.js +109 -0
- package/dist/features/owned-ui/release-notes.d.ts +10 -0
- package/dist/features/owned-ui/release-notes.js +73 -0
- package/dist/features/owned-ui/resources/release-notes.json +9 -0
- package/dist/foundation/release/latest-release.d.ts +55 -0
- package/dist/foundation/release/latest-release.js +166 -0
- package/dist/foundation/release/update.js +9 -0
- package/dist/integrations/pi/components/index.d.ts +1 -1
- package/dist/integrations/pi/components/index.js +1 -1
- package/dist/integrations/pi/components/prompt-chip-presentation.d.ts +3 -0
- package/dist/integrations/pi/components/prompt-chip-presentation.js +14 -0
- package/dist/integrations/pi/components/shell-footer-status.js +30 -9
- package/dist/integrations/pi/components/shell-presenters-info.d.ts +36 -0
- package/dist/integrations/pi/components/shell-presenters-info.js +77 -1
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +0 -6
- package/dist/integrations/pi/components/shell-presenters-transcript.js +0 -19
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +13 -4
- package/dist/integrations/pi/engine/adapter.d.ts +12 -0
- package/dist/integrations/pi/engine/adapter.js +13 -0
- package/dist/integrations/pi/engine/session-runtime.d.ts +1 -0
- package/dist/integrations/pi/engine/session-runtime.js +2 -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 +1 -1
- package/dist/product-identity.js +1 -1
- package/dist/product-identity.json +1 -0
- package/dist/ui/settings/declarations.d.ts +25 -1
- package/dist/ui/settings/declarations.js +19 -1
- package/dist/ui/settings/migrations.js +14 -0
- package/docs/architecture/internal-naming.md +2 -2
- package/docs/architecture/resource-and-data-policy.md +11 -0
- package/docs/architecture/toolchain.md +1 -1
- package/docs/architecture/ui-reference-provenance.md +1 -0
- package/docs/ci-release-runbook.md +154 -70
- package/docs/local-worktree-cleanup.md +21 -4
- package/docs/manual-owned-ui-checkpoint.md +1 -0
- package/docs/openspec-archive-automation.md +1 -1
- package/docs/releases/0.2.2.md +47 -0
- package/docs/validation.md +13 -3
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { acceptsTranscriptUpdate } from "../../contracts/owned-ui/index.js";
|
|
1
|
+
import { acceptsTranscriptUpdate, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
|
|
2
2
|
import { caretCell } from "../../ui/components/line-input.js";
|
|
3
3
|
import { PromptInput, promptArrow } from "../../ui/components/prompt-input.js";
|
|
4
4
|
import { composeSubmittedPromptRows, formatSubmittedPromptTime, submittedPromptLayout, } from "../../ui/components/submitted-prompt.js";
|
|
@@ -10,8 +10,8 @@ import { createPiExtensionUiBridge } from "../../integrations/pi/components/shel
|
|
|
10
10
|
import { createPiShellEditor } from "../../integrations/pi/components/shell-editor-autocomplete.js";
|
|
11
11
|
import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
|
|
12
12
|
import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
13
|
-
import { createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellCommandMessage, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
14
|
-
import { createPiShellTranscriptComponent, isPiPromptStyleCompaction, paintPiSubmittedPromptTimestamp,
|
|
13
|
+
import { createPiShellChangelog, createPiShellCollapsedChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellCommandMessage, renderPiShellPackageUpdateNotice, renderPiShellReleaseUpdateBanner, renderPiShellReleaseUpdateNotice, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
14
|
+
import { createPiShellTranscriptComponent, isPiPromptStyleCompaction, paintPiSubmittedPromptTimestamp, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
|
|
15
15
|
import { onPiThemeChange, PINNED_PI_LAYOUT, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
|
|
16
16
|
import { piShellHyperlink, piShellTruncateToWidth, piShellVisibleWidth, } from "../../integrations/pi/components/shell-shared-facade.js";
|
|
17
17
|
import { classifyPiTuiInput, } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
@@ -62,6 +62,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
62
62
|
#lastWorkflowStatusId;
|
|
63
63
|
// Invariant: the notice is dock chrome, never transcript content; the custom viewport alone uses it.
|
|
64
64
|
#dockNotice;
|
|
65
|
+
// Invariant: bare A1 docks the release notice above the live status; closing it lasts for this session only.
|
|
66
|
+
#releaseNoticeDismissed = false;
|
|
67
|
+
#releaseCloseTarget;
|
|
65
68
|
#copyAcknowledgement;
|
|
66
69
|
#copyAcknowledgementTimer;
|
|
67
70
|
#inputSurface;
|
|
@@ -80,6 +83,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
80
83
|
#extensionStatuses = new Map();
|
|
81
84
|
#extensionWorkingMessage;
|
|
82
85
|
#extensionWorkingVisible = true;
|
|
86
|
+
#imageSubmissionsSending = 0;
|
|
83
87
|
constructor(view, cwd, handlers, startup = {}, agentDir, extensionRenderers = {
|
|
84
88
|
getMessageRenderer: () => undefined,
|
|
85
89
|
getToolDefinition: () => undefined,
|
|
@@ -88,7 +92,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
88
92
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
89
93
|
this.#promptChips = new PromptChipStore({ isolated: this.#customViewport,
|
|
90
94
|
...(handlers.pasteDiagnostics === undefined ? {} : { onEvent: handlers.pasteDiagnostics }),
|
|
91
|
-
...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation })
|
|
95
|
+
...(handlers.pastePreparation === undefined ? {} : { preparation: handlers.pastePreparation }),
|
|
96
|
+
...(handlers.promptImageLimit === undefined ? {} : { imageLimit: handlers.promptImageLimit }) });
|
|
92
97
|
this.#submittedPromptComposer = this.#customViewport
|
|
93
98
|
? {
|
|
94
99
|
layout: submittedPromptLayout,
|
|
@@ -150,18 +155,19 @@ export class OwnedUiSessionShellRoot {
|
|
|
150
155
|
if (rowIndex === 0)
|
|
151
156
|
this.#editorHyperlinks.reset();
|
|
152
157
|
const plain = stripAnsi(row);
|
|
158
|
+
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);
|
|
153
159
|
const ranges = this.#promptChips.hyperlinkRanges(plain);
|
|
154
160
|
if (ranges.length === 0 || !this.#editorHyperlinks.takeCleanup())
|
|
155
|
-
return
|
|
156
|
-
const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(
|
|
161
|
+
return dimmed;
|
|
162
|
+
const linkResetAndTail = `\u001b]8;;\u001b\\\u001b[24m${" ".repeat(Math.max(0, width - piShellVisibleWidth(dimmed)))}`;
|
|
157
163
|
// Platform: VS15 is zero-column and default-ignorable. It breaks Windows Terminal's
|
|
158
164
|
// plain-text URL detector only in the held-button paint; semantic text stays exact.
|
|
159
165
|
const paintRow = this.#viewportController.editorPointerSelecting
|
|
160
|
-
?
|
|
161
|
-
:
|
|
166
|
+
? dimmed.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//")
|
|
167
|
+
: dimmed;
|
|
162
168
|
const decorated = this.#viewportController.editorPointerSelecting
|
|
163
169
|
? 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)
|
|
164
|
-
: 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),
|
|
170
|
+
: 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);
|
|
165
171
|
// Platform: Windows Terminal can retain stale native dotted-link cells when a mutable
|
|
166
172
|
// prompt replaces a longer URL. Explicit non-link spaces overwrite that tail.
|
|
167
173
|
return `${decorated}${linkResetAndTail}`;
|
|
@@ -265,6 +271,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
265
271
|
prepareHistoryText(text) {
|
|
266
272
|
return this.#promptChips.prepareHistoryText(text);
|
|
267
273
|
}
|
|
274
|
+
omitUnsentPromptImages(text) {
|
|
275
|
+
return this.#promptChips.omitUnsentImages(text);
|
|
276
|
+
}
|
|
268
277
|
rehydrateHistoryText(text, resolveImage) {
|
|
269
278
|
return this.#promptChips.rehydrateHistoryText(text, resolveImage);
|
|
270
279
|
}
|
|
@@ -274,6 +283,12 @@ export class OwnedUiSessionShellRoot {
|
|
|
274
283
|
preparePromptSubmission(text) {
|
|
275
284
|
return this.#promptChips.prepareSubmission(text);
|
|
276
285
|
}
|
|
286
|
+
reconcilePromptImageLimitNotice(text = this.editor.getText()) {
|
|
287
|
+
if (this.#dockNotice?.errorCode !== "image-count" || !this.#promptChips.imageLimitState(text).corrected)
|
|
288
|
+
return false;
|
|
289
|
+
this.#dismissDockNotice();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
277
292
|
hasPendingPastes(text) { return this.#promptChips.hasPending(text); }
|
|
278
293
|
waitForPromptPastes(text, signal) {
|
|
279
294
|
return this.#promptChips.waitForPastes(text, signal, () => this.editor.getText());
|
|
@@ -316,7 +331,12 @@ export class OwnedUiSessionShellRoot {
|
|
|
316
331
|
|| view.diagnostics.some((diagnostic, index) => diagnostic.sequence !== this.#view.diagnostics[index]?.sequence)) {
|
|
317
332
|
this.#documentLayouts.clear();
|
|
318
333
|
}
|
|
334
|
+
const promptAccepted = this.#view.lifecycle !== "busy" && view.lifecycle === "busy";
|
|
319
335
|
this.#view = view;
|
|
336
|
+
if (promptAccepted && this.#imageSubmissionsSending > 0) {
|
|
337
|
+
this.#imageSubmissionsSending = 0;
|
|
338
|
+
this.#syncWorkingOverride();
|
|
339
|
+
}
|
|
320
340
|
this.#status.update(view);
|
|
321
341
|
this.#footer.update(this.#viewWithExtensionStatuses(view));
|
|
322
342
|
this.#queued.update(view.editor.queuedSubmissions);
|
|
@@ -384,7 +404,11 @@ export class OwnedUiSessionShellRoot {
|
|
|
384
404
|
: Math.max(1, width - 1);
|
|
385
405
|
const document = this.#renderDocumentLayout(documentWidth);
|
|
386
406
|
const steeringRows = this.#renderQueued(documentWidth);
|
|
387
|
-
const
|
|
407
|
+
const releaseBanner = this.#renderReleaseBanner(documentWidth);
|
|
408
|
+
// Rationale: the release notice rides the bottom-aligned tail directly above Working, so it
|
|
409
|
+
// never displaces transcript content and scrolls away with the live status.
|
|
410
|
+
// Invariant: when idle the banner's last row sits on the line Working would occupy.
|
|
411
|
+
const statusRows = [...releaseBanner?.rows ?? [], ...this.#renderStatus(documentWidth)];
|
|
388
412
|
const transientSignature = transientRowsSignature(steeringRows, statusRows);
|
|
389
413
|
const snapshot = this.#visibleViewportSnapshot;
|
|
390
414
|
// Invariant: paint-only feedback may cover either viewport or dock cells, so
|
|
@@ -459,6 +483,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
459
483
|
}
|
|
460
484
|
else
|
|
461
485
|
this.#dockOnlyViewportCompositions += 1;
|
|
486
|
+
this.#locateReleaseClose(frame, releaseBanner, document.rows.length + steeringRows.length + statusRows.length, statusRows.length);
|
|
462
487
|
this.#visibleViewportSnapshot = {
|
|
463
488
|
width,
|
|
464
489
|
height,
|
|
@@ -598,6 +623,32 @@ export class OwnedUiSessionShellRoot {
|
|
|
598
623
|
if (requestRender)
|
|
599
624
|
this.#componentRuntime.requestRender();
|
|
600
625
|
}
|
|
626
|
+
#renderReleaseBanner(width) {
|
|
627
|
+
if (this.#releaseNoticeDismissed)
|
|
628
|
+
return undefined;
|
|
629
|
+
const diagnostic = this.#view.diagnostics.findLast(item => item.code === "release-update");
|
|
630
|
+
const release = diagnostic === undefined ? null : parseReleaseUpdateDiagnostic(diagnostic.message);
|
|
631
|
+
if (release === null)
|
|
632
|
+
return undefined;
|
|
633
|
+
return renderPiShellReleaseUpdateBanner(release, width, this.#viewportController.tailControlHovered(this.#releaseCloseTarget));
|
|
634
|
+
}
|
|
635
|
+
#locateReleaseClose(frame, banner, tailEnd, tailLength) {
|
|
636
|
+
// Invariant: the tail is the document suffix, so the control's document row is fixed relative to its
|
|
637
|
+
// end; alignment gap rows precede it and the viewport scroll decides whether it is on screen.
|
|
638
|
+
const bannerStart = tailEnd + frame.descriptor.transientAlignmentGapRows - tailLength - frame.scrollTop + 1;
|
|
639
|
+
const rowStart = banner === undefined ? 0 : Math.max(1, bannerStart + banner.close.rowStart);
|
|
640
|
+
const rowEnd = banner === undefined ? 0 : Math.min(frame.hits.viewportHeight, bannerStart + banner.close.rowEnd);
|
|
641
|
+
this.#releaseCloseTarget = banner !== undefined && rowStart <= rowEnd
|
|
642
|
+
? { rowStart, rowEnd, columnStart: banner.close.columnStart, columnEnd: banner.close.columnEnd }
|
|
643
|
+
: undefined;
|
|
644
|
+
this.#viewportController.setTailControl(this.#releaseCloseTarget === undefined ? undefined : {
|
|
645
|
+
...this.#releaseCloseTarget,
|
|
646
|
+
activate: () => {
|
|
647
|
+
this.#releaseNoticeDismissed = true;
|
|
648
|
+
this.#releaseCloseTarget = undefined;
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
601
652
|
#renderDockNotice(width) {
|
|
602
653
|
const notice = this.#dockNotice;
|
|
603
654
|
if (notice === undefined)
|
|
@@ -734,6 +785,14 @@ export class OwnedUiSessionShellRoot {
|
|
|
734
785
|
});
|
|
735
786
|
}
|
|
736
787
|
}
|
|
788
|
+
// Invariant: the A1 release notice precedes the extension-package notice, matching pinned Pi's order.
|
|
789
|
+
// Bare A1 docks it in the viewport tail instead of the document.
|
|
790
|
+
rows.push(...diagnostics
|
|
791
|
+
.filter(diagnostic => !this.#customViewport && diagnostic.code === "release-update")
|
|
792
|
+
.flatMap(diagnostic => {
|
|
793
|
+
const release = parseReleaseUpdateDiagnostic(diagnostic.message);
|
|
794
|
+
return release === null ? [] : renderPiShellReleaseUpdateNotice(release, width);
|
|
795
|
+
}));
|
|
737
796
|
rows.push(...diagnostics
|
|
738
797
|
.filter(diagnostic => diagnostic.code === "package-updates")
|
|
739
798
|
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width)));
|
|
@@ -748,6 +807,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
748
807
|
: createPiShellChangelog(diagnostic.message).render(width)));
|
|
749
808
|
rows.push(...diagnostics
|
|
750
809
|
.filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "project-trust"
|
|
810
|
+
&& diagnostic.code !== "release-update"
|
|
751
811
|
&& diagnostic.code !== "package-updates" && diagnostic.code !== "changelog-collapsed"
|
|
752
812
|
&& diagnostic.code !== "changelog-expanded")
|
|
753
813
|
.slice(-3)
|
|
@@ -842,13 +902,17 @@ export class OwnedUiSessionShellRoot {
|
|
|
842
902
|
this.#workflowStatusMessages.set(id, message);
|
|
843
903
|
this.#lastWorkflowStatusId = id;
|
|
844
904
|
}
|
|
845
|
-
appendWorkflowMessage(message) {
|
|
905
|
+
appendWorkflowMessage(message, errorCode) {
|
|
846
906
|
if (message.kind === "status") {
|
|
847
907
|
this.appendWorkflowStatus(message.message);
|
|
848
908
|
return;
|
|
849
909
|
}
|
|
850
910
|
if (this.#customViewport && (message.kind === "warning" || message.kind === "error")) {
|
|
851
|
-
this.#dockNotice = {
|
|
911
|
+
this.#dockNotice = {
|
|
912
|
+
kind: errorCode === "image-count" ? "warning" : message.kind,
|
|
913
|
+
message: message.message,
|
|
914
|
+
...(errorCode === undefined ? {} : { errorCode }),
|
|
915
|
+
};
|
|
852
916
|
this.#invalidateChrome();
|
|
853
917
|
return;
|
|
854
918
|
}
|
|
@@ -856,10 +920,10 @@ export class OwnedUiSessionShellRoot {
|
|
|
856
920
|
const presentation = { kind: message.kind, message: message.message };
|
|
857
921
|
this.#appendAnchoredWorkflowComponent(width => renderPiShellCommandMessage(presentation, width, this.#outputPad));
|
|
858
922
|
}
|
|
859
|
-
appendWorkflowResult(result) {
|
|
923
|
+
appendWorkflowResult(result, errorCode) {
|
|
860
924
|
if (result.messages !== undefined) {
|
|
861
925
|
for (const message of result.messages)
|
|
862
|
-
this.appendWorkflowMessage(message);
|
|
926
|
+
this.appendWorkflowMessage(message, errorCode);
|
|
863
927
|
return;
|
|
864
928
|
}
|
|
865
929
|
if (result.messageKind === "silent" || (result.outcome === "cancelled" && result.messageKind === undefined))
|
|
@@ -891,7 +955,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
891
955
|
this.appendWorkflowMessage({
|
|
892
956
|
kind: result.messageKind === "warning" ? "warning" : "error",
|
|
893
957
|
message: result.message,
|
|
894
|
-
});
|
|
958
|
+
}, errorCode);
|
|
895
959
|
return;
|
|
896
960
|
}
|
|
897
961
|
if (result.outcome === "completed" && (result.command === "quit" || result.command === "compact"))
|
|
@@ -968,9 +1032,29 @@ export class OwnedUiSessionShellRoot {
|
|
|
968
1032
|
setExtensionWorking(message, visible = this.#extensionWorkingVisible) {
|
|
969
1033
|
this.#extensionWorkingMessage = message;
|
|
970
1034
|
this.#extensionWorkingVisible = visible;
|
|
971
|
-
this.#
|
|
1035
|
+
this.#syncWorkingOverride();
|
|
972
1036
|
this.invalidate();
|
|
973
1037
|
}
|
|
1038
|
+
beginImageSubmissionStatus() {
|
|
1039
|
+
this.#imageSubmissionsSending++;
|
|
1040
|
+
this.#syncWorkingOverride();
|
|
1041
|
+
this.invalidate();
|
|
1042
|
+
let active = true;
|
|
1043
|
+
return () => {
|
|
1044
|
+
if (!active)
|
|
1045
|
+
return;
|
|
1046
|
+
active = false;
|
|
1047
|
+
this.#imageSubmissionsSending = Math.max(0, this.#imageSubmissionsSending - 1);
|
|
1048
|
+
this.#syncWorkingOverride();
|
|
1049
|
+
this.invalidate();
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
#syncWorkingOverride() {
|
|
1053
|
+
const sending = this.#imageSubmissionsSending > 0;
|
|
1054
|
+
this.#status.setWorkingOverride(sending
|
|
1055
|
+
? "Sending…"
|
|
1056
|
+
: this.#extensionWorkingVisible ? this.#extensionWorkingMessage : undefined, sending);
|
|
1057
|
+
}
|
|
974
1058
|
hotkeysPresentation() {
|
|
975
1059
|
return {
|
|
976
1060
|
bindings: this.editor.keybindingConfig(),
|
|
@@ -1002,7 +1086,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
1002
1086
|
this.#extensionWidgets.clear();
|
|
1003
1087
|
this.#extensionStatuses.clear();
|
|
1004
1088
|
this.#extensionWorkingMessage = undefined;
|
|
1005
|
-
this.#
|
|
1089
|
+
this.#syncWorkingOverride();
|
|
1006
1090
|
this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
|
|
1007
1091
|
// Invariant: an extension renderer may have drawn transcript blocks that are now unrendered by it.
|
|
1008
1092
|
this.#renderedRows.clear();
|
|
@@ -1439,6 +1523,15 @@ function isPackageExtensionSource(sourceInfo) {
|
|
|
1439
1523
|
function transientRowsSignature(steeringRows, statusRows) {
|
|
1440
1524
|
return `${steeringRows.length}\u0000${steeringRows.join("\u0000")}\u0001${statusRows.length}\u0000${statusRows.join("\u0000")}`;
|
|
1441
1525
|
}
|
|
1526
|
+
/** The release, command, and optional changelog carried by the adapter's `release-update` diagnostic. */
|
|
1527
|
+
function parseReleaseUpdateDiagnostic(message) {
|
|
1528
|
+
const [instruction, changelog] = message.split("\n");
|
|
1529
|
+
const match = /^New version (\S+) is available\. Run (.+)$/.exec(instruction ?? "");
|
|
1530
|
+
if (match === null)
|
|
1531
|
+
return null;
|
|
1532
|
+
const url = changelog === undefined ? null : /^Changelog: (\S+)$/.exec(changelog)?.[1] ?? null;
|
|
1533
|
+
return { version: match[1], command: match[2], changelogUrl: url };
|
|
1534
|
+
}
|
|
1442
1535
|
/** "Share URL: <url>" with the URL made clickable; other share wordings pass through unchanged. */
|
|
1443
1536
|
function linkShareUrl(message) {
|
|
1444
1537
|
const match = /^Share URL: (\S+)$/.exec(message);
|
|
@@ -8,7 +8,7 @@ import { type PiTuiDamageDecision } from "../../integrations/pi/tui-runtime/dama
|
|
|
8
8
|
import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
9
9
|
import type { PiTuiOverlayHandle } from "../../integrations/pi/tui-runtime/contracts.js";
|
|
10
10
|
import { OwnedUiSessionShellRoot, type OwnedUiBackendPort, type OwnedUiSessionShellOptions } from "./session-shell-root.js";
|
|
11
|
-
export { OwnedUiSessionShellRoot, type OwnedUiSessionShellOptions, type OwnedUiShellDiagnosticOptions, type OwnedUiShellEngineOptions, type OwnedUiShellHistoryOptions, type OwnedUiShellPresentationOptions, type OwnedUiShellSkillsOptions, type OwnedUiShellSuggestionOptions, } from "./session-shell-root.js";
|
|
11
|
+
export { OwnedUiSessionShellRoot, type OwnedUiSessionShellOptions, type OwnedUiShellDiagnosticOptions, type OwnedUiShellEngineOptions, type OwnedUiShellHistoryOptions, type OwnedUiShellPresentationOptions, type OwnedUiShellPromptImagesOptions, type OwnedUiShellSkillsOptions, type OwnedUiShellSuggestionOptions, } from "./session-shell-root.js";
|
|
12
12
|
/** Coordinates backend, owned presentation, and Pi TUI lifecycles for one interactive session. */
|
|
13
13
|
export declare class OwnedUiSessionShell {
|
|
14
14
|
#private;
|
|
@@ -15,8 +15,8 @@ import { createPiShellEditor } from "../../integrations/pi/components/shell-edit
|
|
|
15
15
|
import { SKILLS_COMMAND_NAME, findSkillByArgument, rewriteSkillsTunnelSubmission, skillPrompt, skillsFromCommands, } from "../../integrations/pi/components/skills-command.js";
|
|
16
16
|
import { createPiQueuedInputStatus, createPiShellFooter, createPiShellHeader, createPiShellLoadedResources, createPiShellStatus, } from "../../integrations/pi/components/shell-footer-status.js";
|
|
17
17
|
import { createPiShellArmin, createPiShellAuthProviderSelector, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellExtensionSelector, createPiShellLoginDialog, createPiShellModelSelector, createPiShellModelsDialog, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellSkillsSelector, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, } from "../../integrations/pi/components/shell-selectors-dialogs.js";
|
|
18
|
-
import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
19
|
-
import { createPiShellTranscriptComponent,
|
|
18
|
+
import { createPiShellChangelog, createPiShellHotkeys, createPiShellSessionInfo, renderPiShellPackageUpdateNotice, renderPiShellStatusText, } from "../../integrations/pi/components/shell-presenters-info.js";
|
|
19
|
+
import { createPiShellTranscriptComponent, renderPiShellStartupDiagnostic, renderPiShellTranscriptBlock, } from "../../integrations/pi/components/shell-presenters-transcript.js";
|
|
20
20
|
import { onPiThemeChange, piTheme } from "../../integrations/pi/components/upstream/theme/theme.js";
|
|
21
21
|
import { DamageAwareTerminalAdapter } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
|
|
22
22
|
import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
@@ -36,7 +36,9 @@ export class OwnedUiSessionShell {
|
|
|
36
36
|
#unsubscribe;
|
|
37
37
|
#unsubscribePromptSuggestions;
|
|
38
38
|
#promptSuggestions;
|
|
39
|
+
#promptImages;
|
|
39
40
|
#skills;
|
|
41
|
+
#unsubscribePromptImages;
|
|
40
42
|
#unsubscribeSkills;
|
|
41
43
|
#installedCommandSignature = "";
|
|
42
44
|
#promptHistory = null;
|
|
@@ -47,6 +49,8 @@ export class OwnedUiSessionShell {
|
|
|
47
49
|
#dialogId;
|
|
48
50
|
#dialogSource;
|
|
49
51
|
#routeHost;
|
|
52
|
+
#startupRoute;
|
|
53
|
+
#startupRouteTimer;
|
|
50
54
|
#dialogHandle;
|
|
51
55
|
#sequence = 0;
|
|
52
56
|
#editorRevision = 0;
|
|
@@ -56,7 +60,6 @@ export class OwnedUiSessionShell {
|
|
|
56
60
|
#disposePromise;
|
|
57
61
|
#pointerReporting = false;
|
|
58
62
|
// Invariant: translated startup diagnostics create at most one dock notice per kind per shell.
|
|
59
|
-
#startupChangelogHandled = false;
|
|
60
63
|
#startupTrustHandled = false;
|
|
61
64
|
#customViewport;
|
|
62
65
|
#responseCopy;
|
|
@@ -95,6 +98,7 @@ export class OwnedUiSessionShell {
|
|
|
95
98
|
this.#suggestionModelKey = modelKey(this.backend.view());
|
|
96
99
|
this.#cwd = cwd;
|
|
97
100
|
this.#routeHost = routeHost ?? null;
|
|
101
|
+
this.#startupRoute = options.engine.startupRoute;
|
|
98
102
|
this.#customViewport = sessionLayout === "custom-viewport";
|
|
99
103
|
this.#stopped = new Promise(resolve => {
|
|
100
104
|
this.#resolveStopped = resolve;
|
|
@@ -104,7 +108,8 @@ export class OwnedUiSessionShell {
|
|
|
104
108
|
let streamPresentation;
|
|
105
109
|
let pendingClipboardWrite = Promise.resolve();
|
|
106
110
|
let promptSuggestionController = null;
|
|
107
|
-
// Invariant:
|
|
111
|
+
// Invariant: owned prompt limits and collapsed skills are bare-A1 replacements; comparison profiles keep pinned behavior.
|
|
112
|
+
this.#promptImages = this.#customViewport ? options.promptImages ?? null : null;
|
|
108
113
|
this.#skills = this.#customViewport ? options.skills ?? null : null;
|
|
109
114
|
const terminalCopy = terminal !== undefined || hasAsyncClipboardOutput();
|
|
110
115
|
this.#responseCopy = this.#customViewport ? new ResponseCopyCoordinator({
|
|
@@ -180,7 +185,11 @@ export class OwnedUiSessionShell {
|
|
|
180
185
|
onMessageCopy: () => { void this.runWorkflow({ command: "copy", argument: "" }); },
|
|
181
186
|
onFollowUp: () => { void this.queueFollowUp().catch(() => this.#reportSubmissionError()); },
|
|
182
187
|
onDequeue: () => this.restoreQueuedInput(),
|
|
183
|
-
onEditorChange:
|
|
188
|
+
onEditorChange: text => {
|
|
189
|
+
this.#editorRevision++;
|
|
190
|
+
promptSuggestionController?.abortPending();
|
|
191
|
+
this.root.reconcilePromptImageLimitNotice(text);
|
|
192
|
+
},
|
|
184
193
|
onPromptSuggestionAccepted: () => promptSuggestionController?.accept(),
|
|
185
194
|
onInputSurfaceChanged: () => {
|
|
186
195
|
promptSuggestionController?.invalidate();
|
|
@@ -217,6 +226,7 @@ export class OwnedUiSessionShell {
|
|
|
217
226
|
},
|
|
218
227
|
...(pasteDiagnostics === undefined ? {} : { pasteDiagnostics: pasteDiagnostics }),
|
|
219
228
|
...(pastePreparation === undefined ? {} : { pastePreparation }),
|
|
229
|
+
...(this.#promptImages === null ? {} : { promptImageLimit: this.#promptImages.limit }),
|
|
220
230
|
skillsPresentation: () => this.#skills?.presentation() ?? "expand",
|
|
221
231
|
}, {
|
|
222
232
|
...startup,
|
|
@@ -302,6 +312,10 @@ export class OwnedUiSessionShell {
|
|
|
302
312
|
this.#unsubscribePromptSuggestions = promptSuggestionOptions === undefined
|
|
303
313
|
? () => { }
|
|
304
314
|
: promptSuggestionOptions.onChange(enabled => this.#promptSuggestions?.setEnabled(enabled));
|
|
315
|
+
this.#unsubscribePromptImages = this.#promptImages === null ? () => { } : this.#promptImages.onChange(() => {
|
|
316
|
+
if (!this.#disposed && this.root.reconcilePromptImageLimitNotice())
|
|
317
|
+
this.runtime.requestRender();
|
|
318
|
+
});
|
|
305
319
|
// Rationale: the same listener refreshes the menu for the A1 presentation choice and for the engine's
|
|
306
320
|
// skill-command registration, both of which the owned settings manager reports through one change.
|
|
307
321
|
this.#unsubscribeSkills = this.#skills === null ? () => { } : this.#skills.onChange(() => {
|
|
@@ -586,6 +600,7 @@ export class OwnedUiSessionShell {
|
|
|
586
600
|
setImmediate(() => {
|
|
587
601
|
if (this.#disposed || !this.#customViewport)
|
|
588
602
|
return;
|
|
603
|
+
this.#presentStartupRoute();
|
|
589
604
|
this.root.warmPastePreparation();
|
|
590
605
|
this.#copyExecutor?.warm();
|
|
591
606
|
});
|
|
@@ -698,15 +713,21 @@ export class OwnedUiSessionShell {
|
|
|
698
713
|
// Compatibility: match interactive Pi: input during compaction is queued steering; the engine
|
|
699
714
|
// shows it in the pending rows and delivers it when compaction ends.
|
|
700
715
|
const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
|
|
701
|
-
this.#rememberInput(displayInput, type);
|
|
716
|
+
this.#rememberInput(this.root.omitUnsentPromptImages(displayInput).trim(), type);
|
|
702
717
|
this.root.resumeViewportFollowing();
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
718
|
+
const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
|
|
719
|
+
try {
|
|
720
|
+
return await this.#execute({
|
|
721
|
+
type,
|
|
722
|
+
correlationId: this.#correlation(type),
|
|
723
|
+
sessionId: this.backend.sessionId,
|
|
724
|
+
text: input,
|
|
725
|
+
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
726
|
+
}, displayInput);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
finishSending();
|
|
730
|
+
}
|
|
710
731
|
}
|
|
711
732
|
async clearOrExit(now = Date.now()) {
|
|
712
733
|
this.#promptSuggestions?.abortPending();
|
|
@@ -1559,8 +1580,14 @@ export class OwnedUiSessionShell {
|
|
|
1559
1580
|
attempt(() => this.#setPointerReporting(false, true));
|
|
1560
1581
|
attempt(() => this.#removeViewportPreInput());
|
|
1561
1582
|
attempt(() => this.#streamPresentation.dispose());
|
|
1583
|
+
attempt(() => {
|
|
1584
|
+
if (this.#startupRouteTimer !== undefined)
|
|
1585
|
+
clearTimeout(this.#startupRouteTimer);
|
|
1586
|
+
this.#startupRouteTimer = undefined;
|
|
1587
|
+
});
|
|
1562
1588
|
attempt(() => this.#promptSuggestions?.dispose());
|
|
1563
1589
|
attempt(() => this.#unsubscribePromptSuggestions());
|
|
1590
|
+
attempt(() => this.#unsubscribePromptImages());
|
|
1564
1591
|
attempt(() => this.#unsubscribeSkills());
|
|
1565
1592
|
attempt(() => this.#unsubscribeSettings());
|
|
1566
1593
|
let fullscreenExitText = "";
|
|
@@ -1706,25 +1733,12 @@ export class OwnedUiSessionShell {
|
|
|
1706
1733
|
}
|
|
1707
1734
|
this.root.update(view);
|
|
1708
1735
|
this.#syncDialog(view.dialog);
|
|
1709
|
-
this.#presentStartupChangelog(view);
|
|
1710
1736
|
this.#presentStartupTrustWarning(view);
|
|
1711
1737
|
this.runtime.requestRender();
|
|
1712
1738
|
for (const listener of this.#listeners)
|
|
1713
1739
|
listener(view);
|
|
1714
1740
|
return view;
|
|
1715
1741
|
}
|
|
1716
|
-
// Rationale: release details stay one explicit command away; startup needs only the same quiet,
|
|
1717
|
-
// non-transcript acknowledgement that bare A1 uses for model and thinking changes.
|
|
1718
|
-
#presentStartupChangelog(view) {
|
|
1719
|
-
if (this.#startupChangelogHandled || !this.#customViewport)
|
|
1720
|
-
return;
|
|
1721
|
-
const diagnostic = view.diagnostics.find(candidate => candidate.code === "changelog-collapsed" || candidate.code === "changelog-expanded");
|
|
1722
|
-
// Invariant: a view synchronized before the runtime starts is not the moment of arrival; start() replays it.
|
|
1723
|
-
if (diagnostic === undefined || !this.runtime.active)
|
|
1724
|
-
return;
|
|
1725
|
-
this.#startupChangelogHandled = true;
|
|
1726
|
-
this.root.appendWorkflowStatus(STARTUP_CHANGELOG_NOTICE);
|
|
1727
|
-
}
|
|
1728
1742
|
// Rationale: a cancelled or failed trust choice explains the restricted session where the
|
|
1729
1743
|
// user will next type, rather than leaving a lone warning at the top of an empty viewport.
|
|
1730
1744
|
#presentStartupTrustWarning(view) {
|
|
@@ -1736,7 +1750,22 @@ export class OwnedUiSessionShell {
|
|
|
1736
1750
|
this.#startupTrustHandled = true;
|
|
1737
1751
|
this.root.appendWorkflowMessage({ kind: "warning", message: diagnostic.message });
|
|
1738
1752
|
}
|
|
1739
|
-
#
|
|
1753
|
+
#presentStartupRoute() {
|
|
1754
|
+
const pending = this.#startupRoute;
|
|
1755
|
+
if (pending === undefined || this.#disposed || !this.runtime.active)
|
|
1756
|
+
return;
|
|
1757
|
+
if (this.runtime.hasOverlay() || !this.root.usesDefaultInputSurface()) {
|
|
1758
|
+
this.#startupRouteTimer = setTimeout(() => {
|
|
1759
|
+
this.#startupRouteTimer = undefined;
|
|
1760
|
+
this.#presentStartupRoute();
|
|
1761
|
+
}, 25);
|
|
1762
|
+
this.#startupRouteTimer.unref?.();
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
this.#startupRoute = undefined;
|
|
1766
|
+
this.#openOwnedRoute(pending.route, pending.input, pending.onClosed);
|
|
1767
|
+
}
|
|
1768
|
+
#openOwnedRoute(route, input, onClosed) {
|
|
1740
1769
|
const surface = this.#routeHost?.open(route, input) ?? null;
|
|
1741
1770
|
if (surface === null)
|
|
1742
1771
|
return { outcome: "failed", diagnostic: `route is unavailable: ${route}` };
|
|
@@ -1768,7 +1797,12 @@ export class OwnedUiSessionShell {
|
|
|
1768
1797
|
return { consume: true };
|
|
1769
1798
|
});
|
|
1770
1799
|
let removeSurfacePreInput = () => { };
|
|
1800
|
+
let rendered = false;
|
|
1801
|
+
let closed = false;
|
|
1771
1802
|
const closeSurface = () => {
|
|
1803
|
+
if (closed)
|
|
1804
|
+
return;
|
|
1805
|
+
closed = true;
|
|
1772
1806
|
removeSurfacePreInput();
|
|
1773
1807
|
removeInterruptWatch();
|
|
1774
1808
|
this.#setPointerReporting(false);
|
|
@@ -1776,6 +1810,12 @@ export class OwnedUiSessionShell {
|
|
|
1776
1810
|
this.#dialogHandle = undefined;
|
|
1777
1811
|
this.#dialogId = undefined;
|
|
1778
1812
|
this.#dialogSource = undefined;
|
|
1813
|
+
if (rendered) {
|
|
1814
|
+
try {
|
|
1815
|
+
void onClosed?.()?.catch(() => undefined);
|
|
1816
|
+
}
|
|
1817
|
+
catch { /* Rationale: a failed acknowledgement stays pending. */ }
|
|
1818
|
+
}
|
|
1779
1819
|
};
|
|
1780
1820
|
// Compatibility: fullscreen Pi owns a fallback text-selection layer before focused overlay
|
|
1781
1821
|
// components see pointer input. Route every mouse report to the owned screen
|
|
@@ -1795,7 +1835,11 @@ export class OwnedUiSessionShell {
|
|
|
1795
1835
|
});
|
|
1796
1836
|
const rows = () => Math.max(1, this.runtime.viewport().rows);
|
|
1797
1837
|
const component = {
|
|
1798
|
-
render: (width) =>
|
|
1838
|
+
render: (width) => {
|
|
1839
|
+
const frame = [...surface.render(Math.max(1, width), rows())];
|
|
1840
|
+
rendered = true;
|
|
1841
|
+
return frame;
|
|
1842
|
+
},
|
|
1799
1843
|
handleInput: (data) => {
|
|
1800
1844
|
const { events, rest } = parseMouseInput(data);
|
|
1801
1845
|
for (const event of events)
|
|
@@ -2085,14 +2129,16 @@ export class OwnedUiSessionShell {
|
|
|
2085
2129
|
if (revision === this.#editorRevision && this.root.editor.getText().length === 0)
|
|
2086
2130
|
this.root.editor.setText(draft);
|
|
2087
2131
|
const message = error instanceof ImageAttachmentError ? error.message : "Submission rejected. Check the prompt and attachments.";
|
|
2088
|
-
this.#reportSubmissionError(error,
|
|
2132
|
+
this.#reportSubmissionError(error, error instanceof ImageAttachmentError && error.code === "image-count"
|
|
2133
|
+
? message
|
|
2134
|
+
: `${message} Press Up to recover the draft.`);
|
|
2089
2135
|
return rejected(message);
|
|
2090
2136
|
}
|
|
2091
2137
|
#reportSubmissionError(error, message) {
|
|
2092
2138
|
// Security: arbitrary provider/extension error messages can contain the entire request.
|
|
2093
2139
|
try {
|
|
2094
2140
|
this.root.appendWorkflowResult({ command: "debug", outcome: "failed", message: message
|
|
2095
|
-
?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") });
|
|
2141
|
+
?? (error instanceof ImageAttachmentError ? error.message : "Submission failed. Check the prompt and try again.") }, error instanceof ImageAttachmentError ? error.code : undefined);
|
|
2096
2142
|
this.runtime.requestRender();
|
|
2097
2143
|
}
|
|
2098
2144
|
catch { /* Security: error presentation cannot create another rejected submission callback. */ }
|
|
@@ -2158,7 +2204,6 @@ function workflowAdapterResult(result) {
|
|
|
2158
2204
|
const INTERRUPT = "\u0003";
|
|
2159
2205
|
const INTERRUPT_CHORD_MS = 1_500;
|
|
2160
2206
|
const RELOAD_SURFACE_MIN_VISIBLE_MS = 400;
|
|
2161
|
-
const STARTUP_CHANGELOG_NOTICE = "Run /changelog to view the full release notes.";
|
|
2162
2207
|
// Rationale: the outro is not configurable; the switch only decides whether this plan plays.
|
|
2163
2208
|
const QUIT_OUTRO_EFFECT = "fall";
|
|
2164
2209
|
const QUIT_OUTRO_DURATION_MS = 800;
|
|
@@ -15,6 +15,13 @@ export interface SessionViewportControllerOptions {
|
|
|
15
15
|
readonly pasteDiagnostics?: (event: PasteEvent) => void;
|
|
16
16
|
readonly nextPasteDiagnosticRequest?: () => number;
|
|
17
17
|
}
|
|
18
|
+
/** One-based, inclusive terminal cells of a clickable viewport-tail control. */
|
|
19
|
+
export interface TailControlRegion {
|
|
20
|
+
readonly rowStart: number;
|
|
21
|
+
readonly rowEnd: number;
|
|
22
|
+
readonly columnStart: number;
|
|
23
|
+
readonly columnEnd: number;
|
|
24
|
+
}
|
|
18
25
|
export interface SessionViewportInputResult {
|
|
19
26
|
readonly data: string;
|
|
20
27
|
readonly consumed: boolean;
|
|
@@ -45,6 +52,12 @@ export declare class SessionViewportController {
|
|
|
45
52
|
readonly rowStart: number;
|
|
46
53
|
readonly rowEnd: number;
|
|
47
54
|
} | undefined): void;
|
|
55
|
+
/** The visible close control of a viewport-tail notice, or undefined when none is on screen. */
|
|
56
|
+
setTailControl(control: (TailControlRegion & {
|
|
57
|
+
readonly activate: () => void;
|
|
58
|
+
}) | undefined): void;
|
|
59
|
+
/** Whether the pointer currently rests on the tail control; drives its hover paint. */
|
|
60
|
+
tailControlHovered(target: TailControlRegion | undefined): boolean;
|
|
48
61
|
/** Replacement input changed before its new bounds have been painted. */
|
|
49
62
|
invalidateInputSurface(): void;
|
|
50
63
|
setInputSurfaceFrame(surface: PiTuiPointerSurface | undefined): void;
|
|
@@ -3,6 +3,9 @@ import { TranscriptViewport } from "../../ui/components/transcript-viewport.js";
|
|
|
3
3
|
import { readVisibleHyperlinks } from "../../ui/components/visible-hyperlinks.js";
|
|
4
4
|
import { routeMouseInput } from "../../ui/components/mouse.js";
|
|
5
5
|
import { scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows } from "../../ui/components/scrollbar.js";
|
|
6
|
+
function withinTailControl(region, column, row) {
|
|
7
|
+
return row >= region.rowStart && row <= region.rowEnd && column >= region.columnStart && column <= region.columnEnd;
|
|
8
|
+
}
|
|
6
9
|
/**
|
|
7
10
|
* Owns the stateful interaction policy of the bare-A1 session viewport. The
|
|
8
11
|
* shell root supplies semantic rows and dock geometry; this controller owns
|
|
@@ -38,6 +41,8 @@ export class SessionViewportController {
|
|
|
38
41
|
#pendingEditorClick;
|
|
39
42
|
// Invariant: pointer routing uses the most recently composed editor rows.
|
|
40
43
|
#editorPointerFrame;
|
|
44
|
+
// Invariant: one clickable region in the viewport tail, located by the most recent composition.
|
|
45
|
+
#tailControl;
|
|
41
46
|
#selectionAutoScrollTimer;
|
|
42
47
|
#selectionAutoScrollPointer;
|
|
43
48
|
#pointerPosition;
|
|
@@ -89,6 +94,15 @@ export class SessionViewportController {
|
|
|
89
94
|
setEditorPointerFrame(frame) {
|
|
90
95
|
this.#editorPointerFrame = frame;
|
|
91
96
|
}
|
|
97
|
+
/** The visible close control of a viewport-tail notice, or undefined when none is on screen. */
|
|
98
|
+
setTailControl(control) {
|
|
99
|
+
this.#tailControl = control;
|
|
100
|
+
}
|
|
101
|
+
/** Whether the pointer currently rests on the tail control; drives its hover paint. */
|
|
102
|
+
tailControlHovered(target) {
|
|
103
|
+
const pointer = this.#pointerPosition;
|
|
104
|
+
return target !== undefined && pointer !== undefined && withinTailControl(target, pointer.column, pointer.row);
|
|
105
|
+
}
|
|
92
106
|
/** Replacement input changed before its new bounds have been painted. */
|
|
93
107
|
invalidateInputSurface() {
|
|
94
108
|
this.#inputSurfacePending = true;
|
|
@@ -360,6 +374,9 @@ export class SessionViewportController {
|
|
|
360
374
|
const overSticky = overModal === undefined && hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
|
|
361
375
|
const overBottom = overModal === undefined && hits.bottom !== null && event.row === hits.bottom.row
|
|
362
376
|
&& event.column >= hits.bottom.columnStart && event.column <= hits.bottom.columnEnd;
|
|
377
|
+
const tailControl = this.#tailControl;
|
|
378
|
+
const overTailControl = overModal === undefined && tailControl !== undefined
|
|
379
|
+
&& withinTailControl(tailControl, event.column, event.row);
|
|
363
380
|
const previousPointer = this.#pointerPosition;
|
|
364
381
|
const wasOverBottom = hits.bottom !== null && previousPointer !== undefined
|
|
365
382
|
&& previousPointer.row === hits.bottom.row
|
|
@@ -368,6 +385,9 @@ export class SessionViewportController {
|
|
|
368
385
|
// Keep this location while the control is hidden; composition resolves its next hit region.
|
|
369
386
|
this.#pointerPosition = { column: event.column, row: event.row };
|
|
370
387
|
repaint ||= wasOverBottom !== overBottom;
|
|
388
|
+
const wasOverTailControl = tailControl !== undefined && previousPointer !== undefined
|
|
389
|
+
&& withinTailControl(tailControl, previousPointer.column, previousPointer.row);
|
|
390
|
+
repaint ||= wasOverTailControl !== overTailControl;
|
|
371
391
|
if (!this.#viewport.selectionActive && !this.#editorPointerSelecting) {
|
|
372
392
|
const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
|
|
373
393
|
if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
|
|
@@ -408,7 +428,7 @@ export class SessionViewportController {
|
|
|
408
428
|
activity = true;
|
|
409
429
|
return true;
|
|
410
430
|
}
|
|
411
|
-
return overRail || overSticky || overBottom;
|
|
431
|
+
return overRail || overSticky || overBottom || overTailControl;
|
|
412
432
|
}
|
|
413
433
|
if (event.kind === "wheel-up" || event.kind === "wheel-down") {
|
|
414
434
|
if (!allowWheel || event.row < 1 || event.row > hits.viewportHeight)
|
|
@@ -442,6 +462,13 @@ export class SessionViewportController {
|
|
|
442
462
|
this.#stopSelectionAutoScroll();
|
|
443
463
|
if (this.#viewport.clearSelection())
|
|
444
464
|
repaint = true;
|
|
465
|
+
if (overTailControl) {
|
|
466
|
+
this.#tailControl = undefined;
|
|
467
|
+
tailControl.activate();
|
|
468
|
+
this.#tailPointerSuppressed = true;
|
|
469
|
+
repaint = true;
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
445
472
|
if (overBottom) {
|
|
446
473
|
this.#viewport.scrollToEnd(now);
|
|
447
474
|
this.#tailPointerSuppressed = true;
|