@timurproko/a1 0.2.7-dev.1255 → 0.2.7-dev.1264

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.
Files changed (78) hide show
  1. package/dist/app/session-shell/prompt-submission.d.ts +1 -1
  2. package/dist/app/session-shell/prompt-submission.js +9 -9
  3. package/dist/app/session-shell/resource-labels.d.ts +2 -0
  4. package/dist/app/session-shell/resource-labels.js +158 -0
  5. package/dist/app/session-shell/session-prompt-history.d.ts +29 -0
  6. package/dist/app/session-shell/session-prompt-history.js +80 -0
  7. package/dist/app/session-shell/session-selectors.d.ts +46 -0
  8. package/dist/app/session-shell/session-selectors.js +269 -0
  9. package/dist/app/session-shell/session-shell-root.d.ts +6 -28
  10. package/dist/app/session-shell/session-shell-root.js +26 -220
  11. package/dist/app/session-shell/session-shell.d.ts +3 -23
  12. package/dist/app/session-shell/session-shell.js +54 -365
  13. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -10
  14. package/dist/app/session-shell/session-viewport-controller.js +40 -318
  15. package/dist/app/session-shell/settings-bindings.d.ts +1 -1
  16. package/dist/app/session-shell/settings-bindings.js +2 -2
  17. package/dist/app/session-shell/viewport-pointer.d.ts +83 -0
  18. package/dist/app/session-shell/viewport-pointer.js +361 -0
  19. package/dist/composition/owned-ui.js +63 -59
  20. package/dist/contracts/owned-ui/index.d.ts +1 -1
  21. package/dist/contracts/owned-ui/index.js +1 -1
  22. package/dist/contracts/owned-ui/validation.d.ts +1 -11
  23. package/dist/contracts/owned-ui/validation.js +8 -104
  24. package/dist/contracts/presentation/index.d.ts +1 -1
  25. package/dist/contracts/presentation/terminal.d.ts +0 -6
  26. package/dist/features/owned-ui/settings-app.js +13 -208
  27. package/dist/features/owned-ui/settings-model-dialog.d.ts +41 -0
  28. package/dist/features/owned-ui/settings-model-dialog.js +194 -0
  29. package/dist/foundation/release/automatic-update.js +1 -8
  30. package/dist/foundation/release/dependency-layer.js +3 -3
  31. package/dist/foundation/release/latest-release.js +1 -1
  32. package/dist/foundation/release/managed-path.d.ts +10 -0
  33. package/dist/foundation/release/managed-path.js +26 -0
  34. package/dist/foundation/release/release-gc.js +6 -8
  35. package/dist/foundation/release/restart-certification.js +3 -4
  36. package/dist/foundation/release/update-activation.js +1 -1
  37. package/dist/foundation/release/update-execution-lease.js +1 -7
  38. package/dist/foundation/release/update-installer.d.ts +45 -0
  39. package/dist/foundation/release/update-installer.js +57 -0
  40. package/dist/foundation/release/update-recovery.js +2 -9
  41. package/dist/foundation/release/update.d.ts +4 -23
  42. package/dist/foundation/release/update.js +160 -179
  43. package/dist/integrations/pi/components/bracketed-paste.d.ts +33 -0
  44. package/dist/integrations/pi/components/bracketed-paste.js +123 -0
  45. package/dist/integrations/pi/components/owned-editor-ux.js +46 -140
  46. package/dist/integrations/pi/engine/adapter.d.ts +1 -8
  47. package/dist/integrations/pi/engine/adapter.js +6 -49
  48. package/dist/integrations/pi/engine/host.js +1 -1
  49. package/dist/integrations/pi/engine/repository-metadata.d.ts +49 -0
  50. package/dist/integrations/pi/engine/repository-metadata.js +189 -0
  51. package/dist/integrations/pi/engine/session-runtime.d.ts +6 -12
  52. package/dist/integrations/pi/engine/session-runtime.js +21 -178
  53. package/dist/integrations/pi/engine/view-state.d.ts +21 -2
  54. package/dist/integrations/pi/engine/view-state.js +129 -1
  55. package/dist/integrations/pi/engine/workflow-runner.d.ts +0 -1
  56. package/dist/integrations/pi/engine/workflow-runner.js +254 -365
  57. package/dist/integrations/pi/engine/workflow-share.d.ts +9 -0
  58. package/dist/integrations/pi/engine/workflow-share.js +168 -0
  59. package/dist/integrations/pi/engine/workflow-support.d.ts +0 -14
  60. package/dist/integrations/pi/engine/workflow-support.js +0 -47
  61. package/dist/integrations/pi/tui-runtime/adapter.d.ts +9 -19
  62. package/dist/integrations/pi/tui-runtime/adapter.js +35 -144
  63. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +4 -4
  64. package/dist/integrations/pi/tui-runtime/index.d.ts +0 -1
  65. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  66. package/dist/integrations/pi/tui-runtime/presentation-adapter.d.ts +2 -2
  67. package/dist/ui/components/transcript-viewport-frame.d.ts +120 -0
  68. package/dist/ui/components/transcript-viewport-frame.js +314 -0
  69. package/dist/ui/components/transcript-viewport-types.d.ts +147 -0
  70. package/dist/ui/components/transcript-viewport.d.ts +2 -147
  71. package/dist/ui/components/transcript-viewport.js +87 -315
  72. package/package.json +3 -3
  73. package/dist/integrations/pi/engine/diagnostics-state.d.ts +0 -9
  74. package/dist/integrations/pi/engine/diagnostics-state.js +0 -34
  75. package/dist/integrations/pi/engine/usage-view.d.ts +0 -4
  76. package/dist/integrations/pi/engine/usage-view.js +0 -56
  77. package/dist/integrations/pi/tui-runtime/contracts.d.ts +0 -22
  78. /package/dist/{integrations/pi/tui-runtime/contracts.js → ui/components/transcript-viewport-types.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { TuiAltScreen, type Component, type OverlayHandle, type OverlayOptions } from "@earendil-works/pi-tui";
2
2
  import type { PresentationComponentPort, PresentationPointerSurface } from "../../../contracts/presentation/index.js";
3
3
  import { samePointerSurfaces } from "../../../contracts/presentation/index.js";
4
- import type { PiTuiOverlayOptions } from "./contracts.js";
4
+ import type { TerminalOverlayOptions } from "../../../contracts/presentation/index.js";
5
5
  /** Observes public component renders in compositor order, without accessing renderer state. */
6
6
  export declare class OverlayGeometryTracker {
7
7
  #private;
@@ -10,7 +10,7 @@ export declare class OverlayGeometryTracker {
10
10
  constructor(publish: (surfaces: readonly PresentationPointerSurface[] | null) => void);
11
11
  invalidate(): void;
12
12
  begin(columns: number, rows: number): void;
13
- record(component: PresentationComponentPort, options: PiTuiOverlayOptions | undefined, width: number, height: number): void;
13
+ record(component: PresentationComponentPort, options: TerminalOverlayOptions | undefined, width: number, height: number): void;
14
14
  end(): void;
15
15
  }
16
16
  /** The protected composition hook brackets the actual visible, focus-ordered overlay renders. */
@@ -1,4 +1,4 @@
1
1
  import type { PresentationComponentPort, PresentationRuntimePort, PresentationTerminalPort } from "../../../contracts/presentation/index.js";
2
- import type { PiTuiTerminalPort } from "./contracts.js";
2
+ import type { TerminalPort } from "../../../contracts/presentation/index.js";
3
3
  export declare function createPiPresentationRuntime(root: PresentationComponentPort, terminal: PresentationTerminalPort): PresentationRuntimePort;
4
- export declare function createPiTerminalBridge(terminal: PresentationTerminalPort): PiTuiTerminalPort;
4
+ export declare function createPiTerminalBridge(terminal: PresentationTerminalPort): TerminalPort;
@@ -0,0 +1,120 @@
1
+ import { scrollbarPresentation, type ScrollbarGeometry } from "./scrollbar.js";
2
+ import type { TranscriptViewportConfig, TranscriptViewportFrameDescriptor, TranscriptViewportFrameInput, TranscriptViewportHitRegions, TranscriptViewportTheme } from "./transcript-viewport-types.js";
3
+ export declare const VIEWPORT_PROMPT_ZONES: RegExp;
4
+ type ScrollbarPresentation = ReturnType<typeof scrollbarPresentation>;
5
+ /** The document rows with the flexible gap that bottom-aligns the live tail while content fits. */
6
+ export declare function alignTailRows(sourceRows: readonly string[], requestedTailRows: number | undefined, viewportHeight: number): {
7
+ readonly documentRows: readonly string[];
8
+ readonly bottomAlignedTailRowCount: number;
9
+ readonly transientAlignmentGapRows: number;
10
+ };
11
+ export declare function layoutScrollbar(input: {
12
+ readonly contentLength: number;
13
+ readonly viewportHeight: number;
14
+ readonly scroll: number;
15
+ readonly config: TranscriptViewportConfig;
16
+ readonly hovered: boolean;
17
+ readonly dragging: boolean;
18
+ readonly activeUntil: number;
19
+ readonly now: number;
20
+ }): {
21
+ readonly geometry: ScrollbarGeometry | null;
22
+ readonly presentation: ScrollbarPresentation;
23
+ };
24
+ export interface FloatingBottomControl {
25
+ readonly hit: NonNullable<TranscriptViewportHitRegions["bottom"]>;
26
+ readonly row: number;
27
+ readonly left: number;
28
+ readonly text: string;
29
+ }
30
+ /** The jump-to-bottom control while detached, placed on its anchor row and hovered from this frame's geometry. */
31
+ export declare function placeBottomControl(input: {
32
+ readonly detached: boolean;
33
+ readonly newMessages: number;
34
+ readonly contentWidth: number;
35
+ readonly viewportHeight: number;
36
+ readonly frameRowCount: number;
37
+ readonly anchorRow: number | undefined;
38
+ readonly pointer: TranscriptViewportFrameInput["pointerPosition"];
39
+ readonly theme: TranscriptViewportTheme;
40
+ }): FloatingBottomControl | undefined;
41
+ /** Selection source rows with the cells under the floating control and covering surfaces blanked for copy. */
42
+ export declare function maskSelectionRows(rows: readonly string[], width: number, control: FloatingBottomControl | undefined, coveredCells: TranscriptViewportFrameInput["coveredCells"]): string[];
43
+ /** What every row of one frame is painted with. */
44
+ export interface FrameRowPaint {
45
+ readonly width: number;
46
+ readonly contentWidth: number;
47
+ readonly viewportHeight: number;
48
+ readonly cacheLimit: number;
49
+ readonly theme: TranscriptViewportTheme;
50
+ readonly selectionPainterId: number;
51
+ readonly geometry: ScrollbarGeometry | null;
52
+ readonly presentation: ScrollbarPresentation;
53
+ readonly railActive: boolean;
54
+ readonly overlay: TranscriptViewportFrameInput["frameOverlay"];
55
+ readonly control: FloatingBottomControl | undefined;
56
+ }
57
+ /**
58
+ * The bounded row caches behind frame composition: painted source rows, padded base rows,
59
+ * selected rows and final rows, keyed by content and by the identity of the painter functions.
60
+ */
61
+ export declare class ViewportRowCaches {
62
+ #private;
63
+ get entries(): number;
64
+ clear(): void;
65
+ trim(limit: number): void;
66
+ /** A stable id for a painter function, so cache keys change when the painter does. */
67
+ functionId(value: Function): number;
68
+ /** One document row through the paint-only transform. */
69
+ painted(paintId: number, row: string, limit: number, paint: (row: string) => string): {
70
+ readonly value: string;
71
+ readonly reused: boolean;
72
+ };
73
+ /**
74
+ * One frame row: padded to the frame, selection painted, then the rail, the floating control and
75
+ * the overlay above it. `state` identifies everything the row was painted from.
76
+ */
77
+ paintRow(frame: FrameRowPaint, row: number, painted: string, range: {
78
+ readonly from: number;
79
+ readonly to: number;
80
+ } | null, paintRecomputed: boolean): {
81
+ readonly text: string;
82
+ readonly recomputed: boolean;
83
+ readonly state: string;
84
+ };
85
+ }
86
+ export interface FrameDescriptorInput {
87
+ readonly frameId: number;
88
+ readonly previous: TranscriptViewportFrameDescriptor | null;
89
+ readonly width: number;
90
+ readonly height: number;
91
+ readonly viewportHeight: number;
92
+ readonly dockRowCount: number;
93
+ readonly documentRowCount: number;
94
+ readonly selectableDocumentRowCount: number;
95
+ readonly scrollTop: number;
96
+ readonly followingEnd: boolean;
97
+ readonly selecting: boolean;
98
+ readonly transientAlignmentGapRows: number;
99
+ readonly bottomAlignedTailRowCount: number;
100
+ readonly liveTailStartRow: number | undefined;
101
+ readonly selectionRevision: number;
102
+ readonly selectionDamagedRows: readonly number[];
103
+ }
104
+ /** What changed since the previous frame, and why: geometry, follow shift, detachment or steady state. */
105
+ export declare function frameDescriptor(input: FrameDescriptorInput): TranscriptViewportFrameDescriptor;
106
+ /** Where the rail, the sticky prompt header, the bottom control and the transient tail sit in this frame. */
107
+ export declare function hitRegions(input: {
108
+ readonly width: number;
109
+ readonly contentWidth: number;
110
+ readonly viewportHeight: number;
111
+ readonly documentRowCount: number;
112
+ readonly selectableDocumentRowCount: number;
113
+ readonly scrollTop: number;
114
+ readonly geometry: ScrollbarGeometry | null;
115
+ readonly railHidden: boolean;
116
+ readonly stickyTarget: number | null;
117
+ readonly bottom: TranscriptViewportHitRegions["bottom"];
118
+ }): TranscriptViewportHitRegions;
119
+ export declare function sameRectangle(left: TranscriptViewportFrameDescriptor["transcript"], right: TranscriptViewportFrameDescriptor["transcript"]): boolean;
120
+ export {};
@@ -0,0 +1,314 @@
1
+ import { isThumbRow, scrollbarGeometry, scrollbarPresentation } from "./scrollbar.js";
2
+ import { overlaySpan } from "./spans.js";
3
+ import { displayWidth, truncateToWidth } from "./text.js";
4
+ const CONTROL_STYLE_RESET = "\u001b]8;;\u001b\\\u001b[0m";
5
+ // Protocol: upstream OSC 133 zones are layout annotations, not fullscreen cursor commands.
6
+ // Styling/padding can move them away from the row prefix Pi strips; 133;A then advances a real
7
+ // terminal parser to a fresh line. The custom viewport owns prompt anchors separately.
8
+ export const VIEWPORT_PROMPT_ZONES = /\u001b\]133;[ABC](?:\u0007|\u001b\\)/gu;
9
+ // Invariant: scrollbar chrome closes source links and decoration without clearing
10
+ // the row surface painted beneath its dedicated, non-semantic gutter.
11
+ const GUTTER_DECORATION_RESET = "\u001b]8;;\u001b\\\u001b[22;23;24;25;27;28;29;39;54;55m";
12
+ /** The document rows with the flexible gap that bottom-aligns the live tail while content fits. */
13
+ export function alignTailRows(sourceRows, requestedTailRows, viewportHeight) {
14
+ const bottomAlignedTailRowCount = clamp(requestedTailRows ?? 0, 0, sourceRows.length);
15
+ const transientAlignmentGapRows = bottomAlignedTailRowCount > 0
16
+ ? Math.max(0, viewportHeight - sourceRows.length)
17
+ : 0;
18
+ const tailStart = sourceRows.length - bottomAlignedTailRowCount;
19
+ const documentRows = transientAlignmentGapRows === 0
20
+ ? sourceRows
21
+ : [
22
+ ...sourceRows.slice(0, tailStart),
23
+ ...Array.from({ length: transientAlignmentGapRows }, () => ""),
24
+ ...sourceRows.slice(tailStart),
25
+ ];
26
+ return { documentRows, bottomAlignedTailRowCount, transientAlignmentGapRows };
27
+ }
28
+ export function layoutScrollbar(input) {
29
+ const geometry = scrollbarGeometry({
30
+ contentLength: input.contentLength,
31
+ viewportHeight: input.viewportHeight,
32
+ scroll: input.scroll,
33
+ // Invariant: the rail keeps its top gap; the shell owns the spacing above the dock.
34
+ trackHeight: Math.max(0, input.viewportHeight - 1),
35
+ });
36
+ const presentation = scrollbarPresentation({
37
+ geometry,
38
+ appearance: input.config.scrollbarAppearance,
39
+ style: input.config.scrollbarStyle,
40
+ hovered: input.hovered,
41
+ dragging: input.dragging,
42
+ activeUntil: input.activeUntil,
43
+ now: input.now,
44
+ });
45
+ return { geometry, presentation };
46
+ }
47
+ /** The jump-to-bottom control while detached, placed on its anchor row and hovered from this frame's geometry. */
48
+ export function placeBottomControl(input) {
49
+ if (!input.detached || input.frameRowCount === 0)
50
+ return undefined;
51
+ const genericLabel = " Jump to bottom (Ctrl+End) ↓ ";
52
+ const countedLabel = input.newMessages > 0
53
+ ? ` ${input.newMessages} new message${input.newMessages === 1 ? "" : "s"} (Ctrl+End) ↓ `
54
+ : genericLabel;
55
+ const label = displayWidth(countedLabel) <= input.contentWidth ? countedLabel : genericLabel;
56
+ const labelWidth = displayWidth(label);
57
+ if (labelWidth > input.contentWidth)
58
+ return undefined;
59
+ const fallbackRow = Math.max(0, input.viewportHeight - 1);
60
+ const row = clamp(input.anchorRow ?? fallbackRow, 0, input.frameRowCount - 1);
61
+ const left = Math.floor((input.contentWidth - labelWidth) / 2);
62
+ const hit = { row: row + 1, columnStart: left + 1, columnEnd: left + labelWidth };
63
+ // Invariant: appearance and hit testing share current geometry, even when no mouse moves.
64
+ const pointer = input.pointer;
65
+ const hovered = pointer !== undefined && pointer.row === hit.row
66
+ && pointer.column >= hit.columnStart && pointer.column <= hit.columnEnd;
67
+ return { hit, row, left, text: `${CONTROL_STYLE_RESET}${input.theme.bottomControl(label, hovered)}` };
68
+ }
69
+ /** Selection source rows with the cells under the floating control and covering surfaces blanked for copy. */
70
+ export function maskSelectionRows(rows, width, control, coveredCells) {
71
+ const masked = [...rows];
72
+ if (control !== undefined) {
73
+ // Invariant: the control floats above selection; copy restores the text beneath it.
74
+ const labelWidth = control.hit.columnEnd - control.left;
75
+ masked[control.row] = overlaySpan(padRowPreservingBackground(masked[control.row] ?? "", width), control.left, control.left + labelWidth, " ".repeat(labelWidth));
76
+ }
77
+ for (const covered of coveredCells ?? []) {
78
+ const row = covered.row - 1;
79
+ if (row < 0 || row >= masked.length)
80
+ continue;
81
+ const from = clamp(covered.columnStart - 1, 0, width);
82
+ const to = clamp(covered.columnEnd, from, width);
83
+ masked[row] = overlaySpan(padRowPreservingBackground(masked[row] ?? "", width), from, to, " ".repeat(to - from));
84
+ }
85
+ return masked;
86
+ }
87
+ /**
88
+ * The bounded row caches behind frame composition: painted source rows, padded base rows,
89
+ * selected rows and final rows, keyed by content and by the identity of the painter functions.
90
+ */
91
+ export class ViewportRowCaches {
92
+ #painted = new Map();
93
+ #base = new Map();
94
+ #selected = new Map();
95
+ #final = new Map();
96
+ #functionIds = new WeakMap();
97
+ #nextFunctionId = 1;
98
+ get entries() {
99
+ return this.#painted.size + this.#base.size + this.#selected.size + this.#final.size;
100
+ }
101
+ clear() {
102
+ this.#painted.clear();
103
+ this.#base.clear();
104
+ this.#selected.clear();
105
+ this.#final.clear();
106
+ }
107
+ trim(limit) {
108
+ trimCache(this.#painted, limit);
109
+ trimCache(this.#base, limit);
110
+ trimCache(this.#selected, limit);
111
+ trimCache(this.#final, limit);
112
+ }
113
+ /** A stable id for a painter function, so cache keys change when the painter does. */
114
+ functionId(value) {
115
+ const existing = this.#functionIds.get(value);
116
+ if (existing !== undefined)
117
+ return existing;
118
+ const id = this.#nextFunctionId;
119
+ this.#nextFunctionId += 1;
120
+ this.#functionIds.set(value, id);
121
+ return id;
122
+ }
123
+ /** One document row through the paint-only transform. */
124
+ painted(paintId, row, limit, paint) {
125
+ return cachedString(this.#painted, `${paintId}\u0000${row}`, limit, () => paint(row));
126
+ }
127
+ /**
128
+ * One frame row: padded to the frame, selection painted, then the rail, the floating control and
129
+ * the overlay above it. `state` identifies everything the row was painted from.
130
+ */
131
+ paintRow(frame, row, painted, range, paintRecomputed) {
132
+ const { width, contentWidth, viewportHeight, cacheLimit, theme, geometry, presentation } = frame;
133
+ // Invariant: the reserved rail gutter is not a semantic selection or copy cell,
134
+ // but a range that reaches the content boundary paints through it so the rail
135
+ // floats above one continuous selection surface.
136
+ const paintRange = range !== null && row < viewportHeight
137
+ && contentWidth < width && range.to === contentWidth
138
+ ? { from: range.from, to: width }
139
+ : range;
140
+ const padded = row < viewportHeight || paintRange !== null;
141
+ const base = cachedString(this.#base, `${width}\u0000${contentWidth}\u0000${row < viewportHeight}\u0000${padded}\u0000${painted}`, cacheLimit, () => row < viewportHeight
142
+ ? padViewportRowWithGutter(painted, contentWidth, width)
143
+ : padded ? padRowPreservingBackground(painted, width) : painted);
144
+ let recomputed = row < viewportHeight && paintRecomputed || !base.reused;
145
+ const rangeKey = paintRange === null ? "-" : `${paintRange.from}:${paintRange.to}`;
146
+ const selected = paintRange === null
147
+ ? { value: base.value, reused: true }
148
+ : cachedString(this.#selected, `${frame.selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, paintRange.from, paintRange.to));
149
+ recomputed ||= !selected.reused;
150
+ // Invariant: paint the rail after selection over the gutter's continued row
151
+ // background. Source content and selection still end at the preceding cell.
152
+ let railCell = "";
153
+ if (row > 0 && row < viewportHeight && presentation.visible && geometry !== null) {
154
+ const trackRow = row - 1;
155
+ const thumb = isThumbRow(geometry, trackRow);
156
+ const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
157
+ railCell = thumb ? theme.thumb(glyph, frame.railActive) : theme.track(glyph);
158
+ }
159
+ const overlayLimit = row < viewportHeight ? contentWidth : width;
160
+ const overlayText = frame.overlay?.row === row + 1
161
+ ? truncateToWidth(frame.overlay.text, overlayLimit)
162
+ : "";
163
+ const overlayWidth = displayWidth(overlayText);
164
+ const control = frame.control;
165
+ const rowControl = control?.row === row
166
+ ? { left: control.left, right: control.hit.columnEnd, text: control.text }
167
+ : null;
168
+ const final = cachedString(this.#final, `${width}\u0000${railCell}\u0000${overlayText}\u0000${rowControl?.text ?? ""}\u0000${selected.value}`, cacheLimit, () => {
169
+ const withRail = railCell.length === 0 ? selected.value : overlaySpan(selected.value, width - 1, width, `${GUTTER_DECORATION_RESET}${railCell}`, { inheritStartStyle: true });
170
+ // Invariant: floating control styling is applied only after the row and
171
+ // gutter surfaces are resolved, so its inline background cannot leak.
172
+ const withControl = rowControl === null ? withRail : overlaySpan(withRail, rowControl.left, rowControl.right, rowControl.text);
173
+ return overlayWidth === 0 ? withControl : overlaySpan(padRowPreservingBackground(withControl, width), overlayLimit - overlayWidth, overlayLimit, `${GUTTER_DECORATION_RESET}${overlayText}`, { inheritStartStyle: true });
174
+ });
175
+ recomputed ||= !final.reused;
176
+ const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : frame.selectionPainterId}\u0000${railCell}\u0000${overlayText}\u0000${rowControl?.text ?? ""}`;
177
+ return { text: final.value.replace(VIEWPORT_PROMPT_ZONES, ""), recomputed, state };
178
+ }
179
+ }
180
+ /** What changed since the previous frame, and why: geometry, follow shift, detachment or steady state. */
181
+ export function frameDescriptor(input) {
182
+ const { previous, width, height, viewportHeight, scrollTop, followingEnd } = input;
183
+ const transcript = viewportHeight === 0 ? null : { rowStart: 1, rowEnd: viewportHeight };
184
+ const dock = input.dockRowCount === 0 ? null : { rowStart: viewportHeight + 1, rowEnd: height };
185
+ const nextDocumentRange = {
186
+ start: scrollTop,
187
+ end: Math.min(input.documentRowCount, scrollTop + viewportHeight),
188
+ };
189
+ const previousDocumentRange = previous?.nextDocumentRange ?? null;
190
+ const sameGeometry = previous !== null
191
+ && previous.width === width
192
+ && previous.height === height
193
+ && sameRectangle(previous.transcript, transcript)
194
+ && sameRectangle(previous.dock, dock);
195
+ const verticalShiftRows = previousDocumentRange === null ? 0 : nextDocumentRange.start - previousDocumentRange.start;
196
+ const safeVerticalShift = sameGeometry
197
+ && previous?.followingEnd === true
198
+ && followingEnd
199
+ && !input.selecting
200
+ && verticalShiftRows > 0
201
+ && verticalShiftRows < viewportHeight;
202
+ return {
203
+ frameId: input.frameId,
204
+ width,
205
+ height,
206
+ transcript,
207
+ dock,
208
+ previousDocumentRange,
209
+ nextDocumentRange,
210
+ previousFollowingEnd: previous?.followingEnd ?? null,
211
+ followingEnd,
212
+ transientRowCount: Math.max(0, input.documentRowCount - input.selectableDocumentRowCount),
213
+ transientAlignmentGapRows: input.transientAlignmentGapRows,
214
+ bottomAlignedTailRowCount: input.bottomAlignedTailRowCount,
215
+ liveTailRows: visibleLiveTailRows(input.liveTailStartRow, input.selectableDocumentRowCount, scrollTop, viewportHeight),
216
+ verticalShiftRows,
217
+ safeVerticalShift,
218
+ selectionRevision: input.selectionRevision,
219
+ selectionDamagedRows: input.selectionDamagedRows,
220
+ cause: previous === null
221
+ ? "initial"
222
+ : !sameGeometry
223
+ ? "geometry-change"
224
+ : !followingEnd
225
+ ? "detached"
226
+ : safeVerticalShift
227
+ ? "follow-shift"
228
+ : "steady",
229
+ };
230
+ }
231
+ /** Where the rail, the sticky prompt header, the bottom control and the transient tail sit in this frame. */
232
+ export function hitRegions(input) {
233
+ const { viewportHeight, scrollTop, geometry } = input;
234
+ const tailStart = Math.max(scrollTop, input.selectableDocumentRowCount);
235
+ return {
236
+ viewportHeight,
237
+ rail: geometry === null || input.railHidden ? null : {
238
+ column: input.width,
239
+ rowStart: 2,
240
+ trackHeight: Math.max(0, viewportHeight - 1),
241
+ geometry,
242
+ },
243
+ sticky: input.stickyTarget === null ? null : { row: 1, target: input.stickyTarget, width: input.contentWidth },
244
+ bottom: input.bottom,
245
+ transientTail: Array.from({ length: Math.max(0, Math.min(input.documentRowCount, scrollTop + viewportHeight) - tailStart) }, (_row, index) => tailStart - scrollTop + index + 1),
246
+ };
247
+ }
248
+ export function sameRectangle(left, right) {
249
+ return left === null ? right === null : right !== null && left.rowStart === right.rowStart && left.rowEnd === right.rowEnd;
250
+ }
251
+ function cachedString(cache, key, limit, create) {
252
+ const existing = cache.get(key);
253
+ if (existing !== undefined) {
254
+ cache.delete(key);
255
+ cache.set(key, existing);
256
+ return { value: existing, reused: true };
257
+ }
258
+ const value = create();
259
+ cache.set(key, value);
260
+ while (cache.size > limit)
261
+ cache.delete(cache.keys().next().value);
262
+ return { value, reused: false };
263
+ }
264
+ function trimCache(cache, limit) {
265
+ while (cache.size > limit)
266
+ cache.delete(cache.keys().next().value);
267
+ }
268
+ /**
269
+ * Rows of the streaming block that the visible window actually shows. Gap insertion happens
270
+ * after the selectable document, so live-block indices need no adjustment here.
271
+ */
272
+ function visibleLiveTailRows(liveTailStartRow, selectableDocumentRowCount, scrollTop, viewportHeight) {
273
+ if (liveTailStartRow === undefined || !Number.isSafeInteger(liveTailStartRow))
274
+ return 0;
275
+ const start = clamp(liveTailStartRow, 0, selectableDocumentRowCount);
276
+ const visibleStart = Math.max(start, scrollTop);
277
+ const visibleEnd = Math.min(selectableDocumentRowCount, scrollTop + viewportHeight);
278
+ return Math.max(0, visibleEnd - visibleStart);
279
+ }
280
+ function padViewportRowWithGutter(line, contentWidth, frameWidth) {
281
+ const boundedContentWidth = Math.min(Math.max(1, contentWidth), frameWidth);
282
+ const content = padRowPreservingBackground(line, boundedContentWidth);
283
+ const gutterWidth = frameWidth - boundedContentWidth;
284
+ if (gutterWidth === 0)
285
+ return content;
286
+ // Invariant: render no source glyph beyond contentWidth, but reuse the established
287
+ // row-padding background before stripping source foreground, links, and emphasis.
288
+ const gutterStyle = explicitBackgroundPaddingStyle(line);
289
+ return `${content}${GUTTER_DECORATION_RESET}${gutterStyle?.on ?? ""}${" ".repeat(gutterWidth)}${gutterStyle?.off ?? ""}`;
290
+ }
291
+ function explicitBackgroundPaddingStyle(line) {
292
+ const background = /\u001b\[(?:4[0-8]|10[0-7]|48;(?:2;\d+;\d+;\d+|5;\d+))m/.exec(line)?.[0];
293
+ return background === undefined ? null : { on: background, off: "\u001b[49m" };
294
+ }
295
+ function rowPaddingStyle(line) {
296
+ const background = explicitBackgroundPaddingStyle(line);
297
+ if (background !== null)
298
+ return background;
299
+ const reverse = /\u001b\[(?:7(?:;[0-9]+)*|[0-9;]*;7(?:;[0-9]+)*)m/.exec(line)?.[0];
300
+ return reverse === undefined ? null : { on: reverse, off: "\u001b[27m" };
301
+ }
302
+ function padRowPreservingBackground(line, width) {
303
+ const shown = displayWidth(line) > width ? truncateToWidth(line, width) : line;
304
+ const padding = Math.max(0, width - displayWidth(shown));
305
+ if (padding === 0)
306
+ return shown;
307
+ const style = rowPaddingStyle(line);
308
+ return style === null
309
+ ? shown + " ".repeat(padding)
310
+ : `${shown}${style.on}${" ".repeat(padding)}${style.off}`;
311
+ }
312
+ function clamp(value, minimum, maximum) {
313
+ return Math.min(Math.max(value, minimum), maximum);
314
+ }
@@ -0,0 +1,147 @@
1
+ import type { ScrollbarAppearance, ScrollbarGeometry, ScrollbarStyle } from "./scrollbar.js";
2
+ export interface TranscriptPromptAnchor {
3
+ readonly id: string;
4
+ readonly firstRow: number;
5
+ readonly lastRow: number;
6
+ /** The source first row, including its submitted-prompt styling. */
7
+ readonly sourceRow: string;
8
+ /** Optional source-styled row used only while prominent and not hovered. */
9
+ readonly prominentSourceRow?: string;
10
+ }
11
+ export interface TranscriptViewportConfig {
12
+ readonly scrollbarAppearance: ScrollbarAppearance;
13
+ readonly scrollbarStyle: ScrollbarStyle;
14
+ }
15
+ export interface TranscriptViewportTheme {
16
+ readonly track: (text: string) => string;
17
+ readonly thumb: (text: string, active: boolean) => string;
18
+ readonly sticky: (text: string, hovered: boolean) => string;
19
+ readonly quietSticky: (text: string) => string;
20
+ readonly bottomControl: (text: string, hovered: boolean) => string;
21
+ readonly selection: (line: string, from: number, to: number) => string;
22
+ }
23
+ export interface TranscriptViewportFrameInput {
24
+ readonly documentRows: readonly string[];
25
+ /** Optional paint-only transform; semantic selection and copying keep documentRows exact. */
26
+ readonly paintDocumentRow?: (row: string) => string;
27
+ /** Leading persistent document rows; later transient rows remain selectable visible-frame content. */
28
+ readonly selectableDocumentRowCount?: number;
29
+ /**
30
+ * First document row owned by the block that is currently streaming, if any. Rows from
31
+ * here to the end of the selectable document reflow together whenever that block
32
+ * re-renders, so the frame can declare how much damage is legitimately its own.
33
+ */
34
+ readonly liveTailStartRow?: number;
35
+ /** Final transient rows to bottom-align with unused viewport space while content fits. */
36
+ readonly bottomAlignedTailRowCount?: number;
37
+ readonly dockRows: readonly string[];
38
+ /** Zero-based dock rows admitted to frame copying, classified before dock composition. */
39
+ readonly copyableDockRows?: readonly {
40
+ readonly row: number;
41
+ readonly prompt?: "first" | "continuation";
42
+ }[];
43
+ readonly promptAnchors: readonly TranscriptPromptAnchor[];
44
+ readonly width: number;
45
+ readonly height: number;
46
+ /**
47
+ * Optional zero-based terminal row for the floating bottom control. The shell
48
+ * supplies a stable anchor above its editor group so transient status and
49
+ * notification rows cannot make the control jump vertically.
50
+ */
51
+ readonly bottomControlRow?: number;
52
+ /** Latest reported one-based terminal coordinates; bottom hover uses this frame's hit region. */
53
+ readonly pointerPosition?: {
54
+ readonly column: number;
55
+ readonly row: number;
56
+ };
57
+ /** Base-frame cells hidden by modal/replacement surfaces and therefore excluded from visual copy. */
58
+ readonly coveredCells?: readonly {
59
+ readonly row: number;
60
+ readonly columnStart: number;
61
+ readonly columnEnd: number;
62
+ }[];
63
+ /** Transient paint-only text that must not participate in frame allocation or copying. */
64
+ readonly frameOverlay?: {
65
+ /** One-based terminal row. */
66
+ readonly row: number;
67
+ readonly text: string;
68
+ };
69
+ readonly now?: number;
70
+ readonly theme?: TranscriptViewportTheme;
71
+ }
72
+ export interface TranscriptViewportHitRegions {
73
+ readonly viewportHeight: number;
74
+ readonly rail: {
75
+ readonly column: number;
76
+ /** One-based terminal row. The one-row top inset is intentionally excluded. */
77
+ readonly rowStart: number;
78
+ readonly trackHeight: number;
79
+ readonly geometry: ScrollbarGeometry;
80
+ } | null;
81
+ readonly sticky: {
82
+ readonly row: number;
83
+ readonly target: number;
84
+ readonly width: number;
85
+ } | null;
86
+ readonly bottom: {
87
+ readonly row: number;
88
+ readonly columnStart: number;
89
+ readonly columnEnd: number;
90
+ } | null;
91
+ /** Transient non-persistent document-tail rows currently visible in the viewport. */
92
+ readonly transientTail: readonly number[];
93
+ }
94
+ export interface TranscriptViewportFrameDescriptor {
95
+ readonly frameId: number;
96
+ readonly width: number;
97
+ readonly height: number;
98
+ readonly transcript: {
99
+ readonly rowStart: number;
100
+ readonly rowEnd: number;
101
+ } | null;
102
+ readonly dock: {
103
+ readonly rowStart: number;
104
+ readonly rowEnd: number;
105
+ } | null;
106
+ readonly previousDocumentRange: {
107
+ readonly start: number;
108
+ readonly end: number;
109
+ } | null;
110
+ readonly nextDocumentRange: {
111
+ readonly start: number;
112
+ readonly end: number;
113
+ };
114
+ readonly previousFollowingEnd: boolean | null;
115
+ readonly followingEnd: boolean;
116
+ /** Complete non-persistent suffix, including pending steering, alignment, and status rows. */
117
+ readonly transientRowCount: number;
118
+ /** Flexible rows inserted before the bottom-aligned live status while content fits. */
119
+ readonly transientAlignmentGapRows: number;
120
+ /** Bottom-aligned transient rows. */
121
+ readonly bottomAlignedTailRowCount: number;
122
+ /** Visible rows owned by the currently streaming transcript block. */
123
+ readonly liveTailRows: number;
124
+ readonly verticalShiftRows: number;
125
+ readonly safeVerticalShift: boolean;
126
+ /** Monotonic interaction revision used to reject stale selection evidence. */
127
+ readonly selectionRevision: number;
128
+ /** One-based terminal rows whose source or normalized selection range changed. */
129
+ readonly selectionDamagedRows: readonly number[];
130
+ readonly cause: "initial" | "steady" | "dock-input" | "follow-shift" | "detached" | "geometry-change";
131
+ }
132
+ export interface TranscriptViewportFrame {
133
+ readonly rows: readonly string[];
134
+ readonly contentWidth: number;
135
+ readonly scrollTop: number;
136
+ readonly maxScroll: number;
137
+ readonly followingEnd: boolean;
138
+ readonly hits: TranscriptViewportHitRegions;
139
+ readonly descriptor: TranscriptViewportFrameDescriptor;
140
+ /** Deterministic evidence for bounded selection-only row composition. */
141
+ readonly selectionDamage: {
142
+ readonly revision: number;
143
+ readonly recomputedRows: readonly number[];
144
+ readonly reusedRows: readonly number[];
145
+ readonly cacheEntries: number;
146
+ };
147
+ }