@timurproko/a1 0.1.8-dev.573 → 0.1.8-dev.581

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.
@@ -376,6 +376,7 @@ export class OwnedUiSessionShellRoot {
376
376
  }
377
377
  const height = Math.max(0, this.#componentRuntime.getRows());
378
378
  const dock = this.#renderDockLayout(width);
379
+ const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
379
380
  // Invariant: auto/always own one stable final-column gutter. Every scrollable
380
381
  // surface ends before it; the dock remains full-width and hidden mode returns it.
381
382
  const documentWidth = this.#viewportController.config.scrollbarAppearance === "hidden"
@@ -386,7 +387,9 @@ export class OwnedUiSessionShellRoot {
386
387
  const statusRows = this.#renderStatus(documentWidth);
387
388
  const transientSignature = transientRowsSignature(steeringRows, statusRows);
388
389
  const snapshot = this.#visibleViewportSnapshot;
389
- const dockInputCandidate = this.#dockInputCandidate;
390
+ // Invariant: paint-only feedback may cover either viewport or dock cells, so
391
+ // ordinary dock-only reuse cannot safely omit its overlay composition.
392
+ const dockInputCandidate = this.#dockInputCandidate && copyAcknowledgement === undefined;
390
393
  // Concurrency: input may change after a keyboard receipt but before its paint.
391
394
  // Capture only the live inputs this composition will actually represent.
392
395
  const inputSurface = this.#inputSurface;
@@ -444,6 +447,12 @@ export class OwnedUiSessionShellRoot {
444
447
  // Invariant: the control belongs immediately above the complete dock and
445
448
  // floats over transient viewport content rather than consuming a dock row.
446
449
  bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
450
+ ...(copyAcknowledgement === undefined || height <= 0 ? {} : {
451
+ frameOverlay: {
452
+ row: Math.max(1, Math.min(height, dockStartRow + dock.inputOffset - 1)),
453
+ text: copyAcknowledgement,
454
+ },
455
+ }),
447
456
  theme: this.#viewportTheme,
448
457
  });
449
458
  this.#fullViewportCompositions += 1;
@@ -559,24 +568,24 @@ export class OwnedUiSessionShellRoot {
559
568
  const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
560
569
  const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
561
570
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
562
- const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
563
571
  const input = this.#inputSurface.render(width);
564
572
  // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
565
573
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
566
574
  const belowWidgets = this.#renderWidgets("belowEditor", width);
567
575
  const footer = this.#renderFooter(width);
568
- const rowsWithoutTransient = [...aboveWidgets, ...copyAcknowledgement, ...input, ...belowWidgets, ...footer];
576
+ const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
577
+ const inputOffset = transientRows.length + aboveWidgets.length;
569
578
  return {
570
579
  rows: [...transientRows, ...rowsWithoutTransient],
571
- editorOffset: transientRows.length + aboveWidgets.length + copyAcknowledgement.length + (body?.rowOffset ?? 0),
580
+ inputOffset,
581
+ editorOffset: inputOffset + (body?.rowOffset ?? 0),
572
582
  inputRows: body?.rowCount ?? input.length,
573
583
  };
574
584
  }
575
585
  #renderCopyAcknowledgement(width) {
576
586
  if (this.#copyAcknowledgement === undefined || width <= 0)
577
- return [];
578
- const text = piShellTruncateToWidth(this.#copyAcknowledgement, width);
579
- return [`${" ".repeat(Math.max(0, width - piShellVisibleWidth(text)))}${piTheme().fg("accent", text)}`];
587
+ return undefined;
588
+ return piTheme().fg("accent", piShellTruncateToWidth(this.#copyAcknowledgement, width));
580
589
  }
581
590
  #clearCopyAcknowledgement(requestRender) {
582
591
  if (this.#copyAcknowledgementTimer !== undefined)
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-24T07:17:37.739Z",
8
+ "builtAt": "2026-09-24T10:58:51.409Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-24T07:17:22.316Z",
8
+ "builtAt": "2026-09-24T10:58:49.680Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-24T07:18:02.282Z",
8
+ "builtAt": "2026-09-24T10:59:15.858Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "c7d92e5f9cec0d36ec4482b25e518464acb4e1bbc5bdd6701f1e04ada7775950",
11
+ "sha256": "2829ba3eb49302e6f58a4a11fe7d9597d0d8c46301328e034e41fd9e240f1677",
12
12
  "size": 178176
13
13
  },
14
14
  "provenance": {
@@ -56,6 +56,12 @@ export interface TranscriptViewportFrameInput {
56
56
  readonly columnStart: number;
57
57
  readonly columnEnd: number;
58
58
  }[];
59
+ /** Transient paint-only text that must not participate in frame allocation or copying. */
60
+ readonly frameOverlay?: {
61
+ /** One-based terminal row. */
62
+ readonly row: number;
63
+ readonly text: string;
64
+ };
59
65
  readonly now?: number;
60
66
  readonly theme?: TranscriptViewportTheme;
61
67
  }
@@ -4,10 +4,13 @@ import { backgroundSgrSpan, overlaySpan } from "./spans.js";
4
4
  import { faint, truncateToWidth, displayWidth, stripAnsi } from "./text.js";
5
5
  import { extendTextSelection, orderedTextSelection, pressTextSelection, releaseTextSelection, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, usefulTextLineContent, } from "./text-selection.js";
6
6
  const CONTROL_STYLE_RESET = "\u001b]8;;\u001b\\\u001b[0m";
7
- // Invariant: the scrollbar gutter is presentation chrome: no source link, emphasis,
8
- // selection, or background may cross from the content cell beside it.
9
- const GUTTER_STYLE_RESET = CONTROL_STYLE_RESET;
7
+ // Invariant: scrollbar chrome closes source links and decoration without clearing
8
+ // the row surface painted beneath its dedicated, non-semantic gutter.
9
+ const GUTTER_DECORATION_RESET = "\u001b]8;;\u001b\\\u001b[22;23;24;25;27;28;29;39;54;55m";
10
10
  const IDENTITY_ROW = (row) => row;
11
+ function documentSelectionAnchor(row, source) {
12
+ return { kind: "document", row, source, identity: stripAnsi(source).trimEnd() };
13
+ }
11
14
  const DEFAULT_CONFIG = {
12
15
  scrollbarAppearance: "auto",
13
16
  scrollbarStyle: "thin",
@@ -35,6 +38,7 @@ export class TranscriptViewport {
35
38
  #railDragging = false;
36
39
  #stickyHovered = false;
37
40
  #selection;
41
+ #selectionAnchors;
38
42
  #selectionClick;
39
43
  #selectionRevision = 0;
40
44
  #copyableSelection = false;
@@ -52,6 +56,8 @@ export class TranscriptViewport {
52
56
  #promptAnchors = [];
53
57
  #contentWidth = 0;
54
58
  #selectionWidth = 0;
59
+ #viewportHeight = 0;
60
+ #selectionRowAnchors = [];
55
61
  #frameId = 0;
56
62
  #frame = null;
57
63
  get scrollTop() { return this.#scrollTop; }
@@ -100,11 +106,13 @@ export class TranscriptViewport {
100
106
  now,
101
107
  });
102
108
  this.#selection = pressed.selection;
109
+ const rowAnchor = this.#selectionAnchorAt(line);
110
+ this.#selectionAnchors = pressed.selection === undefined || rowAnchor === undefined
111
+ ? undefined
112
+ : { anchor: rowAnchor, head: rowAnchor };
103
113
  this.#selectionClick = pressed.click;
104
114
  this.#selectionRevision += 1;
105
115
  this.#updateCopyableSelection();
106
- if (this.#copyableSelection)
107
- this.#followingEnd = false;
108
116
  return true;
109
117
  }
110
118
  extendSelection(column, frameRow, now = Date.now(), autoScroll = true) {
@@ -130,11 +138,15 @@ export class TranscriptViewport {
130
138
  : textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
131
139
  const next = extendTextSelection(selection, point);
132
140
  if (next !== selection) {
141
+ const headAnchor = this.#selectionAnchorAt(line);
142
+ if (headAnchor === undefined || this.#selectionAnchors === undefined) {
143
+ this.clearSelection();
144
+ return true;
145
+ }
133
146
  this.#selection = next;
147
+ this.#selectionAnchors = { ...this.#selectionAnchors, head: headAnchor };
134
148
  this.#selectionRevision += 1;
135
149
  this.#updateCopyableSelection();
136
- if (this.#copyableSelection)
137
- this.#followingEnd = false;
138
150
  }
139
151
  return true;
140
152
  }
@@ -142,6 +154,8 @@ export class TranscriptViewport {
142
154
  if (this.#selection?.selecting !== true)
143
155
  return false;
144
156
  this.#selection = releaseTextSelection(this.#selection);
157
+ if (this.#selection === undefined)
158
+ this.#selectionAnchors = undefined;
145
159
  this.#selectionRevision += 1;
146
160
  this.#updateCopyableSelection();
147
161
  return true;
@@ -150,12 +164,13 @@ export class TranscriptViewport {
150
164
  if (this.#selection === undefined)
151
165
  return false;
152
166
  this.#selection = undefined;
167
+ this.#selectionAnchors = undefined;
153
168
  this.#copyableSelection = false;
154
169
  this.#selectionRevision += 1;
155
170
  return true;
156
171
  }
157
172
  selectedText() {
158
- const selection = orderedTextSelection(this.#selection);
173
+ const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
159
174
  if (selection === undefined)
160
175
  return null;
161
176
  const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
@@ -249,8 +264,10 @@ export class TranscriptViewport {
249
264
  if (this.#selection !== undefined)
250
265
  this.#selectionRevision += 1;
251
266
  this.#selection = undefined;
267
+ this.#selectionAnchors = undefined;
252
268
  this.#copyableSelection = false;
253
269
  this.#selectionRows = [];
270
+ this.#selectionRowAnchors = [];
254
271
  this.#selectionClick = undefined;
255
272
  this.#paintedRowCache.clear();
256
273
  this.#baseRowCache.clear();
@@ -262,6 +279,7 @@ export class TranscriptViewport {
262
279
  this.#promptAnchors = [];
263
280
  this.#contentWidth = 0;
264
281
  this.#selectionWidth = 0;
282
+ this.#viewportHeight = 0;
265
283
  this.clearTransient();
266
284
  this.#frame = null;
267
285
  }
@@ -323,6 +341,7 @@ export class TranscriptViewport {
323
341
  // Invariant: dock rows remain selectable at full width; scrollable source rows
324
342
  // use contentWidth and leave the gutter outside semantic selection.
325
343
  this.#selectionWidth = width;
344
+ this.#viewportHeight = viewportHeight;
326
345
  const paintDocumentRow = input.paintDocumentRow ?? IDENTITY_ROW;
327
346
  const cacheLimit = Math.max(32, viewportHeight * 6);
328
347
  trimCache(this.#paintedRowCache, cacheLimit);
@@ -363,6 +382,20 @@ export class TranscriptViewport {
363
382
  ...visibleSource.map(row => truncateToWidth(row, contentWidth)),
364
383
  ...dock,
365
384
  ].slice(0, height);
385
+ const visibleAnchors = Array.from({ length: viewportHeight }, (_value, row) => {
386
+ const documentRow = this.#scrollTop + row;
387
+ return documentRow < documentRows.length
388
+ ? documentSelectionAnchor(documentRow, documentRows[documentRow] ?? "")
389
+ : { kind: "screen", row, source: this.#selectionRows[row] ?? "" };
390
+ });
391
+ if (stickyActive && governing !== null && visibleAnchors.length > 0) {
392
+ visibleAnchors[0] = documentSelectionAnchor(governing.firstRow, documentRows[governing.firstRow] ?? governing.sourceRow);
393
+ }
394
+ this.#selectionRowAnchors = [
395
+ ...visibleAnchors,
396
+ ...dock.map((_row, index) => ({ kind: "dock", fromBottom: dock.length - index - 1 })),
397
+ ].slice(0, height);
398
+ this.#projectSelection(documentRows, dock, height);
366
399
  const selectionPainterId = this.#functionId(theme.selection);
367
400
  // Concurrency: a painter may synchronously route input in tests or host integrations.
368
401
  // Label this frame with the exact selection snapshot it began composing.
@@ -419,8 +452,8 @@ export class TranscriptViewport {
419
452
  ? { value: base.value, reused: true }
420
453
  : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
421
454
  rowRecomputed ||= !selected.reused;
422
- // Invariant: paint the rail after selection, inside its neutral gutter.
423
- // Source and full-row selection end at the preceding content cell.
455
+ // Invariant: paint the rail after selection over the gutter's continued row
456
+ // background. Source content and selection still end at the preceding cell.
424
457
  let railCell = "";
425
458
  if (row < viewportHeight && presentation.visible && geometry !== null && row > 0) {
426
459
  const trackRow = row - 1;
@@ -428,11 +461,19 @@ export class TranscriptViewport {
428
461
  const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
429
462
  railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
430
463
  }
431
- const final = cachedString(this.#finalRowCache, `${width}\u0000${railCell}\u0000${selected.value}`, cacheLimit, () => railCell.length === 0 ? selected.value : overlaySpan(selected.value, width - 1, width, `${GUTTER_STYLE_RESET}${railCell}`));
464
+ const overlayLimit = row < viewportHeight ? contentWidth : width;
465
+ const overlayText = input.frameOverlay?.row === row + 1
466
+ ? truncateToWidth(input.frameOverlay.text, overlayLimit)
467
+ : "";
468
+ const overlayWidth = displayWidth(overlayText);
469
+ const final = cachedString(this.#finalRowCache, `${width}\u0000${railCell}\u0000${overlayText}\u0000${selected.value}`, cacheLimit, () => {
470
+ const withRail = railCell.length === 0 ? selected.value : overlaySpan(selected.value, width - 1, width, `${GUTTER_DECORATION_RESET}${railCell}`, { inheritStartStyle: true });
471
+ return overlayWidth === 0 ? withRail : overlaySpan(padRowPreservingBackground(withRail, width), overlayLimit - overlayWidth, overlayLimit, `${CONTROL_STYLE_RESET}${overlayText}`);
472
+ });
432
473
  rowRecomputed ||= !final.reused;
433
474
  frameRows[row] = final.value;
434
475
  (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
435
- const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}`;
476
+ const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}\u0000${overlayText}`;
436
477
  visibleStates.push(state);
437
478
  if (this.#previousVisibleStates[row] !== state)
438
479
  selectionDamagedRows.push(row + 1);
@@ -579,12 +620,95 @@ export class TranscriptViewport {
579
620
  this.#functionIds.set(value, id);
580
621
  return id;
581
622
  }
623
+ #selectionAnchorAt(line) {
624
+ if (line < 0 || line >= this.#selectionRows.length)
625
+ return undefined;
626
+ if (line < this.#viewportHeight) {
627
+ const existing = this.#selectionRowAnchors[line];
628
+ if (this.#frame?.scrollTop === this.#scrollTop)
629
+ return existing;
630
+ // Concurrency: selection edge auto-scroll updates scrollTop before the next frame is
631
+ // composed. Resolve ordinary viewport rows from that latest position instead of stale paint.
632
+ const documentRow = this.#scrollTop + line;
633
+ if (documentRow >= 0 && documentRow < this.#documentRows.length) {
634
+ return documentSelectionAnchor(documentRow, this.#documentRows[documentRow] ?? "");
635
+ }
636
+ }
637
+ return this.#selectionRowAnchors[line];
638
+ }
639
+ #projectSelection(documentRows, dockRows, height) {
640
+ const selection = this.#selection;
641
+ const anchors = this.#selectionAnchors;
642
+ if (selection === undefined || anchors === undefined)
643
+ return;
644
+ const project = (anchor) => {
645
+ if (anchor.kind === "dock") {
646
+ const dockIndex = dockRows.length - anchor.fromBottom - 1;
647
+ return dockIndex < 0 || dockIndex >= dockRows.length
648
+ ? undefined
649
+ : { line: height - dockRows.length + dockIndex, anchor };
650
+ }
651
+ if (anchor.kind === "screen") {
652
+ return anchor.row < 0 || anchor.row >= this.#selectionRows.length
653
+ || this.#selectionRows[anchor.row] !== anchor.source
654
+ ? undefined
655
+ : { line: anchor.row, anchor };
656
+ }
657
+ if (anchor.row >= 0 && anchor.row < documentRows.length
658
+ && stripAnsi(documentRows[anchor.row] ?? "").trimEnd() === anchor.identity) {
659
+ const current = documentSelectionAnchor(anchor.row, documentRows[anchor.row] ?? "");
660
+ const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document"
661
+ && candidate.row === current.row && candidate.identity === current.identity);
662
+ return { line: visible >= 0 ? visible : current.row - this.#scrollTop, anchor: current };
663
+ }
664
+ // Invariant: reflow may move a retained source row, but remapping is allowed only when
665
+ // that exact source has one visible identity. Ambiguous or off-screen replacement clears it.
666
+ const visibleMatches = this.#selectionRowAnchors
667
+ .map((candidate, line) => ({ candidate, line }))
668
+ .filter((entry) => entry.candidate?.kind === "document" && entry.candidate.identity === anchor.identity);
669
+ return visibleMatches.length === 1
670
+ ? { line: visibleMatches[0].line, anchor: visibleMatches[0].candidate }
671
+ : undefined;
672
+ };
673
+ const projectedAnchor = project(anchors.anchor);
674
+ const projectedHead = project(anchors.head);
675
+ if (projectedAnchor === undefined || projectedHead === undefined) {
676
+ this.clearSelection();
677
+ return;
678
+ }
679
+ this.#selectionAnchors = { anchor: projectedAnchor.anchor, head: projectedHead.anchor };
680
+ if (selection.anchor.line === projectedAnchor.line && selection.head.line === projectedHead.line)
681
+ return;
682
+ this.#selection = {
683
+ ...selection,
684
+ anchor: { ...selection.anchor, line: projectedAnchor.line },
685
+ head: { ...selection.head, line: projectedHead.line },
686
+ };
687
+ }
582
688
  #updateCopyableSelection() {
583
- const selection = orderedTextSelection(this.#selection);
689
+ const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
584
690
  this.#copyableSelection = selection !== undefined
585
691
  && textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? "")).length > 0;
586
692
  }
587
693
  }
694
+ function visibleTextSelection(selection, rowCount) {
695
+ if (selection === undefined || rowCount <= 0 || selection.end.line < 0 || selection.start.line >= rowCount)
696
+ return undefined;
697
+ const startLine = Math.max(0, selection.start.line);
698
+ const endLine = Math.min(rowCount - 1, selection.end.line);
699
+ return {
700
+ start: {
701
+ line: startLine,
702
+ column: selection.start.line < 0 ? 0 : selection.start.column,
703
+ },
704
+ end: {
705
+ line: endLine,
706
+ column: selection.end.line >= rowCount ? Number.MAX_SAFE_INTEGER : selection.end.column,
707
+ },
708
+ ...(selection.fullRow ? { fullRow: true } : {}),
709
+ ...(selection.throughFinalColumn ? { throughFinalColumn: true } : {}),
710
+ };
711
+ }
588
712
  function selectionRangeForLine(selection, documentLine, selectableRowCount, width) {
589
713
  if (selection === undefined || documentLine >= selectableRowCount
590
714
  || documentLine < selection.start.line || documentLine > selection.end.line)
@@ -646,20 +770,33 @@ function padViewportRowWithGutter(line, contentWidth, frameWidth) {
646
770
  const boundedContentWidth = Math.min(Math.max(1, contentWidth), frameWidth);
647
771
  const content = padRowPreservingBackground(line, boundedContentWidth);
648
772
  const gutterWidth = frameWidth - boundedContentWidth;
649
- return gutterWidth === 0 ? content : `${content}${GUTTER_STYLE_RESET}${" ".repeat(gutterWidth)}`;
773
+ if (gutterWidth === 0)
774
+ return content;
775
+ // Invariant: render no source glyph beyond contentWidth, but reuse the established
776
+ // row-padding background before stripping source foreground, links, and emphasis.
777
+ const gutterStyle = explicitBackgroundPaddingStyle(line);
778
+ return `${content}${GUTTER_DECORATION_RESET}${gutterStyle?.on ?? ""}${" ".repeat(gutterWidth)}${gutterStyle?.off ?? ""}`;
779
+ }
780
+ function explicitBackgroundPaddingStyle(line) {
781
+ const background = /\u001b\[(?:4[0-8]|10[0-7]|48;(?:2;\d+;\d+;\d+|5;\d+))m/.exec(line)?.[0];
782
+ return background === undefined ? null : { on: background, off: "\u001b[49m" };
783
+ }
784
+ function rowPaddingStyle(line) {
785
+ const background = explicitBackgroundPaddingStyle(line);
786
+ if (background !== null)
787
+ return background;
788
+ const reverse = /\u001b\[(?:7(?:;[0-9]+)*|[0-9;]*;7(?:;[0-9]+)*)m/.exec(line)?.[0];
789
+ return reverse === undefined ? null : { on: reverse, off: "\u001b[27m" };
650
790
  }
651
791
  function padRowPreservingBackground(line, width) {
652
792
  const shown = displayWidth(line) > width ? truncateToWidth(line, width) : line;
653
793
  const padding = Math.max(0, width - displayWidth(shown));
654
794
  if (padding === 0)
655
795
  return shown;
656
- const background = /\u001b\[(?:4[0-8]|10[0-7]|48;(?:2;\d+;\d+;\d+|5;\d+))m/.exec(line)?.[0];
657
- const reverse = /\u001b\[(?:7(?:;[0-9]+)*|[0-9;]*;7(?:;[0-9]+)*)m/.exec(line)?.[0];
658
- if (background)
659
- return `${shown}${background}${" ".repeat(padding)}\u001b[49m`;
660
- if (reverse)
661
- return `${shown}${reverse}${" ".repeat(padding)}\u001b[27m`;
662
- return shown + " ".repeat(padding);
796
+ const style = rowPaddingStyle(line);
797
+ return style === null
798
+ ? shown + " ".repeat(padding)
799
+ : `${shown}${style.on}${" ".repeat(padding)}${style.off}`;
663
800
  }
664
801
  export function assertTranscriptViewportFrameDescriptor(descriptor) {
665
802
  if (!Number.isSafeInteger(descriptor.frameId) || descriptor.frameId < 1)
@@ -91,6 +91,15 @@ its own `core` facade layer; A1 is a product, so the port adapts imports and kee
91
91
  as styling callbacks rather than imports or literal colours. Engine producers remain semantic;
92
92
  pinned presentation keeps its three periods; and the source-synchronized Pi status indicators,
93
93
  installed packages, `a1 pi`, and vanilla Pi remain untouched.
94
+ - **Surface-relative frame selection and layout-neutral copy feedback.** Bare A1's complete-frame
95
+ selection extends the reference transcript selector across scrollable document and pinned dock
96
+ surfaces. Selection endpoints retain those surface identities: followed agent output carries its
97
+ highlight with the source row, while footer, status, prompt, widget, and notice selection remains
98
+ pinned. The copied-character acknowledgement is final-frame paint over the existing row above the
99
+ input, not a dock child, so appearing, replacement, and expiry do not reallocate the viewport or
100
+ move editor, footer, controls, or selection. Bounded row provenance clears ambiguous reflow rather
101
+ than transferring selection to unrelated terminal cells. The comparison profile retains pinned Pi
102
+ selection and notification behavior.
94
103
  - **Keyboard presentation scheduling.** Pinned components, semantic key handling, and the
95
104
  comparison profiles stay unchanged. Bare A1 adds one A1-owned event-loop-turn coordinator
96
105
  before its fullscreen root and may omit only superseded visual states after every delivery
@@ -24,7 +24,9 @@ node scripts/governance/local-worktree-cleanup.mjs sweep --repo D:/Git/a1
24
24
 
25
25
  ### Nothing left to remove
26
26
 
27
- A released entry whose evidence can never verify (for example `delivery-content-drift` after a stale-base merge, or `source-association` for a PR without an `openspec-implementation` fence) would otherwise be re-verified and reported `blocked` on every sweep. When such an entry's worktree path is gone, Git holds no live row for it (its own dangling row is retired), and its local topic ref is absent, the sweep and `complete` ask GitHub only whether the pull request merged into `develop` in this repository and then mark the journal `done` with `retired-nothing-left` and the original evidence reason. Nothing is deleted; a present path, row, or ref keeps the entry blocked, an unmerged PR keeps it `pending` or `awaiting-discard`, a deferred failure such as `remote-budget` is never treated as unverifiable, and preview retires nothing. For an all-absent entry whose PR is not merged, the maintainer runs the explicit form, which reads nothing from GitHub and deletes nothing:
27
+ A released entry whose evidence can never verify (for example `delivery-content-drift` after a stale-base merge, or `source-association` for a PR without an `openspec-implementation` fence) would otherwise be re-verified and reported `blocked` on every sweep. One narrow exception repairs an integrated unassociated delivery without rewriting its merged body: current `develop` must contain exactly one archived `association-repair.json` for the registered change, naming the exact original PR/head/merge and successful validation run, and its named corrective version-3 PR must itself have authorized manual merge, current exact-head CI, synchronized specs, a valid archive, and absent original/corrective remote refs. Only then does cleanup continue through all ordinary worktree identity, ownership, cleanliness, disposable-content, non-force removal, journaling, and local-ref safeguards. A mismatch remains blocked and generic merge/archive evidence never substitutes.
28
+
29
+ When no valid corrective association exists and such an entry's worktree path is gone, Git holds no live row for it (its own dangling row is retired), and its local topic ref is absent, the sweep and `complete` ask GitHub only whether the pull request merged into `develop` in this repository and then mark the journal `done` with `retired-nothing-left` and the original evidence reason. Nothing is deleted; a present path, row, or ref keeps the entry blocked, an unmerged PR keeps it `pending` or `awaiting-discard`, a deferred failure such as `remote-budget` is never treated as unverifiable, and preview retires nothing. For an all-absent entry whose PR is not merged, the maintainer runs the explicit form, which reads nothing from GitHub and deletes nothing:
28
30
 
29
31
  ```bash
30
32
  node scripts/governance/local-worktree-cleanup.mjs forget --repo D:/Git/a1 --id REGISTRATION_ID --confirm-nothing-left
@@ -148,6 +148,10 @@ The commit is pushed with a lease on the head the run read, so a developer push
148
148
 
149
149
  Because the branch gains commits from the archive App, pull before pushing. A rebase that drops them is harmless: the next push is reconciled from whatever the head contains. Do not revert a finalization commit to make a fix; push the fix and let the workflow re-finalize. When the workflow fails, its summary names the finalization code (`tasks-incomplete`, `acceptance-*`, `delivery-known-gaps`, `openspec-operation`, `finalization-merge-conflict`, ...), nothing is pushed, and `Finalized delivery validation` on the unfinalized head reports that automated finalization is pending.
150
150
 
151
+ A missing fence is not an ordinary ready PR when immutable base/head evidence shows that the candidate introduces or restores an active change, or mixes active-change edits with code or operational paths. Base-controlled readiness and finalization both fail with `missing-implementation-association`; deleting editable body metadata cannot bypass finalization. Existing active-change documentation-only revisions and ordinary code PRs without active delivery paths retain their established routes.
152
+
153
+ If an unassociated implementation has already integrated, do not edit its merged body or publish an archive-only follow-up. A corrective implementation-bound PR for the same still-active change must add the prevention fix and an exact `association-repair.json` record naming the original PR/head/merge/validation and the corrective PR. Normal version-3 finalization then synchronizes and archives the change; the corrective PR's authorized manual merge accepts that repair without pretending the original PR was finalized retroactively.
154
+
151
155
  ## Finalization command
152
156
 
153
157
  The local command remains available for inspection or when a developer prefers to finalize before marking the PR ready; the workflow then verifies the head and pushes nothing. It has inspection mode by default and an explicit `--write` mode. It never commits, pushes, edits GitHub, marks a PR ready, or merges. Use a temporary body file so the operation can update exact version-3 paths without mutating remote PR state:
@@ -94,7 +94,7 @@ Download these artifacts from the exact workflow run:
94
94
 
95
95
  For selected implementation candidates, record focused and pre-finalization full observations before finalization. Keep final head/run/selection identity in Actions and handoff rather than adding a post-success commit that invalidates that evidence. A successful selected PR run replaces an additional mandatory dispatch; standalone runs remain diagnostic, and numbered-package nightly recovery remains independent.
96
96
 
97
- Draft pull requests run no Development test suites. A ready version-3 PR whose body still identifies an active change defers testing and emits no protected aggregate while `OpenSpec finalization` publishes the candidate. After finalization records the archive and acceptance manifest, the ordinary exact-head workflow runs selected product/governance lanes and `Finalized delivery validation` in parallel, then emits the stable protected `Development validation required` aggregate only when both authorities succeed. Green CI enables maintainer review and manual merge but does not claim human acceptance or merge automatically. The finalization fence update is the validation trigger, so no pre-finalization test run or later lifecycle body edit is required. Any subsequent eligible implementation commit changes the head and reruns applicable validation; any body change reruns readiness and finalized-record validation and must continue to match the committed manifest. Legacy acceptance-record-only PRs retain their separate trusted `Acceptance record validation` route. Queue availability remains explicitly unavailable inside a runner and is calculated from the Actions API during final run analysis rather than guessed.
97
+ Draft pull requests run no Development test suites. A ready version-3 PR whose body still identifies an active change defers testing and emits no protected aggregate while `OpenSpec finalization` publishes the candidate. A ready unassociated PR also fails closed when immutable base/head evidence shows that it introduces or restores active delivery content, or combines active-change edits with code or operational paths; it does not fall through to ordinary validation merely because its body fence is absent. Existing active-change documentation-only revisions and ordinary code PRs without active delivery paths retain their established routes. After finalization records the archive and acceptance manifest, the ordinary exact-head workflow runs selected product/governance lanes and `Finalized delivery validation` in parallel, then emits the stable protected `Development validation required` aggregate only when both authorities succeed. Green CI enables maintainer review and manual merge but does not claim human acceptance or merge automatically. The finalization fence update is the validation trigger, so no pre-finalization test run or later lifecycle body edit is required. Any subsequent eligible implementation commit changes the head and reruns applicable validation; any body change reruns readiness and finalized-record validation and must continue to match the committed manifest. Legacy acceptance-record-only PRs retain their separate trusted `Acceptance record validation` route. Queue availability remains explicitly unavailable inside a runner and is calculated from the Actions API during final run analysis rather than guessed.
98
98
 
99
99
  ## Rollback
100
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.573",
3
+ "version": "0.1.8-dev.581",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",