@timurproko/a1 0.1.8-dev.226 → 0.1.8-dev.259
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/guardian.js +4 -2
- package/bin/supervisor.js +1 -1
- package/bin/ui.js +44 -36
- package/bin/warmup.js +18 -9
- package/dist/cli/dispatch.d.ts +9 -1
- package/dist/cli/dispatch.js +73 -14
- package/dist/cli/package-messages.d.ts +26 -0
- package/dist/cli/package-messages.js +71 -0
- package/dist/cli/packages.d.ts +2 -1
- package/dist/cli/packages.js +12 -19
- package/dist/composition/owned-ui.d.ts +5 -2
- package/dist/composition/owned-ui.js +3 -1
- package/dist/contracts/agent-engine/package-ports.d.ts +6 -0
- package/dist/features/launch/intent.d.ts +5 -3
- package/dist/features/launch/intent.js +7 -2
- package/dist/features/owned-ui/index.d.ts +1 -0
- package/dist/features/owned-ui/index.js +1 -0
- package/dist/features/owned-ui/session-fork-prompt.d.ts +8 -0
- package/dist/features/owned-ui/session-fork-prompt.js +34 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -0
- package/dist/foundation/launch-guardian/main.js +8 -3
- package/dist/foundation/lifecycle/index.d.ts +2 -0
- package/dist/foundation/lifecycle/index.js +2 -0
- package/dist/foundation/lifecycle/paths.d.ts +2 -0
- package/dist/foundation/lifecycle/paths.js +9 -2
- package/dist/foundation/lifecycle/session-selection.d.ts +9 -0
- package/dist/foundation/lifecycle/session-selection.js +39 -0
- package/dist/foundation/lifecycle/supervisor-startup.d.ts +30 -0
- package/dist/foundation/lifecycle/supervisor-startup.js +106 -0
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +5 -0
- package/dist/foundation/process-containment/darwin-process-inspector.js +7 -0
- package/dist/foundation/process-containment/index.d.ts +1 -0
- package/dist/foundation/process-containment/index.js +1 -0
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/windows-process-inspector.js +4 -2
- package/dist/foundation/release/bootstrap.d.ts +10 -3
- package/dist/foundation/release/bootstrap.js +58 -25
- package/dist/foundation/release/dependency-layer.js +11 -10
- package/dist/foundation/release/immutable-platform.d.ts +6 -0
- package/dist/foundation/release/immutable-platform.js +13 -0
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/release-store.d.ts +1 -1
- package/dist/foundation/release/release-store.js +4 -2
- package/dist/foundation/release/restart-certification.d.ts +67 -0
- package/dist/foundation/release/restart-certification.js +211 -0
- package/dist/foundation/release/update.js +4 -4
- package/dist/foundation/release/warmup.js +1 -1
- package/dist/foundation/startup/startup-runtime.d.ts +2 -2
- package/dist/foundation/startup/startup-runtime.js +1 -1
- package/dist/foundation/supervision/main.d.ts +1 -1
- package/dist/foundation/supervision/main.js +60 -30
- package/dist/foundation/supervision/server.js +14 -1
- package/dist/integrations/pi/components/shell-footer-status.js +28 -12
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +4 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +6 -0
- package/dist/integrations/pi/engine/adapter.js +354 -53
- package/dist/integrations/pi/engine/index.d.ts +1 -0
- package/dist/integrations/pi/engine/index.js +1 -0
- package/dist/integrations/pi/engine/package-integration.js +31 -25
- package/dist/integrations/pi/engine/runtime-integration.d.ts +3 -0
- package/dist/integrations/pi/engine/runtime-integration.js +11 -4
- package/dist/integrations/pi/engine/session-selection.d.ts +28 -0
- package/dist/integrations/pi/engine/session-selection.js +90 -0
- package/dist/integrations/pi/engine/workflows.d.ts +18 -1
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +5 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +76 -24
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +46 -14
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +47 -18
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +166 -26
- package/dist/native/darwin-arm64/manifest.json +4 -4
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/text-selection.js +8 -35
- package/dist/ui/components/transcript-viewport.d.ts +8 -2
- package/dist/ui/components/transcript-viewport.js +8 -6
- package/dist/ui/components/visible-hyperlinks.d.ts +19 -0
- package/dist/ui/components/visible-hyperlinks.js +64 -0
- package/docs/architecture/process-guardian-provenance.md +3 -1
- package/docs/architecture/ui-reference-provenance.md +2 -0
- package/docs/ci-release-runbook.md +1 -1
- package/docs/features/launch-profiles.md +17 -1
- package/docs/manual-ghost-link-baseline.md +89 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PRODUCT_TEXT } from "../../../product-identity.js";
|
|
2
|
-
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../../../ui/components/index.js";
|
|
2
|
+
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput, readVisibleHyperlinks } from "../../../ui/components/index.js";
|
|
3
3
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../engine/index.js";
|
|
4
4
|
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../components/index.js";
|
|
5
5
|
import { DamageAwareTerminalAdapter, PiTuiRuntimeAdapter, classifyPiTuiInput, } from "../tui-runtime/index.js";
|
|
@@ -63,6 +63,7 @@ export class OwnedUiSessionShell {
|
|
|
63
63
|
getColumns: () => runtime?.viewport().columns ?? options.terminal?.columns ?? 80,
|
|
64
64
|
getRows: () => runtime?.viewport().rows ?? options.terminal?.rows ?? 24,
|
|
65
65
|
requestRender: force => runtime?.requestRender(force),
|
|
66
|
+
requestHyperlinkCleanup: () => damageTerminal?.requestHyperlinkCleanup(),
|
|
66
67
|
onViewportFrame: frame => damageTerminal?.arm(frame.descriptor, {
|
|
67
68
|
overlayActive: runtime?.hasOverlay() ?? false,
|
|
68
69
|
selectionActive: this.root.hasActiveSelection(),
|
|
@@ -129,7 +130,9 @@ export class OwnedUiSessionShell {
|
|
|
129
130
|
decorateTerminal: (terminal) => {
|
|
130
131
|
damageTerminal = new DamageAwareTerminalAdapter(terminal, {
|
|
131
132
|
regionalScroll: process.env.TERM !== "dumb",
|
|
133
|
+
inspectHyperlinks: readVisibleHyperlinks,
|
|
132
134
|
onResize: () => streamPresentation?.noteImmediatePresentation(),
|
|
135
|
+
onHyperlinkCleanupRequired: () => runtime?.requestRender(true),
|
|
133
136
|
});
|
|
134
137
|
return damageTerminal;
|
|
135
138
|
},
|
|
@@ -294,6 +297,10 @@ export class OwnedUiSessionShell {
|
|
|
294
297
|
startLogin: request => this.#startWorkflowLogin(request),
|
|
295
298
|
prompt: request => this.#requestWorkflowInput(request),
|
|
296
299
|
notify: event => this.#notifyWorkflowLogin(event),
|
|
300
|
+
publish: message => {
|
|
301
|
+
this.root.appendWorkflowMessage(message);
|
|
302
|
+
this.runtime.requestRender();
|
|
303
|
+
},
|
|
297
304
|
finishLogin: () => this.#finishWorkflowLogin(),
|
|
298
305
|
});
|
|
299
306
|
this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
|
|
@@ -541,7 +548,7 @@ export class OwnedUiSessionShell {
|
|
|
541
548
|
this.#dialogHandle = handle;
|
|
542
549
|
return handle;
|
|
543
550
|
}
|
|
544
|
-
showModelSelector() {
|
|
551
|
+
showModelSelector(initialSearchInput) {
|
|
545
552
|
const context = this.backend.pinnedModelSelectorContext();
|
|
546
553
|
const close = () => {
|
|
547
554
|
this.root.setInputSurface(null);
|
|
@@ -554,6 +561,7 @@ export class OwnedUiSessionShell {
|
|
|
554
561
|
getRows: () => this.runtime.viewport().rows,
|
|
555
562
|
requestRender: () => this.runtime.requestRender(),
|
|
556
563
|
},
|
|
564
|
+
...(initialSearchInput === undefined ? {} : { initialSearchInput }),
|
|
557
565
|
onSelect: model => {
|
|
558
566
|
close();
|
|
559
567
|
void this.runWorkflow({ command: "model", argument: "", selection: modelReference(model) });
|
|
@@ -566,7 +574,7 @@ export class OwnedUiSessionShell {
|
|
|
566
574
|
showForkSelector() {
|
|
567
575
|
const options = this.backend.pinnedForkOptions();
|
|
568
576
|
if (options.length === 0) {
|
|
569
|
-
this.root.appendWorkflowResult({ command: "fork", outcome: "
|
|
577
|
+
this.root.appendWorkflowResult({ command: "fork", outcome: "completed", message: "No messages to fork from", messageKind: "status" });
|
|
570
578
|
this.runtime.requestRender();
|
|
571
579
|
return;
|
|
572
580
|
}
|
|
@@ -582,9 +590,21 @@ export class OwnedUiSessionShell {
|
|
|
582
590
|
this.runtime.requestRender();
|
|
583
591
|
}
|
|
584
592
|
async showLogoutSelector() {
|
|
585
|
-
|
|
593
|
+
let options;
|
|
594
|
+
try {
|
|
595
|
+
options = await this.backend.pinnedLogoutOptions();
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
this.root.appendWorkflowResult({
|
|
599
|
+
command: "logout",
|
|
600
|
+
outcome: "failed",
|
|
601
|
+
message: `Could not read stored credentials: ${error instanceof Error ? error.message : String(error)}`,
|
|
602
|
+
});
|
|
603
|
+
this.runtime.requestRender();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
586
606
|
if (options.length === 0) {
|
|
587
|
-
this.root.appendWorkflowStatus("No
|
|
607
|
+
this.root.appendWorkflowStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
|
|
588
608
|
this.runtime.requestRender();
|
|
589
609
|
return;
|
|
590
610
|
}
|
|
@@ -805,10 +825,6 @@ export class OwnedUiSessionShell {
|
|
|
805
825
|
await this.showLogoutSelector();
|
|
806
826
|
return { outcome: "completed", diagnostic: null };
|
|
807
827
|
}
|
|
808
|
-
if (request.command === "import" && request.confirmed === undefined && request.argument.trim().length > 0) {
|
|
809
|
-
const confirmed = await this.#extensionBridge.context.confirm("Import session", `Replace current session with ${request.argument.trim()}?`);
|
|
810
|
-
return this.runWorkflow({ ...request, confirmed: confirmed === true });
|
|
811
|
-
}
|
|
812
828
|
if (request.command === "tree" && request.selection === undefined && request.confirmed === undefined && request.argument.trim().length === 0) {
|
|
813
829
|
this.showTreeSelector();
|
|
814
830
|
return { outcome: "completed", diagnostic: null };
|
|
@@ -817,22 +833,21 @@ export class OwnedUiSessionShell {
|
|
|
817
833
|
this.root.resetExtensionUi();
|
|
818
834
|
this.root.resetWorkflowPresentation();
|
|
819
835
|
}
|
|
820
|
-
const
|
|
836
|
+
const shareSurface = request.command === "share"
|
|
821
837
|
? createPiShellOperationLoader({
|
|
822
838
|
getColumns: () => this.runtime.viewport().columns,
|
|
823
839
|
getRows: () => this.runtime.viewport().rows,
|
|
824
840
|
requestRender: () => this.runtime.requestRender(),
|
|
825
841
|
}, "Creating gist...")
|
|
826
|
-
:
|
|
827
|
-
|
|
828
|
-
: undefined;
|
|
842
|
+
: undefined;
|
|
843
|
+
const operationSurface = shareSurface ?? (request.command === "reload" ? createPiShellReloadBox() : undefined);
|
|
829
844
|
if (operationSurface) {
|
|
830
845
|
this.root.setInputSurface(operationSurface);
|
|
831
846
|
this.runtime.requestRender();
|
|
832
847
|
}
|
|
833
848
|
let result;
|
|
834
849
|
try {
|
|
835
|
-
result = await this.backend.executeWorkflow(request);
|
|
850
|
+
result = await this.backend.executeWorkflow(shareSurface === undefined ? request : { ...request, signal: shareSurface.signal });
|
|
836
851
|
}
|
|
837
852
|
finally {
|
|
838
853
|
if (operationSurface) {
|
|
@@ -840,10 +855,27 @@ export class OwnedUiSessionShell {
|
|
|
840
855
|
this.runtime.requestRender();
|
|
841
856
|
}
|
|
842
857
|
}
|
|
858
|
+
if (result.outcome === "requires-selection" && request.command === "model") {
|
|
859
|
+
if (result.messages !== undefined) {
|
|
860
|
+
for (const message of result.messages)
|
|
861
|
+
this.root.appendWorkflowMessage(message);
|
|
862
|
+
}
|
|
863
|
+
this.showModelSelector((result.detail ?? request.argument.trim()) || undefined);
|
|
864
|
+
return { outcome: "completed", diagnostic: null };
|
|
865
|
+
}
|
|
843
866
|
if (result.outcome === "requires-confirmation" && request.command === "resume") {
|
|
844
867
|
const confirmed = await this.#extensionBridge.context.confirm("Session cwd not found", result.message);
|
|
845
868
|
return this.runWorkflow({ ...request, confirmed: confirmed === true });
|
|
846
869
|
}
|
|
870
|
+
if (result.outcome === "requires-confirmation" && request.command === "import") {
|
|
871
|
+
const recoveringCwd = result.detail !== undefined;
|
|
872
|
+
const confirmed = await this.#extensionBridge.context.confirm(recoveringCwd ? "Session cwd not found" : "Import session", result.message);
|
|
873
|
+
return this.runWorkflow({
|
|
874
|
+
...request,
|
|
875
|
+
confirmed: confirmed === true,
|
|
876
|
+
...(confirmed === true && result.detail ? { cwdOverride: result.detail } : {}),
|
|
877
|
+
});
|
|
878
|
+
}
|
|
847
879
|
if (result.outcome === "requires-selection" || result.outcome === "requires-confirmation") {
|
|
848
880
|
this.root.appendWorkflowResult({ command: request.command, outcome: "failed", message: `Owned controller missing for ${request.command}` });
|
|
849
881
|
this.runtime.requestRender();
|
|
@@ -5,6 +5,7 @@ export interface SessionViewportControllerOptions {
|
|
|
5
5
|
readonly enabled: boolean;
|
|
6
6
|
readonly editor: PiShellEditorPort;
|
|
7
7
|
readonly requestRender: (force?: boolean) => void;
|
|
8
|
+
readonly requestHyperlinkCleanup?: () => void;
|
|
8
9
|
/** Whether mutable editor URL chips need an immediate forced repaint on deletion. */
|
|
9
10
|
readonly hasEditorLinks?: () => boolean;
|
|
10
11
|
}
|
|
@@ -32,7 +33,7 @@ export declare class SessionViewportController {
|
|
|
32
33
|
readonly rowStart: number;
|
|
33
34
|
readonly rowEnd: number;
|
|
34
35
|
} | undefined): void;
|
|
35
|
-
compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
|
|
36
|
+
compose(input: Omit<TranscriptViewportFrameInput, "pointerPosition">): TranscriptViewportFrame;
|
|
36
37
|
composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
|
|
37
38
|
/** Returns true when document wrapping may have changed. */
|
|
38
39
|
setConfig(config: OwnedUiViewportSettings): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TranscriptViewport,
|
|
1
|
+
import { TranscriptViewport, readVisibleHyperlinks, routeMouseInput, scrollForTrackPage, scrollForThumbRow, scrollbarSelectionRows, scrollbarWheelRows, } from "../../../ui/components/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Owns the stateful interaction policy of the bare-A1 session viewport. The
|
|
4
4
|
* shell root supplies semantic rows and dock geometry; this controller owns
|
|
@@ -9,6 +9,7 @@ export class SessionViewportController {
|
|
|
9
9
|
#editor;
|
|
10
10
|
#requestRender;
|
|
11
11
|
#hasEditorLinks;
|
|
12
|
+
#requestHyperlinkCleanup;
|
|
12
13
|
#viewport = new TranscriptViewport();
|
|
13
14
|
#config = {
|
|
14
15
|
scrollbarAppearance: "auto",
|
|
@@ -18,6 +19,8 @@ export class SessionViewportController {
|
|
|
18
19
|
#dragGrabOffset = null;
|
|
19
20
|
// Invariant: a left-button sequence begun in dock chrome remains owned by the dock.
|
|
20
21
|
#dockPointerSuppressed = false;
|
|
22
|
+
// Invariant: a left-button sequence begun in non-selectable transcript tail chrome is held there.
|
|
23
|
+
#tailPointerSuppressed = false;
|
|
21
24
|
// Invariant: editor selection remains true only while its left button is held.
|
|
22
25
|
#editorPointerSelecting = false;
|
|
23
26
|
// Invariant: pointer routing uses the most recently composed editor rows.
|
|
@@ -37,6 +40,7 @@ export class SessionViewportController {
|
|
|
37
40
|
options.requestRender(force);
|
|
38
41
|
};
|
|
39
42
|
this.#hasEditorLinks = options.hasEditorLinks ?? (() => false);
|
|
43
|
+
this.#requestHyperlinkCleanup = options.requestHyperlinkCleanup ?? (() => { });
|
|
40
44
|
}
|
|
41
45
|
get config() {
|
|
42
46
|
return this.#config;
|
|
@@ -65,7 +69,10 @@ export class SessionViewportController {
|
|
|
65
69
|
compose(input) {
|
|
66
70
|
this.#viewport.setConfig(this.#config);
|
|
67
71
|
const selectionRevision = this.#viewport.selectionRevision;
|
|
68
|
-
const frame = this.#viewport.compose(
|
|
72
|
+
const frame = this.#viewport.compose({
|
|
73
|
+
...input,
|
|
74
|
+
...(this.#pointerPosition === undefined ? {} : { pointerPosition: this.#pointerPosition }),
|
|
75
|
+
});
|
|
69
76
|
// Concurrency: if selection changed during composition, this frame truthfully carries
|
|
70
77
|
// the older revision and exactly one ordinary follow-up render publishes the latest.
|
|
71
78
|
if (this.#viewport.selectionRevision !== selectionRevision
|
|
@@ -78,6 +85,7 @@ export class SessionViewportController {
|
|
|
78
85
|
if (this.#hoveredHyperlinkKey !== undefined && next !== this.#hoveredHyperlinkKey) {
|
|
79
86
|
// Platform: Windows Terminal can leave its solid native-hover underline behind
|
|
80
87
|
// when a following viewport moves the link away from a stationary pointer.
|
|
88
|
+
this.#requestHyperlinkCleanup();
|
|
81
89
|
this.#requestRender(true);
|
|
82
90
|
}
|
|
83
91
|
this.#hoveredHyperlinkKey = next;
|
|
@@ -126,6 +134,7 @@ export class SessionViewportController {
|
|
|
126
134
|
this.#clearActivityTimer();
|
|
127
135
|
this.#dragGrabOffset = null;
|
|
128
136
|
this.#dockPointerSuppressed = false;
|
|
137
|
+
this.#tailPointerSuppressed = false;
|
|
129
138
|
this.#editorPointerSelecting = false;
|
|
130
139
|
this.#editorPointerFrame = undefined;
|
|
131
140
|
this.#pointerPosition = undefined;
|
|
@@ -146,8 +155,10 @@ export class SessionViewportController {
|
|
|
146
155
|
return { data: "", consumed: true };
|
|
147
156
|
}
|
|
148
157
|
// Platform: URL chip deletion must overwrite terminal link cells in the same frame.
|
|
149
|
-
if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks())
|
|
158
|
+
if (EDITOR_LINK_DELETION_INPUTS.has(data) && this.#hasEditorLinks()) {
|
|
159
|
+
this.#requestHyperlinkCleanup();
|
|
150
160
|
this.#requestRender(true);
|
|
161
|
+
}
|
|
151
162
|
if (this.#viewport.frame === null)
|
|
152
163
|
return { data, consumed: false };
|
|
153
164
|
if (data === "\u0003") {
|
|
@@ -205,20 +216,27 @@ export class SessionViewportController {
|
|
|
205
216
|
const overSticky = hits.sticky !== null && event.row === hits.sticky.row && event.column <= hits.sticky.width;
|
|
206
217
|
const overBottom = hits.bottom !== null && event.row === hits.bottom.row
|
|
207
218
|
&& event.column >= hits.bottom.columnStart && event.column <= hits.bottom.columnEnd;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
const previousPointer = this.#pointerPosition;
|
|
220
|
+
const wasOverBottom = hits.bottom !== null && previousPointer !== undefined
|
|
221
|
+
&& previousPointer.row === hits.bottom.row
|
|
222
|
+
&& previousPointer.column >= hits.bottom.columnStart && previousPointer.column <= hits.bottom.columnEnd;
|
|
223
|
+
// Invariant: every report locates the pointer, independently of which surface owns the action.
|
|
224
|
+
// Keep this location while the control is hidden; composition resolves its next hit region.
|
|
225
|
+
this.#pointerPosition = { column: event.column, row: event.row };
|
|
226
|
+
repaint ||= wasOverBottom !== overBottom;
|
|
227
|
+
if (!this.#viewport.selectionActive && !this.#editorPointerSelecting) {
|
|
228
|
+
const nextHyperlink = this.#hyperlinkKeyAt(frame, event.column, event.row);
|
|
229
|
+
if (this.#hoveredHyperlinkKey !== undefined && nextHyperlink !== this.#hoveredHyperlinkKey) {
|
|
230
|
+
// Platform: overwrite Windows Terminal's cached native-hover underline exactly once.
|
|
231
|
+
this.#requestHyperlinkCleanup();
|
|
232
|
+
forceRepaint = true;
|
|
233
|
+
repaint = true;
|
|
218
234
|
}
|
|
235
|
+
this.#hoveredHyperlinkKey = nextHyperlink;
|
|
236
|
+
}
|
|
237
|
+
if (event.kind === "motion") {
|
|
219
238
|
this.#viewport.setRailHovered(overRail);
|
|
220
239
|
this.#viewport.setStickyHovered(overSticky);
|
|
221
|
-
this.#viewport.setBottomHovered(overBottom);
|
|
222
240
|
repaint = true;
|
|
223
241
|
if (this.#editor.ownsPointer() && this.#editorPointerFrame !== undefined) {
|
|
224
242
|
this.#editor.handlePointer({
|
|
@@ -229,7 +247,7 @@ export class SessionViewportController {
|
|
|
229
247
|
});
|
|
230
248
|
return true;
|
|
231
249
|
}
|
|
232
|
-
if (this.#dockPointerSuppressed)
|
|
250
|
+
if (this.#dockPointerSuppressed || this.#tailPointerSuppressed)
|
|
233
251
|
return true;
|
|
234
252
|
if (this.#viewport.selectionActive) {
|
|
235
253
|
this.#viewport.extendSelection(event.column, event.row, now, false);
|
|
@@ -316,7 +334,13 @@ export class SessionViewportController {
|
|
|
316
334
|
return true;
|
|
317
335
|
}
|
|
318
336
|
if (event.row >= 1 && event.row <= hits.viewportHeight && event.column <= frame.contentWidth) {
|
|
319
|
-
this.#viewport.pressSelection(event.column, event.row, now)
|
|
337
|
+
if (!this.#viewport.pressSelection(event.column, event.row, now)) {
|
|
338
|
+
// Invariant: a full drag begun on transient tail rows cannot become transcript selection.
|
|
339
|
+
if (hits.transientTail.includes(event.row))
|
|
340
|
+
this.#tailPointerSuppressed = true;
|
|
341
|
+
repaint = true;
|
|
342
|
+
return this.#tailPointerSuppressed;
|
|
343
|
+
}
|
|
320
344
|
repaint = true;
|
|
321
345
|
return true;
|
|
322
346
|
}
|
|
@@ -354,6 +378,10 @@ export class SessionViewportController {
|
|
|
354
378
|
this.#dockPointerSuppressed = false;
|
|
355
379
|
return true;
|
|
356
380
|
}
|
|
381
|
+
if (this.#tailPointerSuppressed) {
|
|
382
|
+
this.#tailPointerSuppressed = false;
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
357
385
|
}
|
|
358
386
|
return false;
|
|
359
387
|
});
|
|
@@ -368,8 +396,9 @@ export class SessionViewportController {
|
|
|
368
396
|
#hyperlinkKeyAt(frame, column, row) {
|
|
369
397
|
if (row < 1 || row > frame.rows.length || column < 1)
|
|
370
398
|
return undefined;
|
|
371
|
-
const
|
|
372
|
-
|
|
399
|
+
const range = readVisibleHyperlinks(frame.rows[row - 1] ?? "").ranges
|
|
400
|
+
.find(candidate => column - 1 >= candidate.from && column - 1 < candidate.to);
|
|
401
|
+
return range === undefined ? undefined : JSON.stringify([row, range]);
|
|
373
402
|
}
|
|
374
403
|
#updateSelectionAutoScroll(column, row, viewportHeight) {
|
|
375
404
|
const beyondEdge = row <= 1 || row > viewportHeight;
|
|
@@ -21,7 +21,7 @@ export interface PiTuiDamageFrameSafety {
|
|
|
21
21
|
readonly selectionActive: boolean;
|
|
22
22
|
readonly replacementSurfaceActive: boolean;
|
|
23
23
|
}
|
|
24
|
-
export type PiTuiDamageDecisionReason = "transformed" | "suppressed-redundant-clear" | "not-armed" | "unsafe-frame" | "stale-frame" | "unsupported-region-scroll" | "geometry-mismatch" | "grammar-mismatch" | "unsafe-terminal-content" | "incomplete-prior-frame" | "excessive-real-damage";
|
|
24
|
+
export type PiTuiDamageDecisionReason = "transformed" | "suppressed-redundant-clear" | "hyperlink-cleanup" | "pending-hyperlink-cleanup" | "not-armed" | "unsafe-frame" | "stale-frame" | "unsupported-region-scroll" | "geometry-mismatch" | "grammar-mismatch" | "unsafe-terminal-content" | "incomplete-prior-frame" | "excessive-real-damage";
|
|
25
25
|
export interface PiTuiDamageDecision {
|
|
26
26
|
readonly frameId: number | null;
|
|
27
27
|
readonly transformed: boolean;
|
|
@@ -31,7 +31,15 @@ export interface PiTuiDamageDecision {
|
|
|
31
31
|
}
|
|
32
32
|
export interface DamageAwareTerminalOptions {
|
|
33
33
|
readonly regionalScroll: boolean;
|
|
34
|
+
/** The shell supplies visible-row analysis; the terminal layer owns no UI parser. */
|
|
35
|
+
readonly inspectHyperlinks: (content: string) => {
|
|
36
|
+
readonly ranges: readonly unknown[];
|
|
37
|
+
readonly signature: string;
|
|
38
|
+
readonly replaySafe: boolean;
|
|
39
|
+
readonly width: number;
|
|
40
|
+
};
|
|
34
41
|
readonly onResize?: () => void;
|
|
42
|
+
readonly onHyperlinkCleanupRequired?: () => void;
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* A fail-closed adapter for one pinned Pi fullscreen write grammar. Semantic
|
|
@@ -47,6 +55,9 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
|
|
|
47
55
|
get rows(): number;
|
|
48
56
|
get kittyProtocolActive(): boolean;
|
|
49
57
|
get lastDecision(): PiTuiDamageDecision;
|
|
58
|
+
get hyperlinkCleanupPending(): boolean;
|
|
59
|
+
/** Latches cleanup through composition/coalescing until a complete write covers it. */
|
|
60
|
+
requestHyperlinkCleanup(): void;
|
|
50
61
|
arm(descriptor: PiTuiDamageFrameDescriptor, safety: PiTuiDamageFrameSafety): void;
|
|
51
62
|
start(onInput: (data: string) => void, onResize: () => void): void;
|
|
52
63
|
stop(): void;
|
|
@@ -2,8 +2,8 @@ export const PINNED_PI_TUI_DAMAGE_GRAMMAR = "@earendil-works/pi-tui@0.84.2:tui-a
|
|
|
2
2
|
const BEGIN_SYNCHRONIZED_OUTPUT = "\u001b[?2026h";
|
|
3
3
|
const END_SYNCHRONIZED_OUTPUT = "\u001b[?2026l";
|
|
4
4
|
const ROW_MARKER = /\u001b\[(\d+);1H\u001b\[2K/gu;
|
|
5
|
-
const CURSOR_SUFFIX =
|
|
6
|
-
const
|
|
5
|
+
const CURSOR_SUFFIX = /(?:\u001b\[(\d+);(\d+)H)?\u001b\[\?25[hl]\u001b\[\?2026l$/u;
|
|
6
|
+
const OUT_OF_BAND = /^(?:\u001b\](?:0|2|9|52);[^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b\[\?25[hl])+$/u;
|
|
7
7
|
/**
|
|
8
8
|
* A fail-closed adapter for one pinned Pi fullscreen write grammar. Semantic
|
|
9
9
|
* viewport metadata authorizes a shift; bytes are inspected only to validate
|
|
@@ -13,6 +13,11 @@ export class DamageAwareTerminalAdapter {
|
|
|
13
13
|
inner;
|
|
14
14
|
options;
|
|
15
15
|
#rows = new Map();
|
|
16
|
+
#links = new Map();
|
|
17
|
+
#cleanupRevision = 0;
|
|
18
|
+
#cleanedRevision = 0;
|
|
19
|
+
#recoveryRevision = 0;
|
|
20
|
+
#epoch = 0;
|
|
16
21
|
#armed;
|
|
17
22
|
#lastConsumedFrameId = 0;
|
|
18
23
|
#cacheWidth = 0;
|
|
@@ -32,18 +37,26 @@ export class DamageAwareTerminalAdapter {
|
|
|
32
37
|
get rows() { return this.inner.rows; }
|
|
33
38
|
get kittyProtocolActive() { return this.inner.kittyProtocolActive; }
|
|
34
39
|
get lastDecision() { return this.#decision; }
|
|
40
|
+
get hyperlinkCleanupPending() { return this.#cleanupRevision > this.#cleanedRevision; }
|
|
41
|
+
/** Latches cleanup through composition/coalescing until a complete write covers it. */
|
|
42
|
+
requestHyperlinkCleanup() { this.#cleanupRevision += 1; }
|
|
35
43
|
arm(descriptor, safety) {
|
|
36
44
|
this.#armed = { descriptor, safety };
|
|
37
45
|
}
|
|
38
46
|
start(onInput, onResize) {
|
|
39
47
|
this.inner.start(onInput, () => {
|
|
40
|
-
this.#
|
|
48
|
+
this.#epoch += 1;
|
|
49
|
+
this.#recoveryRevision = 0;
|
|
50
|
+
this.#invalidatePresentation();
|
|
41
51
|
this.options.onResize?.();
|
|
42
52
|
onResize();
|
|
43
53
|
});
|
|
44
54
|
}
|
|
45
55
|
stop() {
|
|
56
|
+
this.#epoch += 1;
|
|
46
57
|
this.#armed = undefined;
|
|
58
|
+
this.#cleanupRevision = this.#cleanedRevision = this.#recoveryRevision = 0;
|
|
59
|
+
this.#lastConsumedFrameId = 0;
|
|
47
60
|
this.#invalidateRows();
|
|
48
61
|
this.inner.stop();
|
|
49
62
|
}
|
|
@@ -52,25 +65,71 @@ export class DamageAwareTerminalAdapter {
|
|
|
52
65
|
const armed = this.#armed;
|
|
53
66
|
this.#armed = undefined;
|
|
54
67
|
const parsed = parsePinnedFullscreenWrite(data);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this
|
|
68
|
+
if (armed !== undefined && armed.descriptor.frameId <= this.#lastConsumedFrameId) {
|
|
69
|
+
// Concurrency: a recognized obsolete frame must never repaint newer cells,
|
|
70
|
+
// acknowledge cleanup, or become the reference for a subsequent differential.
|
|
71
|
+
this.#decision = this.#decide(armed, parsed);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const sameGeometry = this.#cacheWidth === this.columns && this.#cacheHeight === this.rows;
|
|
75
|
+
const unknownPaint = parsed === null && !OUT_OF_BAND.test(data);
|
|
76
|
+
if (!sameGeometry || unknownPaint)
|
|
77
|
+
this.#invalidatePresentation();
|
|
78
|
+
if (unknownPaint && !this.hyperlinkCleanupPending && this.options.inspectHyperlinks(data).ranges.length > 0) {
|
|
79
|
+
this.requestHyperlinkCleanup();
|
|
59
80
|
}
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
if (parsed !== null && !this.hyperlinkCleanupPending) {
|
|
82
|
+
for (const row of parsed.rows) {
|
|
83
|
+
const previous = this.#links.get(row.row);
|
|
84
|
+
if (previous === undefined || this.#rows.get(row.row) === row.content
|
|
85
|
+
|| (previous.replaySafe && previous.ranges.length === 0))
|
|
86
|
+
continue;
|
|
87
|
+
const next = this.options.inspectHyperlinks(row.content);
|
|
88
|
+
if (previous.ranges.length > 0 && previous.signature !== next.signature) {
|
|
89
|
+
this.requestHyperlinkCleanup();
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const decision = this.#decide(armed, parsed);
|
|
95
|
+
const cleanup = this.hyperlinkCleanupPending && parsed !== null
|
|
96
|
+
&& (armed === undefined || (armed.descriptor.width === this.columns && armed.descriptor.height === this.rows))
|
|
97
|
+
? this.#completeCleanupFrame(parsed) : null;
|
|
98
|
+
const coveredRevision = this.#cleanupRevision;
|
|
99
|
+
const epoch = this.#epoch;
|
|
100
|
+
let output = data;
|
|
101
|
+
if (cleanup !== null) {
|
|
102
|
+
output = buildCompleteWrite(cleanup);
|
|
103
|
+
this.#decision = {
|
|
104
|
+
frameId: armed?.descriptor.frameId ?? null, transformed: output !== data,
|
|
105
|
+
reason: "hyperlink-cleanup", shiftRows: 0, paintedRows: cleanup.rows.map(row => row.row),
|
|
106
|
+
};
|
|
62
107
|
}
|
|
63
108
|
else {
|
|
64
|
-
this
|
|
109
|
+
this.#decision = decision;
|
|
110
|
+
if (decision.reason === "suppressed-redundant-clear" && parsed !== null)
|
|
111
|
+
output = buildClearlessWrite(parsed);
|
|
112
|
+
else if (decision.transformed && armed !== undefined && parsed !== null) {
|
|
113
|
+
output = buildDamageWrite(parsed, armed.descriptor, new Set(decision.paintedRows), this.#rows);
|
|
114
|
+
}
|
|
65
115
|
}
|
|
66
|
-
this
|
|
116
|
+
this.inner.write(output);
|
|
117
|
+
if (epoch !== this.#epoch
|
|
118
|
+
|| (armed !== undefined && this.#lastConsumedFrameId !== armed.descriptor.frameId))
|
|
119
|
+
return;
|
|
120
|
+
// Invariant: cache invalidation follows forwarded bytes, not a clear which
|
|
121
|
+
// was stripped from the input. A synthesized cleanup publishes every row.
|
|
122
|
+
this.#rememberFrame(armed?.descriptor, cleanup ?? parsed, output);
|
|
123
|
+
if (cleanup !== null)
|
|
124
|
+
this.#cleanedRevision = Math.max(this.#cleanedRevision, coveredRevision);
|
|
125
|
+
this.#scheduleCleanupRecovery();
|
|
67
126
|
}
|
|
68
127
|
moveBy(lines) { this.inner.moveBy(lines); }
|
|
69
128
|
hideCursor() { this.inner.hideCursor(); }
|
|
70
129
|
showCursor() { this.inner.showCursor(); }
|
|
71
|
-
clearLine() { this.#
|
|
72
|
-
clearFromCursor() { this.#
|
|
73
|
-
clearScreen() { this.#
|
|
130
|
+
clearLine() { this.#invalidatePresentation(); this.inner.clearLine(); }
|
|
131
|
+
clearFromCursor() { this.#invalidatePresentation(); this.inner.clearFromCursor(); }
|
|
132
|
+
clearScreen() { this.#invalidatePresentation(); this.inner.clearScreen(); }
|
|
74
133
|
setTitle(title) { this.inner.setTitle(title); }
|
|
75
134
|
setProgress(active) { this.inner.setProgress(active); }
|
|
76
135
|
#decide(armed, parsed) {
|
|
@@ -87,9 +146,12 @@ export class DamageAwareTerminalAdapter {
|
|
|
87
146
|
if (descriptor.frameId <= this.#lastConsumedFrameId)
|
|
88
147
|
return { ...base, reason: "stale-frame" };
|
|
89
148
|
this.#lastConsumedFrameId = descriptor.frameId;
|
|
149
|
+
if (this.hyperlinkCleanupPending)
|
|
150
|
+
return { ...base, reason: "pending-hyperlink-cleanup" };
|
|
90
151
|
if (parsed !== null && parsed.structuralPrefix === "\u001b[2J"
|
|
91
152
|
&& this.#cacheWidth === descriptor.width && this.#cacheHeight === descriptor.height
|
|
92
|
-
&&
|
|
153
|
+
&& descriptor.width === this.columns && descriptor.height === this.rows
|
|
154
|
+
&& hasCompleteScreenRows(parsed.rows, descriptor.height) && this.#hasSameHyperlinkState(parsed.rows)) {
|
|
93
155
|
return {
|
|
94
156
|
frameId: descriptor.frameId,
|
|
95
157
|
transformed: true,
|
|
@@ -111,7 +173,9 @@ export class DamageAwareTerminalAdapter {
|
|
|
111
173
|
}
|
|
112
174
|
if (parsed === null)
|
|
113
175
|
return { ...base, reason: "grammar-mismatch" };
|
|
114
|
-
if (parsed.structuralPrefix.length > 0
|
|
176
|
+
if (parsed.structuralPrefix.length > 0
|
|
177
|
+
|| hasUnsafeTerminalContent(parsed, this.options.inspectHyperlinks, descriptor.transcript)
|
|
178
|
+
|| this.#hasLinkRisk(descriptor.transcript) || this.#hasUnsafeRows(descriptor.transcript)) {
|
|
115
179
|
return { ...base, reason: "unsafe-terminal-content" };
|
|
116
180
|
}
|
|
117
181
|
const { rowStart, rowEnd } = descriptor.transcript;
|
|
@@ -160,16 +224,85 @@ export class DamageAwareTerminalAdapter {
|
|
|
160
224
|
}
|
|
161
225
|
if (parsed === null)
|
|
162
226
|
return;
|
|
163
|
-
|
|
227
|
+
const width = descriptor?.width ?? this.columns;
|
|
228
|
+
const height = descriptor?.height ?? this.rows;
|
|
229
|
+
if (width !== this.#cacheWidth || height !== this.#cacheHeight) {
|
|
164
230
|
this.#invalidateRows();
|
|
165
|
-
this.#cacheWidth =
|
|
166
|
-
this.#cacheHeight =
|
|
231
|
+
this.#cacheWidth = width;
|
|
232
|
+
this.#cacheHeight = height;
|
|
167
233
|
}
|
|
168
|
-
|
|
234
|
+
if (parsed.rows.some(row => row.row > this.rows)) {
|
|
235
|
+
this.#invalidatePresentation();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
for (const row of parsed.rows) {
|
|
239
|
+
if (this.#rows.get(row.row) !== row.content)
|
|
240
|
+
this.#links.set(row.row, this.options.inspectHyperlinks(row.content));
|
|
169
241
|
this.#rows.set(row.row, row.content);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
#hasSameHyperlinkState(rows) {
|
|
245
|
+
return rows.every(row => {
|
|
246
|
+
const previous = this.#links.get(row.row);
|
|
247
|
+
const next = row.content === this.#rows.get(row.row) ? previous : this.options.inspectHyperlinks(row.content);
|
|
248
|
+
return next?.replaySafe === true && (previous?.signature ?? "") === next.signature;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
#hasLinkRisk(region) {
|
|
252
|
+
for (const [row, state] of this.#links) {
|
|
253
|
+
if (region !== undefined && (row < region.rowStart || row > region.rowEnd))
|
|
254
|
+
continue;
|
|
255
|
+
if (state.ranges.length > 0)
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
#hasUnsafeRows(region) {
|
|
261
|
+
for (const [row, state] of this.#links) {
|
|
262
|
+
if (row >= region.rowStart && row <= region.rowEnd && !state.replaySafe)
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
#completeCleanupFrame(parsed) {
|
|
268
|
+
const desired = parsed.structuralPrefix.length === 0 ? new Map(this.#rows) : new Map();
|
|
269
|
+
for (const row of parsed.rows)
|
|
270
|
+
desired.set(row.row, row.content);
|
|
271
|
+
if (desired.size !== this.rows)
|
|
272
|
+
return null;
|
|
273
|
+
const rows = [];
|
|
274
|
+
for (let row = 1; row <= this.rows; row += 1) {
|
|
275
|
+
const content = desired.get(row);
|
|
276
|
+
if (content === undefined)
|
|
277
|
+
return null;
|
|
278
|
+
const state = content === this.#rows.get(row) ? this.#links.get(row) : this.options.inspectHyperlinks(content);
|
|
279
|
+
if (state?.replaySafe !== true || state.width > this.columns)
|
|
280
|
+
return null;
|
|
281
|
+
rows.push({ row, content, segment: `\u001b[${row};1H\u001b[2K${content}` });
|
|
282
|
+
}
|
|
283
|
+
return { structuralPrefix: "\u001b[2J", rows, cursorSuffix: parsed.cursorSuffix };
|
|
284
|
+
}
|
|
285
|
+
#scheduleCleanupRecovery() {
|
|
286
|
+
if (!this.hyperlinkCleanupPending || this.#recoveryRevision === this.#cleanupRevision
|
|
287
|
+
|| this.options.onHyperlinkCleanupRequired === undefined)
|
|
288
|
+
return;
|
|
289
|
+
this.#recoveryRevision = this.#cleanupRevision;
|
|
290
|
+
const epoch = this.#epoch;
|
|
291
|
+
// Concurrency: force only after Pi finishes updating its previous-screen
|
|
292
|
+
// state. One recovery per revision avoids loops on unsupported image frames.
|
|
293
|
+
queueMicrotask(() => {
|
|
294
|
+
if (epoch === this.#epoch && this.hyperlinkCleanupPending)
|
|
295
|
+
this.options.onHyperlinkCleanupRequired?.();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
#invalidatePresentation() {
|
|
299
|
+
if (this.#hasLinkRisk() && !this.hyperlinkCleanupPending)
|
|
300
|
+
this.requestHyperlinkCleanup();
|
|
301
|
+
this.#invalidateRows();
|
|
170
302
|
}
|
|
171
303
|
#invalidateRows() {
|
|
172
304
|
this.#rows.clear();
|
|
305
|
+
this.#links.clear();
|
|
173
306
|
this.#cacheWidth = 0;
|
|
174
307
|
this.#cacheHeight = 0;
|
|
175
308
|
}
|
|
@@ -183,7 +316,8 @@ function parsePinnedFullscreenWrite(data) {
|
|
|
183
316
|
const body = data.slice(BEGIN_SYNCHRONIZED_OUTPUT.length, suffix.index);
|
|
184
317
|
const matches = [...body.matchAll(ROW_MARKER)];
|
|
185
318
|
if (matches.length === 0)
|
|
186
|
-
return
|
|
319
|
+
return body.length === 0
|
|
320
|
+
? { structuralPrefix: "", rows: [], cursorSuffix: data.slice(suffix.index) } : null;
|
|
187
321
|
const structuralPrefix = body.slice(0, matches[0].index);
|
|
188
322
|
if (structuralPrefix !== "" && structuralPrefix !== "\u001b[2J")
|
|
189
323
|
return null;
|
|
@@ -203,14 +337,16 @@ function parsePinnedFullscreenWrite(data) {
|
|
|
203
337
|
}
|
|
204
338
|
return { structuralPrefix, rows, cursorSuffix: data.slice(suffix.index) };
|
|
205
339
|
}
|
|
206
|
-
function hasUnsafeTerminalContent(parsed) {
|
|
340
|
+
function hasUnsafeTerminalContent(parsed, inspect, linkRegion) {
|
|
207
341
|
const data = parsed.rows.map(row => row.content).join("");
|
|
208
342
|
if (data.includes("\u001b_G") || data.includes("\u001bPq") || data.includes("\u001b[2J"))
|
|
209
343
|
return true;
|
|
210
|
-
|
|
211
|
-
if ((
|
|
212
|
-
return
|
|
213
|
-
|
|
344
|
+
return parsed.rows.some(row => {
|
|
345
|
+
if (linkRegion !== undefined && (row.row < linkRegion.rowStart || row.row > linkRegion.rowEnd))
|
|
346
|
+
return false;
|
|
347
|
+
const state = inspect(row.content);
|
|
348
|
+
return !state.replaySafe || state.ranges.length > 0;
|
|
349
|
+
});
|
|
214
350
|
}
|
|
215
351
|
function buildDamageWrite(parsed, descriptor, paintedRows, previousRows) {
|
|
216
352
|
const transcript = descriptor.transcript;
|
|
@@ -227,6 +363,10 @@ function buildDamageWrite(parsed, descriptor, paintedRows, previousRows) {
|
|
|
227
363
|
}).join("");
|
|
228
364
|
return `${BEGIN_SYNCHRONIZED_OUTPUT}${regionShift}${rowPaint}${parsed.cursorSuffix}`;
|
|
229
365
|
}
|
|
366
|
+
/** Keeps the clear and all current rows in the same synchronized transaction. */
|
|
367
|
+
function buildCompleteWrite(parsed) {
|
|
368
|
+
return `${BEGIN_SYNCHRONIZED_OUTPUT}${parsed.structuralPrefix}${parsed.rows.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
|
|
369
|
+
}
|
|
230
370
|
function buildClearlessWrite(parsed) {
|
|
231
371
|
return `${BEGIN_SYNCHRONIZED_OUTPUT}${parsed.rows.map(row => row.segment).join("")}${parsed.cursorSuffix}`;
|
|
232
372
|
}
|