@timurproko/a1 0.1.8-dev.573 → 0.1.8-dev.581
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/session-shell/session-shell-root.js +16 -7
- 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/transcript-viewport.d.ts +6 -0
- package/dist/ui/components/transcript-viewport.js +158 -21
- 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
|
@@ -376,6 +376,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
376
376
|
}
|
|
377
377
|
const height = Math.max(0, this.#componentRuntime.getRows());
|
|
378
378
|
const dock = this.#renderDockLayout(width);
|
|
379
|
+
const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
|
|
379
380
|
// Invariant: auto/always own one stable final-column gutter. Every scrollable
|
|
380
381
|
// surface ends before it; the dock remains full-width and hidden mode returns it.
|
|
381
382
|
const documentWidth = this.#viewportController.config.scrollbarAppearance === "hidden"
|
|
@@ -386,7 +387,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
386
387
|
const statusRows = this.#renderStatus(documentWidth);
|
|
387
388
|
const transientSignature = transientRowsSignature(steeringRows, statusRows);
|
|
388
389
|
const snapshot = this.#visibleViewportSnapshot;
|
|
389
|
-
|
|
390
|
+
// Invariant: paint-only feedback may cover either viewport or dock cells, so
|
|
391
|
+
// ordinary dock-only reuse cannot safely omit its overlay composition.
|
|
392
|
+
const dockInputCandidate = this.#dockInputCandidate && copyAcknowledgement === undefined;
|
|
390
393
|
// Concurrency: input may change after a keyboard receipt but before its paint.
|
|
391
394
|
// Capture only the live inputs this composition will actually represent.
|
|
392
395
|
const inputSurface = this.#inputSurface;
|
|
@@ -444,6 +447,12 @@ export class OwnedUiSessionShellRoot {
|
|
|
444
447
|
// Invariant: the control belongs immediately above the complete dock and
|
|
445
448
|
// floats over transient viewport content rather than consuming a dock row.
|
|
446
449
|
bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
|
|
450
|
+
...(copyAcknowledgement === undefined || height <= 0 ? {} : {
|
|
451
|
+
frameOverlay: {
|
|
452
|
+
row: Math.max(1, Math.min(height, dockStartRow + dock.inputOffset - 1)),
|
|
453
|
+
text: copyAcknowledgement,
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
447
456
|
theme: this.#viewportTheme,
|
|
448
457
|
});
|
|
449
458
|
this.#fullViewportCompositions += 1;
|
|
@@ -559,24 +568,24 @@ export class OwnedUiSessionShellRoot {
|
|
|
559
568
|
const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
|
|
560
569
|
const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
|
|
561
570
|
const aboveWidgets = this.#renderWidgets("aboveEditor", width);
|
|
562
|
-
const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
|
|
563
571
|
const input = this.#inputSurface.render(width);
|
|
564
572
|
// Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
|
|
565
573
|
const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
|
|
566
574
|
const belowWidgets = this.#renderWidgets("belowEditor", width);
|
|
567
575
|
const footer = this.#renderFooter(width);
|
|
568
|
-
const rowsWithoutTransient = [...aboveWidgets, ...
|
|
576
|
+
const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
|
|
577
|
+
const inputOffset = transientRows.length + aboveWidgets.length;
|
|
569
578
|
return {
|
|
570
579
|
rows: [...transientRows, ...rowsWithoutTransient],
|
|
571
|
-
|
|
580
|
+
inputOffset,
|
|
581
|
+
editorOffset: inputOffset + (body?.rowOffset ?? 0),
|
|
572
582
|
inputRows: body?.rowCount ?? input.length,
|
|
573
583
|
};
|
|
574
584
|
}
|
|
575
585
|
#renderCopyAcknowledgement(width) {
|
|
576
586
|
if (this.#copyAcknowledgement === undefined || width <= 0)
|
|
577
|
-
return
|
|
578
|
-
|
|
579
|
-
return [`${" ".repeat(Math.max(0, width - piShellVisibleWidth(text)))}${piTheme().fg("accent", text)}`];
|
|
587
|
+
return undefined;
|
|
588
|
+
return piTheme().fg("accent", piShellTruncateToWidth(this.#copyAcknowledgement, width));
|
|
580
589
|
}
|
|
581
590
|
#clearCopyAcknowledgement(requestRender) {
|
|
582
591
|
if (this.#copyAcknowledgementTimer !== undefined)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-24T10:58:51.409Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-24T10:58:49.680Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-24T10:59:15.858Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "2829ba3eb49302e6f58a4a11fe7d9597d0d8c46301328e034e41fd9e240f1677",
|
|
12
12
|
"size": 178176
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -56,6 +56,12 @@ export interface TranscriptViewportFrameInput {
|
|
|
56
56
|
readonly columnStart: number;
|
|
57
57
|
readonly columnEnd: number;
|
|
58
58
|
}[];
|
|
59
|
+
/** Transient paint-only text that must not participate in frame allocation or copying. */
|
|
60
|
+
readonly frameOverlay?: {
|
|
61
|
+
/** One-based terminal row. */
|
|
62
|
+
readonly row: number;
|
|
63
|
+
readonly text: string;
|
|
64
|
+
};
|
|
59
65
|
readonly now?: number;
|
|
60
66
|
readonly theme?: TranscriptViewportTheme;
|
|
61
67
|
}
|
|
@@ -4,10 +4,13 @@ import { backgroundSgrSpan, overlaySpan } from "./spans.js";
|
|
|
4
4
|
import { faint, truncateToWidth, displayWidth, stripAnsi } from "./text.js";
|
|
5
5
|
import { extendTextSelection, orderedTextSelection, pressTextSelection, releaseTextSelection, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, usefulTextLineContent, } from "./text-selection.js";
|
|
6
6
|
const CONTROL_STYLE_RESET = "\u001b]8;;\u001b\\\u001b[0m";
|
|
7
|
-
// Invariant:
|
|
8
|
-
//
|
|
9
|
-
const
|
|
7
|
+
// Invariant: scrollbar chrome closes source links and decoration without clearing
|
|
8
|
+
// the row surface painted beneath its dedicated, non-semantic gutter.
|
|
9
|
+
const GUTTER_DECORATION_RESET = "\u001b]8;;\u001b\\\u001b[22;23;24;25;27;28;29;39;54;55m";
|
|
10
10
|
const IDENTITY_ROW = (row) => row;
|
|
11
|
+
function documentSelectionAnchor(row, source) {
|
|
12
|
+
return { kind: "document", row, source, identity: stripAnsi(source).trimEnd() };
|
|
13
|
+
}
|
|
11
14
|
const DEFAULT_CONFIG = {
|
|
12
15
|
scrollbarAppearance: "auto",
|
|
13
16
|
scrollbarStyle: "thin",
|
|
@@ -35,6 +38,7 @@ export class TranscriptViewport {
|
|
|
35
38
|
#railDragging = false;
|
|
36
39
|
#stickyHovered = false;
|
|
37
40
|
#selection;
|
|
41
|
+
#selectionAnchors;
|
|
38
42
|
#selectionClick;
|
|
39
43
|
#selectionRevision = 0;
|
|
40
44
|
#copyableSelection = false;
|
|
@@ -52,6 +56,8 @@ export class TranscriptViewport {
|
|
|
52
56
|
#promptAnchors = [];
|
|
53
57
|
#contentWidth = 0;
|
|
54
58
|
#selectionWidth = 0;
|
|
59
|
+
#viewportHeight = 0;
|
|
60
|
+
#selectionRowAnchors = [];
|
|
55
61
|
#frameId = 0;
|
|
56
62
|
#frame = null;
|
|
57
63
|
get scrollTop() { return this.#scrollTop; }
|
|
@@ -100,11 +106,13 @@ export class TranscriptViewport {
|
|
|
100
106
|
now,
|
|
101
107
|
});
|
|
102
108
|
this.#selection = pressed.selection;
|
|
109
|
+
const rowAnchor = this.#selectionAnchorAt(line);
|
|
110
|
+
this.#selectionAnchors = pressed.selection === undefined || rowAnchor === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: { anchor: rowAnchor, head: rowAnchor };
|
|
103
113
|
this.#selectionClick = pressed.click;
|
|
104
114
|
this.#selectionRevision += 1;
|
|
105
115
|
this.#updateCopyableSelection();
|
|
106
|
-
if (this.#copyableSelection)
|
|
107
|
-
this.#followingEnd = false;
|
|
108
116
|
return true;
|
|
109
117
|
}
|
|
110
118
|
extendSelection(column, frameRow, now = Date.now(), autoScroll = true) {
|
|
@@ -130,11 +138,15 @@ export class TranscriptViewport {
|
|
|
130
138
|
: textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
|
|
131
139
|
const next = extendTextSelection(selection, point);
|
|
132
140
|
if (next !== selection) {
|
|
141
|
+
const headAnchor = this.#selectionAnchorAt(line);
|
|
142
|
+
if (headAnchor === undefined || this.#selectionAnchors === undefined) {
|
|
143
|
+
this.clearSelection();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
133
146
|
this.#selection = next;
|
|
147
|
+
this.#selectionAnchors = { ...this.#selectionAnchors, head: headAnchor };
|
|
134
148
|
this.#selectionRevision += 1;
|
|
135
149
|
this.#updateCopyableSelection();
|
|
136
|
-
if (this.#copyableSelection)
|
|
137
|
-
this.#followingEnd = false;
|
|
138
150
|
}
|
|
139
151
|
return true;
|
|
140
152
|
}
|
|
@@ -142,6 +154,8 @@ export class TranscriptViewport {
|
|
|
142
154
|
if (this.#selection?.selecting !== true)
|
|
143
155
|
return false;
|
|
144
156
|
this.#selection = releaseTextSelection(this.#selection);
|
|
157
|
+
if (this.#selection === undefined)
|
|
158
|
+
this.#selectionAnchors = undefined;
|
|
145
159
|
this.#selectionRevision += 1;
|
|
146
160
|
this.#updateCopyableSelection();
|
|
147
161
|
return true;
|
|
@@ -150,12 +164,13 @@ export class TranscriptViewport {
|
|
|
150
164
|
if (this.#selection === undefined)
|
|
151
165
|
return false;
|
|
152
166
|
this.#selection = undefined;
|
|
167
|
+
this.#selectionAnchors = undefined;
|
|
153
168
|
this.#copyableSelection = false;
|
|
154
169
|
this.#selectionRevision += 1;
|
|
155
170
|
return true;
|
|
156
171
|
}
|
|
157
172
|
selectedText() {
|
|
158
|
-
const selection = orderedTextSelection(this.#selection);
|
|
173
|
+
const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
|
|
159
174
|
if (selection === undefined)
|
|
160
175
|
return null;
|
|
161
176
|
const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
|
|
@@ -249,8 +264,10 @@ export class TranscriptViewport {
|
|
|
249
264
|
if (this.#selection !== undefined)
|
|
250
265
|
this.#selectionRevision += 1;
|
|
251
266
|
this.#selection = undefined;
|
|
267
|
+
this.#selectionAnchors = undefined;
|
|
252
268
|
this.#copyableSelection = false;
|
|
253
269
|
this.#selectionRows = [];
|
|
270
|
+
this.#selectionRowAnchors = [];
|
|
254
271
|
this.#selectionClick = undefined;
|
|
255
272
|
this.#paintedRowCache.clear();
|
|
256
273
|
this.#baseRowCache.clear();
|
|
@@ -262,6 +279,7 @@ export class TranscriptViewport {
|
|
|
262
279
|
this.#promptAnchors = [];
|
|
263
280
|
this.#contentWidth = 0;
|
|
264
281
|
this.#selectionWidth = 0;
|
|
282
|
+
this.#viewportHeight = 0;
|
|
265
283
|
this.clearTransient();
|
|
266
284
|
this.#frame = null;
|
|
267
285
|
}
|
|
@@ -323,6 +341,7 @@ export class TranscriptViewport {
|
|
|
323
341
|
// Invariant: dock rows remain selectable at full width; scrollable source rows
|
|
324
342
|
// use contentWidth and leave the gutter outside semantic selection.
|
|
325
343
|
this.#selectionWidth = width;
|
|
344
|
+
this.#viewportHeight = viewportHeight;
|
|
326
345
|
const paintDocumentRow = input.paintDocumentRow ?? IDENTITY_ROW;
|
|
327
346
|
const cacheLimit = Math.max(32, viewportHeight * 6);
|
|
328
347
|
trimCache(this.#paintedRowCache, cacheLimit);
|
|
@@ -363,6 +382,20 @@ export class TranscriptViewport {
|
|
|
363
382
|
...visibleSource.map(row => truncateToWidth(row, contentWidth)),
|
|
364
383
|
...dock,
|
|
365
384
|
].slice(0, height);
|
|
385
|
+
const visibleAnchors = Array.from({ length: viewportHeight }, (_value, row) => {
|
|
386
|
+
const documentRow = this.#scrollTop + row;
|
|
387
|
+
return documentRow < documentRows.length
|
|
388
|
+
? documentSelectionAnchor(documentRow, documentRows[documentRow] ?? "")
|
|
389
|
+
: { kind: "screen", row, source: this.#selectionRows[row] ?? "" };
|
|
390
|
+
});
|
|
391
|
+
if (stickyActive && governing !== null && visibleAnchors.length > 0) {
|
|
392
|
+
visibleAnchors[0] = documentSelectionAnchor(governing.firstRow, documentRows[governing.firstRow] ?? governing.sourceRow);
|
|
393
|
+
}
|
|
394
|
+
this.#selectionRowAnchors = [
|
|
395
|
+
...visibleAnchors,
|
|
396
|
+
...dock.map((_row, index) => ({ kind: "dock", fromBottom: dock.length - index - 1 })),
|
|
397
|
+
].slice(0, height);
|
|
398
|
+
this.#projectSelection(documentRows, dock, height);
|
|
366
399
|
const selectionPainterId = this.#functionId(theme.selection);
|
|
367
400
|
// Concurrency: a painter may synchronously route input in tests or host integrations.
|
|
368
401
|
// Label this frame with the exact selection snapshot it began composing.
|
|
@@ -419,8 +452,8 @@ export class TranscriptViewport {
|
|
|
419
452
|
? { value: base.value, reused: true }
|
|
420
453
|
: cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
|
|
421
454
|
rowRecomputed ||= !selected.reused;
|
|
422
|
-
// Invariant: paint the rail after selection
|
|
423
|
-
// Source and
|
|
455
|
+
// Invariant: paint the rail after selection over the gutter's continued row
|
|
456
|
+
// background. Source content and selection still end at the preceding cell.
|
|
424
457
|
let railCell = "";
|
|
425
458
|
if (row < viewportHeight && presentation.visible && geometry !== null && row > 0) {
|
|
426
459
|
const trackRow = row - 1;
|
|
@@ -428,11 +461,19 @@ export class TranscriptViewport {
|
|
|
428
461
|
const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
|
|
429
462
|
railCell = thumb ? theme.thumb(glyph, this.#railHovered || this.#railDragging) : theme.track(glyph);
|
|
430
463
|
}
|
|
431
|
-
const
|
|
464
|
+
const overlayLimit = row < viewportHeight ? contentWidth : width;
|
|
465
|
+
const overlayText = input.frameOverlay?.row === row + 1
|
|
466
|
+
? truncateToWidth(input.frameOverlay.text, overlayLimit)
|
|
467
|
+
: "";
|
|
468
|
+
const overlayWidth = displayWidth(overlayText);
|
|
469
|
+
const final = cachedString(this.#finalRowCache, `${width}\u0000${railCell}\u0000${overlayText}\u0000${selected.value}`, cacheLimit, () => {
|
|
470
|
+
const withRail = railCell.length === 0 ? selected.value : overlaySpan(selected.value, width - 1, width, `${GUTTER_DECORATION_RESET}${railCell}`, { inheritStartStyle: true });
|
|
471
|
+
return overlayWidth === 0 ? withRail : overlaySpan(padRowPreservingBackground(withRail, width), overlayLimit - overlayWidth, overlayLimit, `${CONTROL_STYLE_RESET}${overlayText}`);
|
|
472
|
+
});
|
|
432
473
|
rowRecomputed ||= !final.reused;
|
|
433
474
|
frameRows[row] = final.value;
|
|
434
475
|
(rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
|
|
435
|
-
const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}`;
|
|
476
|
+
const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}\u0000${overlayText}`;
|
|
436
477
|
visibleStates.push(state);
|
|
437
478
|
if (this.#previousVisibleStates[row] !== state)
|
|
438
479
|
selectionDamagedRows.push(row + 1);
|
|
@@ -579,12 +620,95 @@ export class TranscriptViewport {
|
|
|
579
620
|
this.#functionIds.set(value, id);
|
|
580
621
|
return id;
|
|
581
622
|
}
|
|
623
|
+
#selectionAnchorAt(line) {
|
|
624
|
+
if (line < 0 || line >= this.#selectionRows.length)
|
|
625
|
+
return undefined;
|
|
626
|
+
if (line < this.#viewportHeight) {
|
|
627
|
+
const existing = this.#selectionRowAnchors[line];
|
|
628
|
+
if (this.#frame?.scrollTop === this.#scrollTop)
|
|
629
|
+
return existing;
|
|
630
|
+
// Concurrency: selection edge auto-scroll updates scrollTop before the next frame is
|
|
631
|
+
// composed. Resolve ordinary viewport rows from that latest position instead of stale paint.
|
|
632
|
+
const documentRow = this.#scrollTop + line;
|
|
633
|
+
if (documentRow >= 0 && documentRow < this.#documentRows.length) {
|
|
634
|
+
return documentSelectionAnchor(documentRow, this.#documentRows[documentRow] ?? "");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return this.#selectionRowAnchors[line];
|
|
638
|
+
}
|
|
639
|
+
#projectSelection(documentRows, dockRows, height) {
|
|
640
|
+
const selection = this.#selection;
|
|
641
|
+
const anchors = this.#selectionAnchors;
|
|
642
|
+
if (selection === undefined || anchors === undefined)
|
|
643
|
+
return;
|
|
644
|
+
const project = (anchor) => {
|
|
645
|
+
if (anchor.kind === "dock") {
|
|
646
|
+
const dockIndex = dockRows.length - anchor.fromBottom - 1;
|
|
647
|
+
return dockIndex < 0 || dockIndex >= dockRows.length
|
|
648
|
+
? undefined
|
|
649
|
+
: { line: height - dockRows.length + dockIndex, anchor };
|
|
650
|
+
}
|
|
651
|
+
if (anchor.kind === "screen") {
|
|
652
|
+
return anchor.row < 0 || anchor.row >= this.#selectionRows.length
|
|
653
|
+
|| this.#selectionRows[anchor.row] !== anchor.source
|
|
654
|
+
? undefined
|
|
655
|
+
: { line: anchor.row, anchor };
|
|
656
|
+
}
|
|
657
|
+
if (anchor.row >= 0 && anchor.row < documentRows.length
|
|
658
|
+
&& stripAnsi(documentRows[anchor.row] ?? "").trimEnd() === anchor.identity) {
|
|
659
|
+
const current = documentSelectionAnchor(anchor.row, documentRows[anchor.row] ?? "");
|
|
660
|
+
const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document"
|
|
661
|
+
&& candidate.row === current.row && candidate.identity === current.identity);
|
|
662
|
+
return { line: visible >= 0 ? visible : current.row - this.#scrollTop, anchor: current };
|
|
663
|
+
}
|
|
664
|
+
// Invariant: reflow may move a retained source row, but remapping is allowed only when
|
|
665
|
+
// that exact source has one visible identity. Ambiguous or off-screen replacement clears it.
|
|
666
|
+
const visibleMatches = this.#selectionRowAnchors
|
|
667
|
+
.map((candidate, line) => ({ candidate, line }))
|
|
668
|
+
.filter((entry) => entry.candidate?.kind === "document" && entry.candidate.identity === anchor.identity);
|
|
669
|
+
return visibleMatches.length === 1
|
|
670
|
+
? { line: visibleMatches[0].line, anchor: visibleMatches[0].candidate }
|
|
671
|
+
: undefined;
|
|
672
|
+
};
|
|
673
|
+
const projectedAnchor = project(anchors.anchor);
|
|
674
|
+
const projectedHead = project(anchors.head);
|
|
675
|
+
if (projectedAnchor === undefined || projectedHead === undefined) {
|
|
676
|
+
this.clearSelection();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
this.#selectionAnchors = { anchor: projectedAnchor.anchor, head: projectedHead.anchor };
|
|
680
|
+
if (selection.anchor.line === projectedAnchor.line && selection.head.line === projectedHead.line)
|
|
681
|
+
return;
|
|
682
|
+
this.#selection = {
|
|
683
|
+
...selection,
|
|
684
|
+
anchor: { ...selection.anchor, line: projectedAnchor.line },
|
|
685
|
+
head: { ...selection.head, line: projectedHead.line },
|
|
686
|
+
};
|
|
687
|
+
}
|
|
582
688
|
#updateCopyableSelection() {
|
|
583
|
-
const selection = orderedTextSelection(this.#selection);
|
|
689
|
+
const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
|
|
584
690
|
this.#copyableSelection = selection !== undefined
|
|
585
691
|
&& textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? "")).length > 0;
|
|
586
692
|
}
|
|
587
693
|
}
|
|
694
|
+
function visibleTextSelection(selection, rowCount) {
|
|
695
|
+
if (selection === undefined || rowCount <= 0 || selection.end.line < 0 || selection.start.line >= rowCount)
|
|
696
|
+
return undefined;
|
|
697
|
+
const startLine = Math.max(0, selection.start.line);
|
|
698
|
+
const endLine = Math.min(rowCount - 1, selection.end.line);
|
|
699
|
+
return {
|
|
700
|
+
start: {
|
|
701
|
+
line: startLine,
|
|
702
|
+
column: selection.start.line < 0 ? 0 : selection.start.column,
|
|
703
|
+
},
|
|
704
|
+
end: {
|
|
705
|
+
line: endLine,
|
|
706
|
+
column: selection.end.line >= rowCount ? Number.MAX_SAFE_INTEGER : selection.end.column,
|
|
707
|
+
},
|
|
708
|
+
...(selection.fullRow ? { fullRow: true } : {}),
|
|
709
|
+
...(selection.throughFinalColumn ? { throughFinalColumn: true } : {}),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
588
712
|
function selectionRangeForLine(selection, documentLine, selectableRowCount, width) {
|
|
589
713
|
if (selection === undefined || documentLine >= selectableRowCount
|
|
590
714
|
|| documentLine < selection.start.line || documentLine > selection.end.line)
|
|
@@ -646,20 +770,33 @@ function padViewportRowWithGutter(line, contentWidth, frameWidth) {
|
|
|
646
770
|
const boundedContentWidth = Math.min(Math.max(1, contentWidth), frameWidth);
|
|
647
771
|
const content = padRowPreservingBackground(line, boundedContentWidth);
|
|
648
772
|
const gutterWidth = frameWidth - boundedContentWidth;
|
|
649
|
-
|
|
773
|
+
if (gutterWidth === 0)
|
|
774
|
+
return content;
|
|
775
|
+
// Invariant: render no source glyph beyond contentWidth, but reuse the established
|
|
776
|
+
// row-padding background before stripping source foreground, links, and emphasis.
|
|
777
|
+
const gutterStyle = explicitBackgroundPaddingStyle(line);
|
|
778
|
+
return `${content}${GUTTER_DECORATION_RESET}${gutterStyle?.on ?? ""}${" ".repeat(gutterWidth)}${gutterStyle?.off ?? ""}`;
|
|
779
|
+
}
|
|
780
|
+
function explicitBackgroundPaddingStyle(line) {
|
|
781
|
+
const background = /\u001b\[(?:4[0-8]|10[0-7]|48;(?:2;\d+;\d+;\d+|5;\d+))m/.exec(line)?.[0];
|
|
782
|
+
return background === undefined ? null : { on: background, off: "\u001b[49m" };
|
|
783
|
+
}
|
|
784
|
+
function rowPaddingStyle(line) {
|
|
785
|
+
const background = explicitBackgroundPaddingStyle(line);
|
|
786
|
+
if (background !== null)
|
|
787
|
+
return background;
|
|
788
|
+
const reverse = /\u001b\[(?:7(?:;[0-9]+)*|[0-9;]*;7(?:;[0-9]+)*)m/.exec(line)?.[0];
|
|
789
|
+
return reverse === undefined ? null : { on: reverse, off: "\u001b[27m" };
|
|
650
790
|
}
|
|
651
791
|
function padRowPreservingBackground(line, width) {
|
|
652
792
|
const shown = displayWidth(line) > width ? truncateToWidth(line, width) : line;
|
|
653
793
|
const padding = Math.max(0, width - displayWidth(shown));
|
|
654
794
|
if (padding === 0)
|
|
655
795
|
return shown;
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (reverse)
|
|
661
|
-
return `${shown}${reverse}${" ".repeat(padding)}\u001b[27m`;
|
|
662
|
-
return shown + " ".repeat(padding);
|
|
796
|
+
const style = rowPaddingStyle(line);
|
|
797
|
+
return style === null
|
|
798
|
+
? shown + " ".repeat(padding)
|
|
799
|
+
: `${shown}${style.on}${" ".repeat(padding)}${style.off}`;
|
|
663
800
|
}
|
|
664
801
|
export function assertTranscriptViewportFrameDescriptor(descriptor) {
|
|
665
802
|
if (!Number.isSafeInteger(descriptor.frameId) || descriptor.frameId < 1)
|
|
@@ -91,6 +91,15 @@ its own `core` facade layer; A1 is a product, so the port adapts imports and kee
|
|
|
91
91
|
as styling callbacks rather than imports or literal colours. Engine producers remain semantic;
|
|
92
92
|
pinned presentation keeps its three periods; and the source-synchronized Pi status indicators,
|
|
93
93
|
installed packages, `a1 pi`, and vanilla Pi remain untouched.
|
|
94
|
+
- **Surface-relative frame selection and layout-neutral copy feedback.** Bare A1's complete-frame
|
|
95
|
+
selection extends the reference transcript selector across scrollable document and pinned dock
|
|
96
|
+
surfaces. Selection endpoints retain those surface identities: followed agent output carries its
|
|
97
|
+
highlight with the source row, while footer, status, prompt, widget, and notice selection remains
|
|
98
|
+
pinned. The copied-character acknowledgement is final-frame paint over the existing row above the
|
|
99
|
+
input, not a dock child, so appearing, replacement, and expiry do not reallocate the viewport or
|
|
100
|
+
move editor, footer, controls, or selection. Bounded row provenance clears ambiguous reflow rather
|
|
101
|
+
than transferring selection to unrelated terminal cells. The comparison profile retains pinned Pi
|
|
102
|
+
selection and notification behavior.
|
|
94
103
|
- **Keyboard presentation scheduling.** Pinned components, semantic key handling, and the
|
|
95
104
|
comparison profiles stay unchanged. Bare A1 adds one A1-owned event-loop-turn coordinator
|
|
96
105
|
before its fullscreen root and may omit only superseded visual states after every delivery
|
|
@@ -24,7 +24,9 @@ node scripts/governance/local-worktree-cleanup.mjs sweep --repo D:/Git/a1
|
|
|
24
24
|
|
|
25
25
|
### Nothing left to remove
|
|
26
26
|
|
|
27
|
-
A released entry whose evidence can never verify (for example `delivery-content-drift` after a stale-base merge, or `source-association` for a PR without an `openspec-implementation` fence) would otherwise be re-verified and reported `blocked` on every sweep.
|
|
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