@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.
- package/dist/app/session-shell/prompt-chips.js +20 -28
- package/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell-root.js +63 -14
- package/dist/app/session-shell/session-shell.js +30 -2
- package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
- package/dist/app/session-shell/session-viewport-controller.js +5 -1
- package/dist/contracts/owned-ui/index.d.ts +2 -0
- package/dist/contracts/owned-ui/index.js +1 -0
- package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
- package/dist/contracts/owned-ui/prompt-chips.js +59 -0
- package/dist/contracts/owned-ui/validation.js +1 -1
- package/dist/integrations/pi/components/index.d.ts +1 -0
- package/dist/integrations/pi/components/index.js +1 -0
- package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
- package/dist/integrations/pi/components/modal-frame.js +110 -0
- package/dist/integrations/pi/components/models-dialog.js +19 -5
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
- package/dist/integrations/pi/components/skills-dialog.js +3 -3
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
- package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
- package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
- package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -6
- package/dist/integrations/pi/engine/compaction-progress.js +10 -10
- package/dist/integrations/pi/engine/settings-bridge.js +1 -1
- package/dist/integrations/pi/engine/settings-port.js +1 -1
- package/dist/integrations/pi/tui-runtime/adapter.js +6 -2
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/dialog-panel.js +10 -6
- package/dist/ui/components/mouse.d.ts +2 -2
- package/dist/ui/components/mouse.js +2 -2
- package/dist/ui/components/transcript-viewport.d.ts +6 -0
- package/dist/ui/components/transcript-viewport.js +186 -27
- package/docs/architecture/ui-reference-provenance.md +9 -0
- package/docs/local-worktree-cleanup.md +3 -1
- package/docs/openspec-archive-automation.md +4 -0
- package/docs/validation.md +1 -1
- 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
|
-
|
|
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:
|
|
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,
|
|
118
|
-
: clamp(column, 1,
|
|
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:
|
|
315
|
-
//
|
|
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
|
|
352
|
-
|
|
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
|
|
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, () =>
|
|
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
|
|
407
|
-
//
|
|
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
|
|
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
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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.
|
|
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:
|
package/docs/validation.md
CHANGED
|
@@ -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