@timurproko/a1 0.1.8-dev.560 → 0.1.8-dev.572

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 (45) hide show
  1. package/README.md +13 -11
  2. package/bin/guardian.js +5 -0
  3. package/dist/app/session-shell/session-shell-root.js +13 -8
  4. package/dist/app/session-shell/session-shell.js +14 -13
  5. package/dist/app/session-shell/session-viewport-controller.js +48 -30
  6. package/dist/cli/dispatch.d.ts +3 -1
  7. package/dist/cli/dispatch.js +56 -56
  8. package/dist/cli/package-messages.d.ts +4 -3
  9. package/dist/cli/package-messages.js +23 -11
  10. package/dist/foundation/launch-guardian/main.js +1 -2
  11. package/dist/foundation/lifecycle/model.d.ts +1 -1
  12. package/dist/foundation/release/bootstrap.js +72 -29
  13. package/dist/foundation/release/cohort-state.d.ts +2 -0
  14. package/dist/foundation/release/cohort-state.js +35 -0
  15. package/dist/foundation/release/index.d.ts +1 -0
  16. package/dist/foundation/release/index.js +1 -0
  17. package/dist/foundation/release/ordinary-launch-selection.d.ts +14 -0
  18. package/dist/foundation/release/ordinary-launch-selection.js +15 -0
  19. package/dist/foundation/supervision/server.js +15 -4
  20. package/dist/integrations/pi/components/shell-footer-status.d.ts +4 -3
  21. package/dist/integrations/pi/components/shell-footer-status.js +53 -15
  22. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -1
  23. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  24. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +3 -2
  25. package/dist/integrations/pi/engine/compaction-progress.d.ts +1 -1
  26. package/dist/integrations/pi/engine/compaction-progress.js +1 -0
  27. package/dist/integrations/pi/engine/repository-pr.d.ts +3 -3
  28. package/dist/integrations/pi/engine/repository-pr.js +6 -6
  29. package/dist/integrations/pi/engine/session-runtime.js +5 -4
  30. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  31. package/dist/integrations/pi/tui-runtime/adapter.js +5 -0
  32. package/dist/native/darwin-arm64/manifest.json +1 -1
  33. package/dist/native/linux-x64/manifest.json +1 -1
  34. package/dist/native/win32-x64/manifest.json +3 -3
  35. package/dist/native/win32-x64/process-guardian.exe +0 -0
  36. package/dist/ui/components/index.d.ts +2 -1
  37. package/dist/ui/components/index.js +1 -1
  38. package/dist/ui/components/progress-status.d.ts +9 -2
  39. package/dist/ui/components/progress-status.js +24 -3
  40. package/dist/ui/components/transcript-viewport.d.ts +9 -3
  41. package/dist/ui/components/transcript-viewport.js +90 -132
  42. package/docs/architecture/ui-reference-provenance.md +8 -5
  43. package/docs/openspec-archive-automation.md +2 -2
  44. package/docs/validation.md +1 -1
  45. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selectionCopyRowText } from "./selection-copy.js";
1
+ import { MAX_COPY_SOURCE_UNITS } from "./selection-copy.js";
2
2
  import { isThumbRow, scrollbarGeometry, scrollbarPresentation, } from "./scrollbar.js";
3
3
  import { backgroundSgrSpan, overlaySpan } from "./spans.js";
4
4
  import { faint, truncateToWidth, displayWidth, stripAnsi } from "./text.js";
5
- import { extendTextSelection, orderedTextSelection, pressTextSelection, releaseTextSelection, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, } from "./text-selection.js";
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
7
  const IDENTITY_ROW = (row) => row;
8
8
  const DEFAULT_CONFIG = {
@@ -35,7 +35,8 @@ export class TranscriptViewport {
35
35
  #selectionClick;
36
36
  #selectionRevision = 0;
37
37
  #copyableSelection = false;
38
- #copyableRow;
38
+ // Invariant: exact unselected rows cover the latest visible base frame and dock.
39
+ #selectionRows = [];
39
40
  #paintedRowCache = new Map();
40
41
  #baseRowCache = new Map();
41
42
  #selectedRowCache = new Map();
@@ -78,19 +79,17 @@ export class TranscriptViewport {
78
79
  setStickyHovered(hovered) { this.#stickyHovered = hovered; }
79
80
  get selectionActive() { return this.#selection?.selecting === true; }
80
81
  get hasSelection() { return orderedTextSelection(this.#selection) !== undefined; }
81
- pressSelection(column, viewportRow, now = Date.now()) {
82
- const viewportHeight = this.#frame?.hits.viewportHeight ?? 0;
83
- if (viewportRow < 1 || viewportRow > viewportHeight || column < 1 || column > this.#contentWidth)
84
- return false;
85
- const line = clamp(this.#scrollTop + viewportRow - 1, 0, Math.max(0, this.#documentRows.length - 1));
86
- if (line >= this.#selectableDocumentRowCount)
82
+ pressSelection(column, frameRow, now = Date.now()) {
83
+ const frameHeight = this.#frame?.rows.length ?? 0;
84
+ if (frameRow < 1 || frameRow > frameHeight || column < 1 || column > this.#selectionWidth)
87
85
  return false;
86
+ const line = frameRow - 1;
88
87
  const pressed = pressTextSelection({
89
88
  line,
90
89
  column,
91
- contentWidth: this.#contentWidth,
92
- lineText: this.#documentRows[line] ?? "",
93
- lineContent: this.#lineContentAt(line),
90
+ contentWidth: this.#selectionWidth,
91
+ lineText: this.#selectionRows[line] ?? "",
92
+ lineContent: usefulTextLineContent(this.#selectionRows[line] ?? ""),
94
93
  ...(this.#selectionClick === undefined ? {} : { previousClick: this.#selectionClick }),
95
94
  now,
96
95
  });
@@ -98,36 +97,35 @@ export class TranscriptViewport {
98
97
  this.#selectionClick = pressed.click;
99
98
  this.#selectionRevision += 1;
100
99
  this.#updateCopyableSelection();
100
+ if (this.#copyableSelection)
101
+ this.#followingEnd = false;
101
102
  return true;
102
103
  }
103
- extendSelection(column, viewportRow, now = Date.now(), autoScroll = true) {
104
+ extendSelection(column, frameRow, now = Date.now(), autoScroll = true) {
104
105
  const selection = this.#selection;
105
- const viewportHeight = this.#frame?.hits.viewportHeight ?? 0;
106
- if (selection?.selecting !== true || viewportHeight <= 0 || this.#documentRows.length === 0)
106
+ const frameHeight = this.#frame?.rows.length ?? 0;
107
+ if (selection?.selecting !== true || frameHeight <= 0 || this.#selectionRows.length === 0)
107
108
  return false;
108
109
  // Performance: pointer motion updates only the endpoint; the shell's fixed-cadence timer
109
- // owns edge scrolling. This option prevents high-rate motion reports from
110
- // adding irregular extra rows between timer ticks.
111
- if (autoScroll && viewportRow > viewportHeight)
110
+ // owns edge scrolling. Selection itself remains in visible-frame coordinates.
111
+ if (autoScroll && frameRow > frameHeight)
112
112
  this.scrollBy(1, now);
113
- else if (autoScroll && viewportRow <= 1 && this.#scrollTop > 0)
113
+ else if (autoScroll && frameRow <= 1 && this.#scrollTop > 0)
114
114
  this.scrollBy(-1, now);
115
- const lastSelectableLine = Math.min(this.#documentRows.length, this.#selectableDocumentRowCount) - 1;
116
- if (lastSelectableLine < 0)
117
- return false;
118
- const visibleRow = clamp(viewportRow, 1, viewportHeight);
119
- const line = clamp(this.#scrollTop + visibleRow - 1, 0, lastSelectableLine);
115
+ const line = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
120
116
  const targetColumn = selection.fullRow
121
117
  ? textSelectionLineExtendColumn(selection, line, this.#selectionWidth)
122
118
  : clamp(column, 1, this.#selectionWidth);
123
119
  const point = selection.fullRow
124
120
  ? { line, column: targetColumn }
125
- : textSelectionPointAt(line, targetColumn, this.#documentRows[line] ?? "");
121
+ : textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
126
122
  const next = extendTextSelection(selection, point);
127
123
  if (next !== selection) {
128
124
  this.#selection = next;
129
125
  this.#selectionRevision += 1;
130
126
  this.#updateCopyableSelection();
127
+ if (this.#copyableSelection)
128
+ this.#followingEnd = false;
131
129
  }
132
130
  return true;
133
131
  }
@@ -136,6 +134,7 @@ export class TranscriptViewport {
136
134
  return false;
137
135
  this.#selection = releaseTextSelection(this.#selection);
138
136
  this.#selectionRevision += 1;
137
+ this.#updateCopyableSelection();
139
138
  return true;
140
139
  }
141
140
  clearSelection() {
@@ -143,7 +142,6 @@ export class TranscriptViewport {
143
142
  return false;
144
143
  this.#selection = undefined;
145
144
  this.#copyableSelection = false;
146
- this.#copyableRow = undefined;
147
145
  this.#selectionRevision += 1;
148
146
  return true;
149
147
  }
@@ -151,39 +149,27 @@ export class TranscriptViewport {
151
149
  const selection = orderedTextSelection(this.#selection);
152
150
  if (selection === undefined)
153
151
  return null;
154
- const endLine = Math.min(selection.end.line, this.#selectableDocumentRowCount - 1);
155
- return textSelectionText({ ...selection, end: { line: endLine, column: endLine === selection.end.line ? selection.end.column : Number.MAX_SAFE_INTEGER } }, this.#documentRows, line => this.#lineContentAt(line));
152
+ const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
153
+ return text.length === 0 ? null : text;
156
154
  }
157
- /** Captures selected source references, never the entire mutable transcript or prompt graph. */
155
+ /** Captures only the selected visible frame text, never off-screen transcript history. */
158
156
  captureSelectedText() {
159
157
  if (!this.#copyableSelection)
160
158
  return null;
161
159
  const started = performance.now();
162
- const selection = orderedTextSelection(this.#selection);
163
- if (selection === undefined)
164
- return null;
165
- const start = selection.start.line;
166
- const end = Math.min(selection.end.line, this.#selectableDocumentRowCount - 1, this.#documentRows.length - 1);
167
- if (end < start)
160
+ const text = this.selectedText();
161
+ if (text === null)
168
162
  return null;
169
163
  const normalized = {
170
- start: { line: 0, column: selection.start.column },
171
- end: { line: end - start, column: end === selection.end.line ? selection.end.column : Number.MAX_SAFE_INTEGER },
164
+ start: { line: 0, column: 0 },
165
+ end: { line: 0, column: Number.MAX_SAFE_INTEGER },
172
166
  };
173
- const rejected = () => ({ selection: normalized, revision: this.#selectionRevision, rows: [], sourceUnits: 0, captureMs: performance.now() - started, rejected: "size" });
174
- if (end - start + 1 > MAX_COPY_ROWS)
175
- return rejected();
176
- const rows = [];
177
- let sourceUnits = 0;
178
- for (let line = start; line <= end; line++) {
179
- const text = this.#documentRows[line] ?? "";
180
- sourceUnits += text.length;
181
- if (sourceUnits > MAX_COPY_SOURCE_UNITS)
182
- return rejected();
183
- const prompt = this.#promptKindAt(line);
184
- rows.push({ text, ...(prompt === undefined ? {} : { prompt }) });
167
+ if (text.length > MAX_COPY_SOURCE_UNITS) {
168
+ return { literal: true, selection: normalized, revision: this.#selectionRevision, rows: [], sourceUnits: 0,
169
+ captureMs: performance.now() - started, rejected: "size" };
185
170
  }
186
- return { selection: normalized, revision: this.#selectionRevision, rows, sourceUnits, captureMs: performance.now() - started };
171
+ return { literal: true, selection: normalized, revision: this.#selectionRevision, rows: [{ text }],
172
+ sourceUnits: text.length, captureMs: performance.now() - started };
187
173
  }
188
174
  scrollBy(lines, now = Date.now()) {
189
175
  if (this.#maxScroll <= 0 || lines === 0)
@@ -255,7 +241,7 @@ export class TranscriptViewport {
255
241
  this.#selectionRevision += 1;
256
242
  this.#selection = undefined;
257
243
  this.#copyableSelection = false;
258
- this.#copyableRow = undefined;
244
+ this.#selectionRows = [];
259
245
  this.#selectionClick = undefined;
260
246
  this.#paintedRowCache.clear();
261
247
  this.#baseRowCache.clear();
@@ -328,7 +314,6 @@ export class TranscriptViewport {
328
314
  // Invariant: controls reserve the rail column, but source text beneath it
329
315
  // remains reachable by a drag already owned by transcript selection.
330
316
  this.#selectionWidth = width;
331
- this.#updateCopyableSelection();
332
317
  const paintDocumentRow = input.paintDocumentRow ?? IDENTITY_ROW;
333
318
  const cacheLimit = Math.max(32, viewportHeight * 6);
334
319
  trimCache(this.#paintedRowCache, cacheLimit);
@@ -337,16 +322,15 @@ export class TranscriptViewport {
337
322
  trimCache(this.#finalRowCache, cacheLimit);
338
323
  const paintId = this.#functionId(paintDocumentRow);
339
324
  const paintRecomputedRows = new Set();
340
- const visible = documentRows
341
- .slice(this.#scrollTop, this.#scrollTop + viewportHeight)
342
- .map((row, index) => {
325
+ const visibleSource = documentRows.slice(this.#scrollTop, this.#scrollTop + viewportHeight);
326
+ while (visibleSource.length < viewportHeight)
327
+ visibleSource.push("");
328
+ const visible = visibleSource.map((row, index) => {
343
329
  const painted = cachedString(this.#paintedRowCache, `${paintId}\u0000${row}`, cacheLimit, () => paintDocumentRow(row));
344
330
  if (!painted.reused)
345
331
  paintRecomputedRows.add(index);
346
332
  return painted.value;
347
333
  });
348
- while (visible.length < viewportHeight)
349
- visible.push("");
350
334
  const governing = governingPrompt(input.promptAnchors, this.#scrollTop);
351
335
  const stickyActive = governing !== null && governing.firstRow < this.#scrollTop;
352
336
  if (stickyActive && visible.length > 0) {
@@ -362,7 +346,10 @@ export class TranscriptViewport {
362
346
  paintRecomputedRows.add(0);
363
347
  const sticky = theme.sticky(source.value, this.#stickyHovered);
364
348
  visible[0] = quiet && !this.#stickyHovered ? theme.quietSticky(sticky) : sticky;
349
+ visibleSource[0] = sourceRow;
365
350
  }
351
+ // Invariant: selection uses exact visible source rows; paint-only hyperlink guards stay rendered-only.
352
+ this.#selectionRows = [...visibleSource, ...dock].slice(0, height);
366
353
  const selectionPainterId = this.#functionId(theme.selection);
367
354
  // Concurrency: a painter may synchronously route input in tests or host integrations.
368
355
  // Label this frame with the exact selection snapshot it began composing.
@@ -372,40 +359,8 @@ export class TranscriptViewport {
372
359
  const reusedRows = [];
373
360
  const selectionDamagedRows = [];
374
361
  const visibleStates = [];
375
- for (let row = 0; row < visible.length; row += 1) {
376
- const documentLine = this.#scrollTop + row;
377
- const painted = visible[row] ?? "";
378
- const base = cachedString(this.#baseRowCache, `${width}\u0000${painted}`, cacheLimit, () => padRowPreservingBackground(painted, width));
379
- let rowRecomputed = paintRecomputedRows.has(row) || !base.reused;
380
- const range = stickyActive && row === 0
381
- ? null
382
- : selectionRangeForLine(orderedSelection, documentLine, this.#selectableDocumentRowCount, width);
383
- const rangeKey = range === null ? "-" : `${range.from}:${range.to}`;
384
- const selected = range === null
385
- ? { value: base.value, reused: true }
386
- : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
387
- rowRecomputed ||= !selected.reused;
388
- // Invariant: paint the rail after selection. Full-row selection reaches the terminal
389
- // edge, while the foreground thumb/track remains visible above that
390
- // background instead of disappearing into it.
391
- let railCell = "";
392
- if (presentation.visible && geometry !== null && row > 0) {
393
- const trackRow = row - 1;
394
- const thumb = isThumbRow(geometry, trackRow);
395
- const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
396
- railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
397
- }
398
- 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 }));
399
- rowRecomputed ||= !final.reused;
400
- visible[row] = final.value;
401
- (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
402
- const state = `${width}\u0000${documentLine}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}`;
403
- visibleStates.push(state);
404
- if (this.#previousVisibleStates[row] !== state)
405
- selectionDamagedRows.push(row + 1);
406
- }
407
- this.#previousVisibleStates = visibleStates;
408
362
  const frameRows = [...visible, ...dock].slice(0, height);
363
+ const selectionRows = [...this.#selectionRows];
409
364
  let bottomHit = null;
410
365
  if (this.#maxScroll > 0 && !this.#followingEnd && frameRows.length > 0) {
411
366
  const genericLabel = " Jump to bottom (Ctrl+End) ↓ ";
@@ -424,8 +379,49 @@ export class TranscriptViewport {
424
379
  const bottomHovered = pointer !== undefined && pointer.row === bottomHit.row
425
380
  && pointer.column >= bottomHit.columnStart && pointer.column <= bottomHit.columnEnd;
426
381
  frameRows[row] = overlaySpan(padRowPreservingBackground(frameRows[row] ?? "", width), left, left + labelWidth, `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}`);
382
+ selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, label);
427
383
  }
428
384
  }
385
+ for (const covered of input.coveredCells ?? []) {
386
+ const row = covered.row - 1;
387
+ if (row < 0 || row >= selectionRows.length)
388
+ continue;
389
+ const from = clamp(covered.columnStart - 1, 0, width);
390
+ const to = clamp(covered.columnEnd, from, width);
391
+ selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), from, to, " ".repeat(to - from));
392
+ }
393
+ this.#selectionRows = selectionRows;
394
+ this.#updateCopyableSelection();
395
+ for (let row = 0; row < frameRows.length; row += 1) {
396
+ const painted = frameRows[row] ?? "";
397
+ const range = selectionRangeForLine(orderedSelection, row, this.#selectionRows.length, width);
398
+ const padded = row < viewportHeight || range !== null;
399
+ const base = cachedString(this.#baseRowCache, `${width}\u0000${padded}\u0000${painted}`, cacheLimit, () => padded ? padRowPreservingBackground(painted, width) : painted);
400
+ let rowRecomputed = row < viewportHeight && paintRecomputedRows.has(row) || !base.reused;
401
+ const rangeKey = range === null ? "-" : `${range.from}:${range.to}`;
402
+ const selected = range === null
403
+ ? { value: base.value, reused: true }
404
+ : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
405
+ 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.
408
+ let railCell = "";
409
+ if (row < viewportHeight && presentation.visible && geometry !== null && row > 0) {
410
+ const trackRow = row - 1;
411
+ const thumb = isThumbRow(geometry, trackRow);
412
+ const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
413
+ railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
414
+ }
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 }));
416
+ rowRecomputed ||= !final.reused;
417
+ frameRows[row] = final.value;
418
+ (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
419
+ const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}`;
420
+ visibleStates.push(state);
421
+ if (this.#previousVisibleStates[row] !== state)
422
+ selectionDamagedRows.push(row + 1);
423
+ }
424
+ this.#previousVisibleStates = visibleStates;
429
425
  const nextDocumentRange = {
430
426
  start: this.#scrollTop,
431
427
  end: Math.min(documentRows.length, this.#scrollTop + viewportHeight),
@@ -509,7 +505,7 @@ export class TranscriptViewport {
509
505
  const boundedWidth = Math.max(1, width);
510
506
  const boundedHeight = Math.max(0, height);
511
507
  if (previous === null || previous.descriptor.width !== boundedWidth || previous.descriptor.height !== boundedHeight
512
- || previous.descriptor.selectionRevision !== this.#selectionRevision)
508
+ || previous.descriptor.selectionRevision !== this.#selectionRevision || this.#selection !== undefined)
513
509
  return null;
514
510
  const dock = dockRows.length > boundedHeight ? dockRows.slice(-boundedHeight) : [...dockRows];
515
511
  const viewportHeight = Math.max(0, boundedHeight - dock.length);
@@ -539,6 +535,7 @@ export class TranscriptViewport {
539
535
  cause: "dock-input",
540
536
  };
541
537
  assertTranscriptViewportFrameDescriptor(descriptor);
538
+ this.#selectionRows = [...this.#selectionRows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight);
542
539
  const frame = {
543
540
  rows: [...previous.rows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight),
544
541
  contentWidth: previous.contentWidth,
@@ -568,47 +565,8 @@ export class TranscriptViewport {
568
565
  }
569
566
  #updateCopyableSelection() {
570
567
  const selection = orderedTextSelection(this.#selection);
571
- if (!selection) {
572
- this.#copyableSelection = false;
573
- this.#copyableRow = undefined;
574
- return;
575
- }
576
- const end = Math.min(selection.end.line, this.#selectableDocumentRowCount - 1);
577
- if (end !== selection.start.line) {
578
- this.#copyableSelection = end > selection.start.line;
579
- this.#copyableRow = undefined;
580
- return;
581
- }
582
- const text = this.#documentRows[end] ?? "";
583
- const prompt = this.#promptKindAt(end);
584
- const from = selection.start.column, to = end === selection.end.line ? selection.end.column : Number.MAX_SAFE_INTEGER;
585
- const cached = this.#copyableRow;
586
- if (cached?.text === text && cached.prompt === prompt && cached.from === from && cached.to === to) {
587
- this.#copyableSelection = cached.value;
588
- return;
589
- }
590
- // Compatibility: learn whether an actual range is empty while presenting selection, not on Ctrl+C.
591
- const value = selectionCopyRowText({ selection: { start: { line: 0, column: from }, end: { line: 0, column: to } } }, { text, ...(prompt === undefined ? {} : { prompt }) }, 0).length > 0;
592
- this.#copyableRow = { text, prompt, from, to, value };
593
- this.#copyableSelection = value;
594
- }
595
- #lineContentAt(line) {
596
- return selectionCopyLineContent(this.#documentRows[line] ?? "", this.#promptKindAt(line));
597
- }
598
- #promptKindAt(line) {
599
- // Performance: anchors are ordered, disjoint source ranges; do not scan off-screen prompts per copied row.
600
- let low = 0, high = this.#promptAnchors.length - 1;
601
- while (low <= high) {
602
- const middle = (low + high) >>> 1;
603
- const anchor = this.#promptAnchors[middle];
604
- if (line < anchor.firstRow)
605
- high = middle - 1;
606
- else if (line > anchor.lastRow)
607
- low = middle + 1;
608
- else
609
- return line === anchor.firstRow ? "first" : "continuation";
610
- }
611
- return undefined;
568
+ this.#copyableSelection = selection !== undefined
569
+ && textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? "")).length > 0;
612
570
  }
613
571
  }
614
572
  function selectionRangeForLine(selection, documentLine, selectableRowCount, width) {
@@ -81,13 +81,16 @@ its own `core` facade layer; A1 is a product, so the port adapts imports and kee
81
81
  input behavior, but A1 uses its prior dark floating panel, lighter white-text active row,
82
82
  and independent effective-value check mark because physical review rejected a menu that
83
83
  blended into the settings rows.
84
- - **Spinner-backed progress punctuation.** Bare A1 resolves built-in and extension working
84
+ - **Spinner-backed progress presentation.** Bare A1 resolves built-in and extension working
85
85
  labels through `ui-components/progress-status.ts`, injected by
86
86
  `session-shell/session-shell-root.ts` into the shared `pi-components/shell-footer-status.ts`
87
- spinner factory. The factory canonicalizes the visible progress marker once, to three ASCII
88
- periods, without importing across the component-adapter boundary. Engine producers remain
89
- semantic and the source-synchronized Pi status indicators, installed packages, `a1 pi`, and
90
- vanilla Pi remain untouched.
87
+ spinner factory. The factory canonicalizes the visible progress marker once, to one Unicode
88
+ ellipsis, and uses the existing spinner updates to move a two-grapheme accent band slowly
89
+ across otherwise muted label text with a muted pause between passes. The ellipsis remains
90
+ stationary, no additional timer exists, and theme roles cross the component-adapter boundary
91
+ as styling callbacks rather than imports or literal colours. Engine producers remain semantic;
92
+ pinned presentation keeps its three periods; and the source-synchronized Pi status indicators,
93
+ installed packages, `a1 pi`, and vanilla Pi remain untouched.
91
94
  - **Keyboard presentation scheduling.** Pinned components, semantic key handling, and the
92
95
  comparison profiles stay unchanged. Bare A1 adds one A1-owned event-loop-turn coordinator
93
96
  before its fullscreen root and may omit only superseded visual states after every delivery
@@ -19,7 +19,7 @@ Version-1 and version-2 deliveries and their existing comments, acceptance PRs,
19
19
 
20
20
  ### Complete-regression evidence ordering
21
21
 
22
- PR-attached Full regression is selected only for a repair that the trusted triage created from a failed Full regression: base policy verifies the immutable `openspec-ci` App author, generated candidate identity, and `regression-provenance.json`. Human-authored lookalikes, publishing or validation-authority changes, labels, Release-only repairs, and successful-run persistent-overrun candidates retain bounded ordinary validation. Planning-only generated repair drafts remain lightweight. Eligible implementation drafts expose all four native lane checks without becoming mergeable. Finish substantive implementation tasks, focused evidence, pre-finalization observations, and known-gap disposition before marking ready. The finalized head then receives new full evidence, required by `Development validation required` alongside the existing gates.
22
+ PR-attached Full regression is selected only for a repair that the trusted triage created from a failed Full regression: base policy verifies the immutable `openspec-ci` App author, generated candidate identity, and `regression-provenance.json`. Human-authored lookalikes, publishing or validation-authority changes, labels, Release-only repairs, and successful-run persistent-overrun candidates retain bounded ordinary validation. Generated repair drafts run no Development test suites, including after implementation begins. Finish substantive implementation tasks, focused local evidence, pre-finalization observations, and known-gap disposition before marking ready. The ready active head defers testing while trusted finalization publishes the candidate; the finalized head then receives all four native lanes as new full evidence, required by `Development validation required` alongside the existing gates.
23
23
 
24
24
  The final exact-head run is a handoff gate, not a task requiring a future run ID in committed `design.md`. Keep final head/run/selection identity in Actions outcomes and the maintainer handoff; do not invalidate successful evidence with an extra recording commit. Changed code, acceptance/body metadata, selection labels, or target baseline requires fresh evidence. A selected PR success removes the additional mandatory Full regression dispatch, but a standalone/manual run never substitutes for selected PR checks. Before deployment, existing separate-dispatch obligations remain. Neither PR validation nor manual regression proves numbered-package nightly publication recovery.
25
25
 
@@ -202,7 +202,7 @@ Unavailable, stale, automatic, unauthorized, conflicting, or contradictory prove
202
202
 
203
203
  ## CI and automation ownership
204
204
 
205
- Normal Development CI remains complete for the implementation. An archive-shaped final diff does not select documentation-only validation because the authoritative version-3 association remains implementation-bound. The trusted acceptance-policy job validates finalization from base-controlled policy while ordinary impact selection retains all applicable product and governance owners. A ready head that still holds the active change fails that job with an `Awaiting automated finalization` notice pointing at the finalization workflow; the head the workflow pushes receives its own complete validation, and `Development validation` cancels the superseded run.
205
+ Normal Development CI remains complete for the implementation once its reviewable candidate exists. An archive-shaped final diff does not select documentation-only validation because the authoritative version-3 association remains implementation-bound. Base-controlled readiness defers a ready head that still holds the active change without running test suites or emitting `Development validation required`. The finalization workflow pushes the finalized head and updates its implementation fence; that update starts one complete selected validation, while PR-level concurrency cancels any superseded eligible run.
206
206
 
207
207
  `pull_request` body edits rerun required CI. The ordinary finalized phase-free run exposes the stable protected aggregate directly; it does not wait for a lifecycle body edit. Documentation auto-merge's trusted owner also reevaluates lifecycle association and disables any armed merge. Every publication entry point explicitly refuses version 3.
208
208
 
@@ -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
- A finalized version-3 PR keeps its phase-free body unchanged. Its 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. No lifecycle body edit or second workflow run is required. Any implementation commit changes the head and reruns applicable validation; any body change reruns finalized-record validation and must continue to match the committed manifest. A ready head that still holds the active change fails `Finalized delivery validation` with an `Awaiting automated finalization` notice; the `OpenSpec finalization` workflow pushes the finalized head, which receives its own run. 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. 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.560",
3
+ "version": "0.1.8-dev.572",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",