@timurproko/a1 0.1.8-dev.572 → 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.
Files changed (54) hide show
  1. package/dist/app/session-shell/prompt-chips.js +20 -28
  2. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  3. package/dist/app/session-shell/session-shell-root.js +63 -14
  4. package/dist/app/session-shell/session-shell.js +30 -2
  5. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  6. package/dist/app/session-shell/session-viewport-controller.js +5 -1
  7. package/dist/contracts/owned-ui/index.d.ts +2 -0
  8. package/dist/contracts/owned-ui/index.js +1 -0
  9. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  10. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  11. package/dist/contracts/owned-ui/validation.js +1 -1
  12. package/dist/integrations/pi/components/index.d.ts +1 -0
  13. package/dist/integrations/pi/components/index.js +1 -0
  14. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  15. package/dist/integrations/pi/components/modal-frame.js +110 -0
  16. package/dist/integrations/pi/components/models-dialog.js +19 -5
  17. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  18. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  19. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  20. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  21. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  22. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  23. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  24. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  25. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  26. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  27. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  28. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  29. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  30. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  31. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  32. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  33. package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -6
  34. package/dist/integrations/pi/engine/compaction-progress.js +10 -10
  35. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  36. package/dist/integrations/pi/engine/settings-port.js +1 -1
  37. package/dist/integrations/pi/tui-runtime/adapter.js +6 -2
  38. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  39. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  40. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  41. package/dist/native/darwin-arm64/manifest.json +1 -1
  42. package/dist/native/linux-x64/manifest.json +1 -1
  43. package/dist/native/win32-x64/manifest.json +2 -2
  44. package/dist/native/win32-x64/process-guardian.exe +0 -0
  45. package/dist/ui/components/dialog-panel.js +10 -6
  46. package/dist/ui/components/mouse.d.ts +2 -2
  47. package/dist/ui/components/mouse.js +2 -2
  48. package/dist/ui/components/transcript-viewport.d.ts +6 -0
  49. package/dist/ui/components/transcript-viewport.js +186 -27
  50. package/docs/architecture/ui-reference-provenance.md +9 -0
  51. package/docs/local-worktree-cleanup.md +3 -1
  52. package/docs/openspec-archive-automation.md +4 -0
  53. package/docs/validation.md +1 -1
  54. package/package.json +1 -1
@@ -4,7 +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: 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";
7
10
  const IDENTITY_ROW = (row) => row;
11
+ function documentSelectionAnchor(row, source) {
12
+ return { kind: "document", row, source, identity: stripAnsi(source).trimEnd() };
13
+ }
8
14
  const DEFAULT_CONFIG = {
9
15
  scrollbarAppearance: "auto",
10
16
  scrollbarStyle: "thin",
@@ -32,6 +38,7 @@ export class TranscriptViewport {
32
38
  #railDragging = false;
33
39
  #stickyHovered = false;
34
40
  #selection;
41
+ #selectionAnchors;
35
42
  #selectionClick;
36
43
  #selectionRevision = 0;
37
44
  #copyableSelection = false;
@@ -49,6 +56,8 @@ export class TranscriptViewport {
49
56
  #promptAnchors = [];
50
57
  #contentWidth = 0;
51
58
  #selectionWidth = 0;
59
+ #viewportHeight = 0;
60
+ #selectionRowAnchors = [];
52
61
  #frameId = 0;
53
62
  #frame = null;
54
63
  get scrollTop() { return this.#scrollTop; }
@@ -81,24 +90,29 @@ export class TranscriptViewport {
81
90
  get hasSelection() { return orderedTextSelection(this.#selection) !== undefined; }
82
91
  pressSelection(column, frameRow, now = Date.now()) {
83
92
  const frameHeight = this.#frame?.rows.length ?? 0;
84
- if (frameRow < 1 || frameRow > frameHeight || column < 1 || column > this.#selectionWidth)
93
+ const selectionWidth = frameRow <= (this.#frame?.hits.viewportHeight ?? 0)
94
+ ? this.#contentWidth
95
+ : this.#selectionWidth;
96
+ if (frameRow < 1 || frameRow > frameHeight || column < 1 || column > selectionWidth)
85
97
  return false;
86
98
  const line = frameRow - 1;
87
99
  const pressed = pressTextSelection({
88
100
  line,
89
101
  column,
90
- contentWidth: this.#selectionWidth,
102
+ contentWidth: selectionWidth,
91
103
  lineText: this.#selectionRows[line] ?? "",
92
104
  lineContent: usefulTextLineContent(this.#selectionRows[line] ?? ""),
93
105
  ...(this.#selectionClick === undefined ? {} : { previousClick: this.#selectionClick }),
94
106
  now,
95
107
  });
96
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 };
97
113
  this.#selectionClick = pressed.click;
98
114
  this.#selectionRevision += 1;
99
115
  this.#updateCopyableSelection();
100
- if (this.#copyableSelection)
101
- this.#followingEnd = false;
102
116
  return true;
103
117
  }
104
118
  extendSelection(column, frameRow, now = Date.now(), autoScroll = true) {
@@ -113,19 +127,26 @@ export class TranscriptViewport {
113
127
  else if (autoScroll && frameRow <= 1 && this.#scrollTop > 0)
114
128
  this.scrollBy(-1, now);
115
129
  const line = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
130
+ const selectionWidth = line < (this.#frame?.hits.viewportHeight ?? 0)
131
+ ? this.#contentWidth
132
+ : this.#selectionWidth;
116
133
  const targetColumn = selection.fullRow
117
- ? textSelectionLineExtendColumn(selection, line, this.#selectionWidth)
118
- : clamp(column, 1, this.#selectionWidth);
134
+ ? textSelectionLineExtendColumn(selection, line, selectionWidth)
135
+ : clamp(column, 1, selectionWidth);
119
136
  const point = selection.fullRow
120
137
  ? { line, column: targetColumn }
121
138
  : textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
122
139
  const next = extendTextSelection(selection, point);
123
140
  if (next !== selection) {
141
+ const headAnchor = this.#selectionAnchorAt(line);
142
+ if (headAnchor === undefined || this.#selectionAnchors === undefined) {
143
+ this.clearSelection();
144
+ return true;
145
+ }
124
146
  this.#selection = next;
147
+ this.#selectionAnchors = { ...this.#selectionAnchors, head: headAnchor };
125
148
  this.#selectionRevision += 1;
126
149
  this.#updateCopyableSelection();
127
- if (this.#copyableSelection)
128
- this.#followingEnd = false;
129
150
  }
130
151
  return true;
131
152
  }
@@ -133,6 +154,8 @@ export class TranscriptViewport {
133
154
  if (this.#selection?.selecting !== true)
134
155
  return false;
135
156
  this.#selection = releaseTextSelection(this.#selection);
157
+ if (this.#selection === undefined)
158
+ this.#selectionAnchors = undefined;
136
159
  this.#selectionRevision += 1;
137
160
  this.#updateCopyableSelection();
138
161
  return true;
@@ -141,12 +164,13 @@ export class TranscriptViewport {
141
164
  if (this.#selection === undefined)
142
165
  return false;
143
166
  this.#selection = undefined;
167
+ this.#selectionAnchors = undefined;
144
168
  this.#copyableSelection = false;
145
169
  this.#selectionRevision += 1;
146
170
  return true;
147
171
  }
148
172
  selectedText() {
149
- const selection = orderedTextSelection(this.#selection);
173
+ const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
150
174
  if (selection === undefined)
151
175
  return null;
152
176
  const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
@@ -240,8 +264,10 @@ export class TranscriptViewport {
240
264
  if (this.#selection !== undefined)
241
265
  this.#selectionRevision += 1;
242
266
  this.#selection = undefined;
267
+ this.#selectionAnchors = undefined;
243
268
  this.#copyableSelection = false;
244
269
  this.#selectionRows = [];
270
+ this.#selectionRowAnchors = [];
245
271
  this.#selectionClick = undefined;
246
272
  this.#paintedRowCache.clear();
247
273
  this.#baseRowCache.clear();
@@ -253,6 +279,7 @@ export class TranscriptViewport {
253
279
  this.#promptAnchors = [];
254
280
  this.#contentWidth = 0;
255
281
  this.#selectionWidth = 0;
282
+ this.#viewportHeight = 0;
256
283
  this.clearTransient();
257
284
  this.#frame = null;
258
285
  }
@@ -311,9 +338,10 @@ export class TranscriptViewport {
311
338
  this.#selectableDocumentRowCount = clamp(input.selectableDocumentRowCount ?? input.documentRows.length, 0, input.documentRows.length);
312
339
  this.#promptAnchors = input.promptAnchors;
313
340
  this.#contentWidth = contentWidth;
314
- // Invariant: controls reserve the rail column, but source text beneath it
315
- // remains reachable by a drag already owned by transcript selection.
341
+ // Invariant: dock rows remain selectable at full width; scrollable source rows
342
+ // use contentWidth and leave the gutter outside semantic selection.
316
343
  this.#selectionWidth = width;
344
+ this.#viewportHeight = viewportHeight;
317
345
  const paintDocumentRow = input.paintDocumentRow ?? IDENTITY_ROW;
318
346
  const cacheLimit = Math.max(32, viewportHeight * 6);
319
347
  trimCache(this.#paintedRowCache, cacheLimit);
@@ -348,8 +376,26 @@ export class TranscriptViewport {
348
376
  visible[0] = quiet && !this.#stickyHovered ? theme.quietSticky(sticky) : sticky;
349
377
  visibleSource[0] = sourceRow;
350
378
  }
351
- // Invariant: selection uses exact visible source rows; paint-only hyperlink guards stay rendered-only.
352
- this.#selectionRows = [...visibleSource, ...dock].slice(0, height);
379
+ // Invariant: selection uses exact visible source rows within the rendered
380
+ // content boundary; paint-only guards and the rail gutter stay presentation-only.
381
+ this.#selectionRows = [
382
+ ...visibleSource.map(row => truncateToWidth(row, contentWidth)),
383
+ ...dock,
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);
353
399
  const selectionPainterId = this.#functionId(theme.selection);
354
400
  // Concurrency: a painter may synchronously route input in tests or host integrations.
355
401
  // Label this frame with the exact selection snapshot it began composing.
@@ -394,17 +440,20 @@ export class TranscriptViewport {
394
440
  this.#updateCopyableSelection();
395
441
  for (let row = 0; row < frameRows.length; row += 1) {
396
442
  const painted = frameRows[row] ?? "";
397
- const range = selectionRangeForLine(orderedSelection, row, this.#selectionRows.length, width);
443
+ const rowSelectionWidth = row < viewportHeight ? contentWidth : width;
444
+ const range = selectionRangeForLine(orderedSelection, row, this.#selectionRows.length, rowSelectionWidth);
398
445
  const padded = row < viewportHeight || range !== null;
399
- const base = cachedString(this.#baseRowCache, `${width}\u0000${padded}\u0000${painted}`, cacheLimit, () => padded ? padRowPreservingBackground(painted, width) : painted);
446
+ const base = cachedString(this.#baseRowCache, `${width}\u0000${contentWidth}\u0000${row < viewportHeight}\u0000${padded}\u0000${painted}`, cacheLimit, () => row < viewportHeight
447
+ ? padViewportRowWithGutter(painted, contentWidth, width)
448
+ : padded ? padRowPreservingBackground(painted, width) : painted);
400
449
  let rowRecomputed = row < viewportHeight && paintRecomputedRows.has(row) || !base.reused;
401
450
  const rangeKey = range === null ? "-" : `${range.from}:${range.to}`;
402
451
  const selected = range === null
403
452
  ? { value: base.value, reused: true }
404
453
  : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
405
454
  rowRecomputed ||= !selected.reused;
406
- // Invariant: paint the rail after selection. Full-row selection reaches the terminal
407
- // edge, while the foreground thumb/track remains visible above that background.
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.
408
457
  let railCell = "";
409
458
  if (row < viewportHeight && presentation.visible && geometry !== null && row > 0) {
410
459
  const trackRow = row - 1;
@@ -412,11 +461,19 @@ export class TranscriptViewport {
412
461
  const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
413
462
  railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
414
463
  }
415
- const final = cachedString(this.#finalRowCache, `${width}\u0000${railCell}\u0000${selected.value}`, cacheLimit, () => railCell.length === 0 ? selected.value : overlaySpan(selected.value, width - 1, width, railCell, { inheritStartStyle: true }));
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
+ });
416
473
  rowRecomputed ||= !final.reused;
417
474
  frameRows[row] = final.value;
418
475
  (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
419
- 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}`;
420
477
  visibleStates.push(state);
421
478
  if (this.#previousVisibleStates[row] !== state)
422
479
  selectionDamagedRows.push(row + 1);
@@ -563,12 +620,95 @@ export class TranscriptViewport {
563
620
  this.#functionIds.set(value, id);
564
621
  return id;
565
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
+ }
566
688
  #updateCopyableSelection() {
567
- const selection = orderedTextSelection(this.#selection);
689
+ const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
568
690
  this.#copyableSelection = selection !== undefined
569
691
  && textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? "")).length > 0;
570
692
  }
571
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
+ }
572
712
  function selectionRangeForLine(selection, documentLine, selectableRowCount, width) {
573
713
  if (selection === undefined || documentLine >= selectableRowCount
574
714
  || documentLine < selection.start.line || documentLine > selection.end.line)
@@ -626,18 +766,37 @@ function governingPrompt(anchors, scrollTop) {
626
766
  }
627
767
  return result;
628
768
  }
769
+ function padViewportRowWithGutter(line, contentWidth, frameWidth) {
770
+ const boundedContentWidth = Math.min(Math.max(1, contentWidth), frameWidth);
771
+ const content = padRowPreservingBackground(line, boundedContentWidth);
772
+ const gutterWidth = frameWidth - boundedContentWidth;
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" };
790
+ }
629
791
  function padRowPreservingBackground(line, width) {
630
792
  const shown = displayWidth(line) > width ? truncateToWidth(line, width) : line;
631
793
  const padding = Math.max(0, width - displayWidth(shown));
632
794
  if (padding === 0)
633
795
  return shown;
634
- const background = /\u001b\[(?:4[0-8]|10[0-7]|48;(?:2;\d+;\d+;\d+|5;\d+))m/.exec(line)?.[0];
635
- const reverse = /\u001b\[(?:7(?:;[0-9]+)*|[0-9;]*;7(?:;[0-9]+)*)m/.exec(line)?.[0];
636
- if (background)
637
- return `${shown}${background}${" ".repeat(padding)}\u001b[49m`;
638
- if (reverse)
639
- return `${shown}${reverse}${" ".repeat(padding)}\u001b[27m`;
640
- 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}`;
641
800
  }
642
801
  export function assertTranscriptViewportFrameDescriptor(descriptor) {
643
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.572",
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",