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

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 (71) hide show
  1. package/README.md +13 -11
  2. package/dist/app/session-shell/prompt-chips.js +20 -28
  3. package/dist/app/session-shell/session-shell-root.d.ts +2 -0
  4. package/dist/app/session-shell/session-shell-root.js +60 -17
  5. package/dist/app/session-shell/session-shell.js +35 -1
  6. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
  7. package/dist/app/session-shell/session-viewport-controller.js +52 -30
  8. package/dist/cli/dispatch.d.ts +3 -1
  9. package/dist/cli/dispatch.js +56 -56
  10. package/dist/cli/package-messages.d.ts +4 -3
  11. package/dist/cli/package-messages.js +23 -11
  12. package/dist/contracts/owned-ui/index.d.ts +2 -0
  13. package/dist/contracts/owned-ui/index.js +1 -0
  14. package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
  15. package/dist/contracts/owned-ui/prompt-chips.js +59 -0
  16. package/dist/contracts/owned-ui/validation.js +1 -1
  17. package/dist/integrations/pi/components/index.d.ts +1 -0
  18. package/dist/integrations/pi/components/index.js +1 -0
  19. package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
  20. package/dist/integrations/pi/components/modal-frame.js +110 -0
  21. package/dist/integrations/pi/components/models-dialog.js +19 -5
  22. package/dist/integrations/pi/components/shell-footer-status.d.ts +4 -3
  23. package/dist/integrations/pi/components/shell-footer-status.js +53 -15
  24. package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
  25. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -1
  26. package/dist/integrations/pi/components/skills-dialog.js +3 -3
  27. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
  29. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +2 -2
  30. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +3 -2
  31. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
  32. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
  33. package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
  34. package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
  35. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  36. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
  37. package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
  38. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  39. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
  40. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  41. package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
  42. package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
  43. package/dist/integrations/pi/engine/compaction-progress.d.ts +3 -7
  44. package/dist/integrations/pi/engine/compaction-progress.js +11 -10
  45. package/dist/integrations/pi/engine/repository-pr.d.ts +3 -3
  46. package/dist/integrations/pi/engine/repository-pr.js +6 -6
  47. package/dist/integrations/pi/engine/session-runtime.js +5 -4
  48. package/dist/integrations/pi/engine/settings-bridge.js +1 -1
  49. package/dist/integrations/pi/engine/settings-port.js +1 -1
  50. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -0
  51. package/dist/integrations/pi/tui-runtime/adapter.js +11 -2
  52. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  53. package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
  54. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
  55. package/dist/native/darwin-arm64/manifest.json +1 -1
  56. package/dist/native/linux-x64/manifest.json +1 -1
  57. package/dist/native/win32-x64/manifest.json +3 -3
  58. package/dist/native/win32-x64/process-guardian.exe +0 -0
  59. package/dist/ui/components/dialog-panel.js +10 -6
  60. package/dist/ui/components/index.d.ts +2 -1
  61. package/dist/ui/components/index.js +1 -1
  62. package/dist/ui/components/mouse.d.ts +2 -2
  63. package/dist/ui/components/mouse.js +2 -2
  64. package/dist/ui/components/progress-status.d.ts +9 -2
  65. package/dist/ui/components/progress-status.js +24 -3
  66. package/dist/ui/components/transcript-viewport.d.ts +9 -3
  67. package/dist/ui/components/transcript-viewport.js +116 -136
  68. package/docs/architecture/ui-reference-provenance.md +8 -5
  69. package/docs/openspec-archive-automation.md +2 -2
  70. package/docs/validation.md +1 -1
  71. package/package.json +1 -1
@@ -15,7 +15,8 @@ export { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput, routeMouseInput
15
15
  export type { ParsedMouseInput, RoutedMouseInput } from "./mouse.js";
16
16
  export { isInsidePane, toPaneLocalMouse } from "./pane.js";
17
17
  export type { Pane, PaneInputResult, PaneMouseEvent } from "./pane.js";
18
- export { progressStatusText } from "./progress-status.js";
18
+ export { progressStatusFrame, progressStatusText } from "./progress-status.js";
19
+ export type { ProgressStatusFrameStyles, ProgressStatusMarker } from "./progress-status.js";
19
20
  export { FrameCache, RENDER_REVISION_KINDS, RenderRevisionTracker, ZERO_REVISIONS, normalizeRevisions, revisionsEqual } from "./revision.js";
20
21
  export type { RenderCacheContract, RenderRevisionKind, RenderRevisions } from "./revision.js";
21
22
  export { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selectionCopyRowText } from "./selection-copy.js";
@@ -7,7 +7,7 @@ export { blockJumpTarget, blockRowSpan, clampScroll, indexOfKey, layoutList, max
7
7
  export { STEPPER_RESERVE, placementFor, regionAt, renderGroupHeader, renderListRow, renderNote, valueColumnFor } from "./list-view.js";
8
8
  export { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput, routeMouseInput } from "./mouse.js";
9
9
  export { isInsidePane, toPaneLocalMouse } from "./pane.js";
10
- export { progressStatusText } from "./progress-status.js";
10
+ export { progressStatusFrame, progressStatusText } from "./progress-status.js";
11
11
  export { FrameCache, RENDER_REVISION_KINDS, RenderRevisionTracker, ZERO_REVISIONS, normalizeRevisions, revisionsEqual } from "./revision.js";
12
12
  export { MAX_COPY_ROWS, MAX_COPY_SOURCE_UNITS, selectionCopyLineContent, selectionCopyRowText } from "./selection-copy.js";
13
13
  export { ScrollbarRails, isThumbRow, scrollForThumbRow, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarReservesSpace, scrollbarSelectionRows, scrollbarWheelRows, } from "./scrollbar.js";
@@ -4,8 +4,8 @@ import type { PaneMouseEvent } from "./pane.js";
4
4
  * terminal to send reports; nothing else in A1 turns this on, and the sequences
5
5
  * are paired so the terminal is always left as it was found.
6
6
  */
7
- export declare const MOUSE_TRACKING_ON = "\u001B[?1006h\u001B[?1000h\u001B[?1003h";
8
- export declare const MOUSE_TRACKING_OFF = "\u001B[?1003l\u001B[?1000l\u001B[?1006l";
7
+ export declare const MOUSE_TRACKING_ON = "\u001B[?1006h\u001B[?1000h\u001B[?1003h\u001B[?1004h";
8
+ export declare const MOUSE_TRACKING_OFF = "\u001B[?1004l\u001B[?1003l\u001B[?1000l\u001B[?1006l";
9
9
  export interface ParsedMouseInput {
10
10
  readonly events: readonly PaneMouseEvent[];
11
11
  /** Input with every mouse report removed, for ordinary key handling. */
@@ -3,8 +3,8 @@
3
3
  * terminal to send reports; nothing else in A1 turns this on, and the sequences
4
4
  * are paired so the terminal is always left as it was found.
5
5
  */
6
- export const MOUSE_TRACKING_ON = "\u001b[?1006h\u001b[?1000h\u001b[?1003h";
7
- export const MOUSE_TRACKING_OFF = "\u001b[?1003l\u001b[?1000l\u001b[?1006l";
6
+ export const MOUSE_TRACKING_ON = "\u001b[?1006h\u001b[?1000h\u001b[?1003h\u001b[?1004h";
7
+ export const MOUSE_TRACKING_OFF = "\u001b[?1004l\u001b[?1003l\u001b[?1000l\u001b[?1006l";
8
8
  const SGR_PATTERN = /\u001b\[<(\d+);(\d+);(\d+)([Mm])/g;
9
9
  /** Extracts SGR mouse reports, leaving any keyboard input untouched. */
10
10
  export function parseMouseInput(data) {
@@ -1,2 +1,9 @@
1
- /** Returns spinner-backed status text with A1's canonical three-period marker. */
2
- export declare function progressStatusText(message: string): string;
1
+ export type ProgressStatusMarker = "…" | "...";
2
+ export interface ProgressStatusFrameStyles {
3
+ readonly muted: (text: string) => string;
4
+ readonly accent: (text: string) => string;
5
+ }
6
+ /** Returns spinner-backed status text with the selected terminal progress marker. */
7
+ export declare function progressStatusText(message: string, marker?: ProgressStatusMarker): string;
8
+ /** Paints one width-stable, grapheme-safe frame of the quiet progress-label sweep. */
9
+ export declare function progressStatusFrame(message: string, phase: number, styles: ProgressStatusFrameStyles): string;
@@ -1,4 +1,25 @@
1
- /** Returns spinner-backed status text with A1's canonical three-period marker. */
2
- export function progressStatusText(message) {
3
- return `${message.replace(/(?:…|\.+)$/u, "")}...`;
1
+ const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
2
+ const HIGHLIGHT_WIDTH = 2;
3
+ const UPDATES_PER_STEP = 3;
4
+ /** Returns spinner-backed status text with the selected terminal progress marker. */
5
+ export function progressStatusText(message, marker = "…") {
6
+ return `${message.replace(/(?:…|\.+)$/u, "")}${marker}`;
7
+ }
8
+ /** Paints one width-stable, grapheme-safe frame of the quiet progress-label sweep. */
9
+ export function progressStatusFrame(message, phase, styles) {
10
+ const marker = message.endsWith("…") ? "…" : "";
11
+ const label = marker === "" ? message : message.slice(0, -marker.length);
12
+ const graphemes = [...GRAPHEMES.segment(label)].map(segment => segment.segment);
13
+ if (graphemes.length === 0)
14
+ return paint(marker, styles.muted);
15
+ const step = Math.floor(Math.max(0, phase) / UPDATES_PER_STEP) % (graphemes.length * 2);
16
+ if (step >= graphemes.length)
17
+ return paint(`${label}${marker}`, styles.muted);
18
+ const before = graphemes.slice(0, step).join("");
19
+ const highlight = graphemes.slice(step, step + HIGHLIGHT_WIDTH).join("");
20
+ const after = `${graphemes.slice(step + HIGHLIGHT_WIDTH).join("")}${marker}`;
21
+ return `${paint(before, styles.muted)}${paint(highlight, styles.accent)}${paint(after, styles.muted)}`;
22
+ }
23
+ function paint(text, style) {
24
+ return text === "" ? "" : style(text);
4
25
  }
@@ -50,6 +50,12 @@ export interface TranscriptViewportFrameInput {
50
50
  readonly column: number;
51
51
  readonly row: number;
52
52
  };
53
+ /** Base-frame cells hidden by modal/replacement surfaces and therefore excluded from visual copy. */
54
+ readonly coveredCells?: readonly {
55
+ readonly row: number;
56
+ readonly columnStart: number;
57
+ readonly columnEnd: number;
58
+ }[];
53
59
  readonly now?: number;
54
60
  readonly theme?: TranscriptViewportTheme;
55
61
  }
@@ -150,12 +156,12 @@ export declare class TranscriptViewport {
150
156
  setStickyHovered(hovered: boolean): void;
151
157
  get selectionActive(): boolean;
152
158
  get hasSelection(): boolean;
153
- pressSelection(column: number, viewportRow: number, now?: number): boolean;
154
- extendSelection(column: number, viewportRow: number, now?: number, autoScroll?: boolean): boolean;
159
+ pressSelection(column: number, frameRow: number, now?: number): boolean;
160
+ extendSelection(column: number, frameRow: number, now?: number, autoScroll?: boolean): boolean;
155
161
  releaseSelection(): boolean;
156
162
  clearSelection(): boolean;
157
163
  selectedText(): string | null;
158
- /** Captures selected source references, never the entire mutable transcript or prompt graph. */
164
+ /** Captures only the selected visible frame text, never off-screen transcript history. */
159
165
  captureSelectedText(): SelectionCopySnapshot | null;
160
166
  scrollBy(lines: number, now?: number): boolean;
161
167
  scrollTo(position: number, now?: number): boolean;
@@ -1,9 +1,12 @@
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
+ // 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
10
  const IDENTITY_ROW = (row) => row;
8
11
  const DEFAULT_CONFIG = {
9
12
  scrollbarAppearance: "auto",
@@ -35,7 +38,8 @@ export class TranscriptViewport {
35
38
  #selectionClick;
36
39
  #selectionRevision = 0;
37
40
  #copyableSelection = false;
38
- #copyableRow;
41
+ // Invariant: exact unselected rows cover the latest visible base frame and dock.
42
+ #selectionRows = [];
39
43
  #paintedRowCache = new Map();
40
44
  #baseRowCache = new Map();
41
45
  #selectedRowCache = new Map();
@@ -78,19 +82,20 @@ export class TranscriptViewport {
78
82
  setStickyHovered(hovered) { this.#stickyHovered = hovered; }
79
83
  get selectionActive() { return this.#selection?.selecting === true; }
80
84
  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)
85
+ pressSelection(column, frameRow, now = Date.now()) {
86
+ const frameHeight = this.#frame?.rows.length ?? 0;
87
+ const selectionWidth = frameRow <= (this.#frame?.hits.viewportHeight ?? 0)
88
+ ? this.#contentWidth
89
+ : this.#selectionWidth;
90
+ if (frameRow < 1 || frameRow > frameHeight || column < 1 || column > selectionWidth)
87
91
  return false;
92
+ const line = frameRow - 1;
88
93
  const pressed = pressTextSelection({
89
94
  line,
90
95
  column,
91
- contentWidth: this.#contentWidth,
92
- lineText: this.#documentRows[line] ?? "",
93
- lineContent: this.#lineContentAt(line),
96
+ contentWidth: selectionWidth,
97
+ lineText: this.#selectionRows[line] ?? "",
98
+ lineContent: usefulTextLineContent(this.#selectionRows[line] ?? ""),
94
99
  ...(this.#selectionClick === undefined ? {} : { previousClick: this.#selectionClick }),
95
100
  now,
96
101
  });
@@ -98,36 +103,38 @@ export class TranscriptViewport {
98
103
  this.#selectionClick = pressed.click;
99
104
  this.#selectionRevision += 1;
100
105
  this.#updateCopyableSelection();
106
+ if (this.#copyableSelection)
107
+ this.#followingEnd = false;
101
108
  return true;
102
109
  }
103
- extendSelection(column, viewportRow, now = Date.now(), autoScroll = true) {
110
+ extendSelection(column, frameRow, now = Date.now(), autoScroll = true) {
104
111
  const selection = this.#selection;
105
- const viewportHeight = this.#frame?.hits.viewportHeight ?? 0;
106
- if (selection?.selecting !== true || viewportHeight <= 0 || this.#documentRows.length === 0)
112
+ const frameHeight = this.#frame?.rows.length ?? 0;
113
+ if (selection?.selecting !== true || frameHeight <= 0 || this.#selectionRows.length === 0)
107
114
  return false;
108
115
  // 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)
116
+ // owns edge scrolling. Selection itself remains in visible-frame coordinates.
117
+ if (autoScroll && frameRow > frameHeight)
112
118
  this.scrollBy(1, now);
113
- else if (autoScroll && viewportRow <= 1 && this.#scrollTop > 0)
119
+ else if (autoScroll && frameRow <= 1 && this.#scrollTop > 0)
114
120
  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);
121
+ const line = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
122
+ const selectionWidth = line < (this.#frame?.hits.viewportHeight ?? 0)
123
+ ? this.#contentWidth
124
+ : this.#selectionWidth;
120
125
  const targetColumn = selection.fullRow
121
- ? textSelectionLineExtendColumn(selection, line, this.#selectionWidth)
122
- : clamp(column, 1, this.#selectionWidth);
126
+ ? textSelectionLineExtendColumn(selection, line, selectionWidth)
127
+ : clamp(column, 1, selectionWidth);
123
128
  const point = selection.fullRow
124
129
  ? { line, column: targetColumn }
125
- : textSelectionPointAt(line, targetColumn, this.#documentRows[line] ?? "");
130
+ : textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
126
131
  const next = extendTextSelection(selection, point);
127
132
  if (next !== selection) {
128
133
  this.#selection = next;
129
134
  this.#selectionRevision += 1;
130
135
  this.#updateCopyableSelection();
136
+ if (this.#copyableSelection)
137
+ this.#followingEnd = false;
131
138
  }
132
139
  return true;
133
140
  }
@@ -136,6 +143,7 @@ export class TranscriptViewport {
136
143
  return false;
137
144
  this.#selection = releaseTextSelection(this.#selection);
138
145
  this.#selectionRevision += 1;
146
+ this.#updateCopyableSelection();
139
147
  return true;
140
148
  }
141
149
  clearSelection() {
@@ -143,7 +151,6 @@ export class TranscriptViewport {
143
151
  return false;
144
152
  this.#selection = undefined;
145
153
  this.#copyableSelection = false;
146
- this.#copyableRow = undefined;
147
154
  this.#selectionRevision += 1;
148
155
  return true;
149
156
  }
@@ -151,39 +158,27 @@ export class TranscriptViewport {
151
158
  const selection = orderedTextSelection(this.#selection);
152
159
  if (selection === undefined)
153
160
  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));
161
+ const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
162
+ return text.length === 0 ? null : text;
156
163
  }
157
- /** Captures selected source references, never the entire mutable transcript or prompt graph. */
164
+ /** Captures only the selected visible frame text, never off-screen transcript history. */
158
165
  captureSelectedText() {
159
166
  if (!this.#copyableSelection)
160
167
  return null;
161
168
  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)
169
+ const text = this.selectedText();
170
+ if (text === null)
168
171
  return null;
169
172
  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 },
173
+ start: { line: 0, column: 0 },
174
+ end: { line: 0, column: Number.MAX_SAFE_INTEGER },
172
175
  };
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 }) });
176
+ if (text.length > MAX_COPY_SOURCE_UNITS) {
177
+ return { literal: true, selection: normalized, revision: this.#selectionRevision, rows: [], sourceUnits: 0,
178
+ captureMs: performance.now() - started, rejected: "size" };
185
179
  }
186
- return { selection: normalized, revision: this.#selectionRevision, rows, sourceUnits, captureMs: performance.now() - started };
180
+ return { literal: true, selection: normalized, revision: this.#selectionRevision, rows: [{ text }],
181
+ sourceUnits: text.length, captureMs: performance.now() - started };
187
182
  }
188
183
  scrollBy(lines, now = Date.now()) {
189
184
  if (this.#maxScroll <= 0 || lines === 0)
@@ -255,7 +250,7 @@ export class TranscriptViewport {
255
250
  this.#selectionRevision += 1;
256
251
  this.#selection = undefined;
257
252
  this.#copyableSelection = false;
258
- this.#copyableRow = undefined;
253
+ this.#selectionRows = [];
259
254
  this.#selectionClick = undefined;
260
255
  this.#paintedRowCache.clear();
261
256
  this.#baseRowCache.clear();
@@ -325,10 +320,9 @@ export class TranscriptViewport {
325
320
  this.#selectableDocumentRowCount = clamp(input.selectableDocumentRowCount ?? input.documentRows.length, 0, input.documentRows.length);
326
321
  this.#promptAnchors = input.promptAnchors;
327
322
  this.#contentWidth = contentWidth;
328
- // Invariant: controls reserve the rail column, but source text beneath it
329
- // remains reachable by a drag already owned by transcript selection.
323
+ // Invariant: dock rows remain selectable at full width; scrollable source rows
324
+ // use contentWidth and leave the gutter outside semantic selection.
330
325
  this.#selectionWidth = width;
331
- this.#updateCopyableSelection();
332
326
  const paintDocumentRow = input.paintDocumentRow ?? IDENTITY_ROW;
333
327
  const cacheLimit = Math.max(32, viewportHeight * 6);
334
328
  trimCache(this.#paintedRowCache, cacheLimit);
@@ -337,16 +331,15 @@ export class TranscriptViewport {
337
331
  trimCache(this.#finalRowCache, cacheLimit);
338
332
  const paintId = this.#functionId(paintDocumentRow);
339
333
  const paintRecomputedRows = new Set();
340
- const visible = documentRows
341
- .slice(this.#scrollTop, this.#scrollTop + viewportHeight)
342
- .map((row, index) => {
334
+ const visibleSource = documentRows.slice(this.#scrollTop, this.#scrollTop + viewportHeight);
335
+ while (visibleSource.length < viewportHeight)
336
+ visibleSource.push("");
337
+ const visible = visibleSource.map((row, index) => {
343
338
  const painted = cachedString(this.#paintedRowCache, `${paintId}\u0000${row}`, cacheLimit, () => paintDocumentRow(row));
344
339
  if (!painted.reused)
345
340
  paintRecomputedRows.add(index);
346
341
  return painted.value;
347
342
  });
348
- while (visible.length < viewportHeight)
349
- visible.push("");
350
343
  const governing = governingPrompt(input.promptAnchors, this.#scrollTop);
351
344
  const stickyActive = governing !== null && governing.firstRow < this.#scrollTop;
352
345
  if (stickyActive && visible.length > 0) {
@@ -362,7 +355,14 @@ export class TranscriptViewport {
362
355
  paintRecomputedRows.add(0);
363
356
  const sticky = theme.sticky(source.value, this.#stickyHovered);
364
357
  visible[0] = quiet && !this.#stickyHovered ? theme.quietSticky(sticky) : sticky;
358
+ visibleSource[0] = sourceRow;
365
359
  }
360
+ // Invariant: selection uses exact visible source rows within the rendered
361
+ // content boundary; paint-only guards and the rail gutter stay presentation-only.
362
+ this.#selectionRows = [
363
+ ...visibleSource.map(row => truncateToWidth(row, contentWidth)),
364
+ ...dock,
365
+ ].slice(0, height);
366
366
  const selectionPainterId = this.#functionId(theme.selection);
367
367
  // Concurrency: a painter may synchronously route input in tests or host integrations.
368
368
  // Label this frame with the exact selection snapshot it began composing.
@@ -372,40 +372,8 @@ export class TranscriptViewport {
372
372
  const reusedRows = [];
373
373
  const selectionDamagedRows = [];
374
374
  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
375
  const frameRows = [...visible, ...dock].slice(0, height);
376
+ const selectionRows = [...this.#selectionRows];
409
377
  let bottomHit = null;
410
378
  if (this.#maxScroll > 0 && !this.#followingEnd && frameRows.length > 0) {
411
379
  const genericLabel = " Jump to bottom (Ctrl+End) ↓ ";
@@ -424,8 +392,52 @@ export class TranscriptViewport {
424
392
  const bottomHovered = pointer !== undefined && pointer.row === bottomHit.row
425
393
  && pointer.column >= bottomHit.columnStart && pointer.column <= bottomHit.columnEnd;
426
394
  frameRows[row] = overlaySpan(padRowPreservingBackground(frameRows[row] ?? "", width), left, left + labelWidth, `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}`);
395
+ selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, label);
427
396
  }
428
397
  }
398
+ for (const covered of input.coveredCells ?? []) {
399
+ const row = covered.row - 1;
400
+ if (row < 0 || row >= selectionRows.length)
401
+ continue;
402
+ const from = clamp(covered.columnStart - 1, 0, width);
403
+ const to = clamp(covered.columnEnd, from, width);
404
+ selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), from, to, " ".repeat(to - from));
405
+ }
406
+ this.#selectionRows = selectionRows;
407
+ this.#updateCopyableSelection();
408
+ for (let row = 0; row < frameRows.length; row += 1) {
409
+ const painted = frameRows[row] ?? "";
410
+ const rowSelectionWidth = row < viewportHeight ? contentWidth : width;
411
+ const range = selectionRangeForLine(orderedSelection, row, this.#selectionRows.length, rowSelectionWidth);
412
+ const padded = row < viewportHeight || range !== null;
413
+ const base = cachedString(this.#baseRowCache, `${width}\u0000${contentWidth}\u0000${row < viewportHeight}\u0000${padded}\u0000${painted}`, cacheLimit, () => row < viewportHeight
414
+ ? padViewportRowWithGutter(painted, contentWidth, width)
415
+ : padded ? padRowPreservingBackground(painted, width) : painted);
416
+ let rowRecomputed = row < viewportHeight && paintRecomputedRows.has(row) || !base.reused;
417
+ const rangeKey = range === null ? "-" : `${range.from}:${range.to}`;
418
+ const selected = range === null
419
+ ? { value: base.value, reused: true }
420
+ : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
421
+ 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.
424
+ let railCell = "";
425
+ if (row < viewportHeight && presentation.visible && geometry !== null && row > 0) {
426
+ const trackRow = row - 1;
427
+ const thumb = isThumbRow(geometry, trackRow);
428
+ const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
429
+ railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
430
+ }
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}`));
432
+ rowRecomputed ||= !final.reused;
433
+ frameRows[row] = final.value;
434
+ (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
435
+ const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}`;
436
+ visibleStates.push(state);
437
+ if (this.#previousVisibleStates[row] !== state)
438
+ selectionDamagedRows.push(row + 1);
439
+ }
440
+ this.#previousVisibleStates = visibleStates;
429
441
  const nextDocumentRange = {
430
442
  start: this.#scrollTop,
431
443
  end: Math.min(documentRows.length, this.#scrollTop + viewportHeight),
@@ -509,7 +521,7 @@ export class TranscriptViewport {
509
521
  const boundedWidth = Math.max(1, width);
510
522
  const boundedHeight = Math.max(0, height);
511
523
  if (previous === null || previous.descriptor.width !== boundedWidth || previous.descriptor.height !== boundedHeight
512
- || previous.descriptor.selectionRevision !== this.#selectionRevision)
524
+ || previous.descriptor.selectionRevision !== this.#selectionRevision || this.#selection !== undefined)
513
525
  return null;
514
526
  const dock = dockRows.length > boundedHeight ? dockRows.slice(-boundedHeight) : [...dockRows];
515
527
  const viewportHeight = Math.max(0, boundedHeight - dock.length);
@@ -539,6 +551,7 @@ export class TranscriptViewport {
539
551
  cause: "dock-input",
540
552
  };
541
553
  assertTranscriptViewportFrameDescriptor(descriptor);
554
+ this.#selectionRows = [...this.#selectionRows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight);
542
555
  const frame = {
543
556
  rows: [...previous.rows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight),
544
557
  contentWidth: previous.contentWidth,
@@ -568,47 +581,8 @@ export class TranscriptViewport {
568
581
  }
569
582
  #updateCopyableSelection() {
570
583
  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;
584
+ this.#copyableSelection = selection !== undefined
585
+ && textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? "")).length > 0;
612
586
  }
613
587
  }
614
588
  function selectionRangeForLine(selection, documentLine, selectableRowCount, width) {
@@ -668,6 +642,12 @@ function governingPrompt(anchors, scrollTop) {
668
642
  }
669
643
  return result;
670
644
  }
645
+ function padViewportRowWithGutter(line, contentWidth, frameWidth) {
646
+ const boundedContentWidth = Math.min(Math.max(1, contentWidth), frameWidth);
647
+ const content = padRowPreservingBackground(line, boundedContentWidth);
648
+ const gutterWidth = frameWidth - boundedContentWidth;
649
+ return gutterWidth === 0 ? content : `${content}${GUTTER_STYLE_RESET}${" ".repeat(gutterWidth)}`;
650
+ }
671
651
  function padRowPreservingBackground(line, width) {
672
652
  const shown = displayWidth(line) > width ? truncateToWidth(line, width) : line;
673
653
  const padding = Math.max(0, width - displayWidth(shown));
@@ -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.563",
3
+ "version": "0.1.8-dev.573",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",