@timurproko/a1 0.1.8-dev.583 → 0.1.8-dev.588

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.
@@ -398,7 +398,7 @@ export class SessionViewportController {
398
398
  && (event.column !== this.#pendingEditorClick.column || event.row !== this.#pendingEditorClick.row)) {
399
399
  this.#pendingEditorClick = undefined;
400
400
  }
401
- this.#updateSelectionAutoScroll(event.column, event.row, hits.viewportHeight);
401
+ this.#updateSelectionAutoScroll(event.column, event.row, hits.viewportHeight, frame.rows.length);
402
402
  activity = true;
403
403
  return true;
404
404
  }
@@ -555,8 +555,12 @@ export class SessionViewportController {
555
555
  .find(candidate => column - 1 >= candidate.from && column - 1 < candidate.to);
556
556
  return range === undefined ? undefined : JSON.stringify([row, range]);
557
557
  }
558
- #updateSelectionAutoScroll(column, row, frameHeight) {
559
- const beyondEdge = row <= 1 || row >= frameHeight;
558
+ #updateSelectionAutoScroll(column, row, viewportHeight, frameHeight) {
559
+ // Invariant: the last content row stays selectable; only the pointer below it scrolls,
560
+ // unless no row exists below. The top row scrolls because nothing lies above it. A
561
+ // dock-originated gesture never moves the transcript behind the fixed dock.
562
+ const beyondEdge = this.#viewport.selectionFromContent
563
+ && (row <= 1 || row > viewportHeight || row >= frameHeight);
560
564
  if (!beyondEdge) {
561
565
  this.#stopSelectionAutoScroll();
562
566
  return;
@@ -4,7 +4,8 @@ import { PiModalFrame } from "./modal-frame.js";
4
4
  import { piTheme, renderPiModalShortcutHints } from "./theme.js";
5
5
  const MAX_VISIBLE_ROWS = 10;
6
6
  const MODELS_TITLE = "Models";
7
- const REFRESHED_TITLE_DURATION_MS = 1_000;
7
+ const REFRESHING_TITLE_MIN_DURATION_MS = 1_000;
8
+ const REFRESHED_TITLE_DURATION_MS = 2_000;
8
9
  class ModalTitleLine {
9
10
  #text = "";
10
11
  setText(text) { this.#text = text; }
@@ -51,6 +52,9 @@ export class ModelsDialogComponent {
51
52
  #selectedIndex = 0;
52
53
  #preferredId;
53
54
  #refreshStatus;
55
+ #refreshing = false;
56
+ #refreshStartedAt;
57
+ #refreshOutcomeTimer;
54
58
  #refreshed = false;
55
59
  #refreshDismissalTimer;
56
60
  #disposed = false;
@@ -61,7 +65,10 @@ export class ModelsDialogComponent {
61
65
  this.#scopeIds = [...config.scopeIds];
62
66
  this.#savedScopeIds = [...config.savedScopeIds];
63
67
  this.#filter = config.initialFilter ?? "all";
64
- this.#refreshStatus = config.refreshStatus === undefined ? undefined : { message: config.refreshStatus, kind: "muted" };
68
+ if (config.refreshStatus !== undefined) {
69
+ this.#refreshing = true;
70
+ this.#refreshStartedAt = Date.now();
71
+ }
65
72
  if (config.initialQuery)
66
73
  this.#input.setValue(config.initialQuery);
67
74
  this.#replaceModels(config.models);
@@ -101,29 +108,41 @@ export class ModelsDialogComponent {
101
108
  setRefreshStatus(message, kind) {
102
109
  if (this.#disposed)
103
110
  return;
104
- this.#clearRefreshDismissal();
105
- if (kind !== "success") {
111
+ if (kind === "muted") {
112
+ this.#clearRefreshOutcome();
113
+ this.#clearRefreshDismissal();
114
+ this.#refreshStatus = undefined;
115
+ this.#refreshing = true;
116
+ this.#refreshStartedAt = Date.now();
106
117
  this.#refreshed = false;
107
- this.#refreshStatus = { message, kind };
108
118
  return;
109
119
  }
110
- this.#refreshStatus = undefined;
111
- this.#refreshed = true;
112
- const timer = setTimeout(() => {
113
- if (this.#refreshDismissalTimer !== timer)
114
- return;
115
- this.#refreshDismissalTimer = undefined;
116
- if (this.#disposed)
117
- return;
118
- this.#refreshed = false;
119
- this.#callbacks.requestRender();
120
- }, REFRESHED_TITLE_DURATION_MS);
121
- timer.unref?.();
122
- this.#refreshDismissalTimer = timer;
120
+ const visibleMs = this.#refreshStartedAt === undefined ? REFRESHING_TITLE_MIN_DURATION_MS : Date.now() - this.#refreshStartedAt;
121
+ const remainingMs = Math.max(0, REFRESHING_TITLE_MIN_DURATION_MS - visibleMs);
122
+ if (this.#refreshing && remainingMs > 0) {
123
+ this.#clearRefreshOutcome();
124
+ const timer = setTimeout(() => {
125
+ if (this.#refreshOutcomeTimer !== timer)
126
+ return;
127
+ this.#refreshOutcomeTimer = undefined;
128
+ if (this.#disposed)
129
+ return;
130
+ this.#applyRefreshOutcome(message, kind);
131
+ this.#callbacks.requestRender();
132
+ }, remainingMs);
133
+ timer.unref?.();
134
+ this.#refreshOutcomeTimer = timer;
135
+ return;
136
+ }
137
+ this.#clearRefreshOutcome();
138
+ this.#applyRefreshOutcome(message, kind);
123
139
  }
124
140
  dispose() {
125
141
  this.#disposed = true;
142
+ this.#clearRefreshOutcome();
126
143
  this.#clearRefreshDismissal();
144
+ this.#refreshing = false;
145
+ this.#refreshStartedAt = undefined;
127
146
  this.#refreshed = false;
128
147
  }
129
148
  invalidate() {
@@ -224,9 +243,36 @@ export class ModelsDialogComponent {
224
243
  const theme = piTheme();
225
244
  this.#title.setText(theme.fg("accent", theme.bold(MODELS_TITLE))
226
245
  + (this.dirty ? theme.fg("warning", " (unsaved)") : "")
246
+ + (this.#refreshing ? theme.fg("muted", " (refreshing)") : "")
227
247
  + (this.#refreshed ? theme.fg("success", " (refreshed)") : ""));
228
248
  return this.#frame.render(width);
229
249
  }
250
+ #applyRefreshOutcome(message, kind) {
251
+ this.#clearRefreshDismissal();
252
+ this.#refreshing = false;
253
+ this.#refreshStartedAt = undefined;
254
+ this.#refreshed = kind === "success";
255
+ this.#refreshStatus = kind === "warning" ? { message, kind } : undefined;
256
+ if (kind !== "success")
257
+ return;
258
+ const timer = setTimeout(() => {
259
+ if (this.#refreshDismissalTimer !== timer)
260
+ return;
261
+ this.#refreshDismissalTimer = undefined;
262
+ if (this.#disposed)
263
+ return;
264
+ this.#refreshed = false;
265
+ this.#callbacks.requestRender();
266
+ }, REFRESHED_TITLE_DURATION_MS);
267
+ timer.unref?.();
268
+ this.#refreshDismissalTimer = timer;
269
+ }
270
+ #clearRefreshOutcome() {
271
+ if (this.#refreshOutcomeTimer === undefined)
272
+ return;
273
+ clearTimeout(this.#refreshOutcomeTimer);
274
+ this.#refreshOutcomeTimer = undefined;
275
+ }
230
276
  #clearRefreshDismissal() {
231
277
  if (this.#refreshDismissalTimer === undefined)
232
278
  return;
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-24T17:46:17.847Z",
8
+ "builtAt": "2026-09-25T08:17:55.130Z",
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-24T17:46:08.913Z",
8
+ "builtAt": "2026-09-25T08:17:49.253Z",
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-24T17:46:57.821Z",
8
+ "builtAt": "2026-09-25T08:18:48.658Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "8337e1cdc9ea513c3e4567120db5b6e1275d6daec8535dc1be025fc41da1044a",
11
+ "sha256": "56eab3f4e22daaf23780b05cf751fb4a0eb62185877dcb18ebbea2123ece8595",
12
12
  "size": 178176
13
13
  },
14
14
  "provenance": {
@@ -24,7 +24,7 @@ export type ScrollbarStyle = "thin" | "thick";
24
24
  export type ScrollbarSpeed = "normal" | "fast" | "high";
25
25
  /** Wheel distance: normal (3), fast (6), high as normal + fast (9). */
26
26
  export declare function scrollbarWheelRows(speed: ScrollbarSpeed): number;
27
- /** Selection edge distance uses the same configured rate as a wheel event. */
27
+ /** Selection edge distance per auto-scroll tick: normal (1), fast (2), high (3). */
28
28
  export declare function scrollbarSelectionRows(speed: ScrollbarSpeed): number;
29
29
  export interface ScrollbarPresentationInput {
30
30
  readonly geometry: ScrollbarGeometry | null;
@@ -7,9 +7,9 @@
7
7
  export function scrollbarWheelRows(speed) {
8
8
  return speed === "high" ? 9 : speed === "fast" ? 6 : 3;
9
9
  }
10
- /** Selection edge distance uses the same configured rate as a wheel event. */
10
+ /** Selection edge distance per auto-scroll tick: normal (1), fast (2), high (3). */
11
11
  export function scrollbarSelectionRows(speed) {
12
- return scrollbarWheelRows(speed);
12
+ return speed === "high" ? 3 : speed === "fast" ? 2 : 1;
13
13
  }
14
14
  /** Pure appearance decision; activity time is supplied so tests own the clock. */
15
15
  export function scrollbarPresentation(input) {
@@ -162,6 +162,8 @@ export declare class TranscriptViewport {
162
162
  setStickyHovered(hovered: boolean): void;
163
163
  get selectionActive(): boolean;
164
164
  get hasSelection(): boolean;
165
+ /** True when the active gesture began on transcript rows rather than the dock. */
166
+ get selectionFromContent(): boolean;
165
167
  pressSelection(column: number, frameRow: number, now?: number): boolean;
166
168
  extendSelection(column: number, frameRow: number, now?: number, autoScroll?: boolean): boolean;
167
169
  releaseSelection(): boolean;
@@ -57,6 +57,7 @@ export class TranscriptViewport {
57
57
  #contentWidth = 0;
58
58
  #selectionWidth = 0;
59
59
  #viewportHeight = 0;
60
+ #firstSelectableRow = 0;
60
61
  #selectionRowAnchors = [];
61
62
  #frameId = 0;
62
63
  #frame = null;
@@ -88,12 +89,14 @@ export class TranscriptViewport {
88
89
  setStickyHovered(hovered) { this.#stickyHovered = hovered; }
89
90
  get selectionActive() { return this.#selection?.selecting === true; }
90
91
  get hasSelection() { return orderedTextSelection(this.#selection) !== undefined; }
92
+ /** True when the active gesture began on transcript rows rather than the dock. */
93
+ get selectionFromContent() { return this.#selectionAnchors !== undefined && this.#selectionAnchors.anchor.kind !== "dock"; }
91
94
  pressSelection(column, frameRow, now = Date.now()) {
92
95
  const frameHeight = this.#frame?.rows.length ?? 0;
93
96
  const selectionWidth = frameRow <= (this.#frame?.hits.viewportHeight ?? 0)
94
97
  ? this.#contentWidth
95
98
  : this.#selectionWidth;
96
- if (frameRow < 1 || frameRow > frameHeight || column < 1 || column > selectionWidth)
99
+ if (frameRow <= this.#firstSelectableRow || frameRow > frameHeight || column < 1 || column > selectionWidth)
97
100
  return false;
98
101
  const line = frameRow - 1;
99
102
  const pressed = pressTextSelection({
@@ -170,7 +173,7 @@ export class TranscriptViewport {
170
173
  return true;
171
174
  }
172
175
  selectedText() {
173
- const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
176
+ const selection = this.#visibleSelection();
174
177
  if (selection === undefined)
175
178
  return null;
176
179
  const text = textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? ""));
@@ -388,9 +391,8 @@ export class TranscriptViewport {
388
391
  ? documentSelectionAnchor(documentRow, documentRows[documentRow] ?? "")
389
392
  : { kind: "screen", row, source: this.#selectionRows[row] ?? "" };
390
393
  });
391
- if (stickyActive && governing !== null && visibleAnchors.length > 0) {
392
- visibleAnchors[0] = documentSelectionAnchor(governing.firstRow, documentRows[governing.firstRow] ?? governing.sourceRow);
393
- }
394
+ // Invariant: a pinned prompt is chrome over its hidden source row, which clips like off-screen rows.
395
+ this.#firstSelectableRow = stickyActive && viewportHeight > 0 ? 1 : 0;
394
396
  this.#selectionRowAnchors = [
395
397
  ...visibleAnchors,
396
398
  ...dock.map((_row, index) => ({ kind: "dock", fromBottom: dock.length - index - 1 })),
@@ -400,7 +402,7 @@ export class TranscriptViewport {
400
402
  // Concurrency: a painter may synchronously route input in tests or host integrations.
401
403
  // Label this frame with the exact selection snapshot it began composing.
402
404
  const composingSelectionRevision = this.#selectionRevision;
403
- const orderedSelection = orderedTextSelection(this.#selection);
405
+ const orderedSelection = this.#visibleSelection();
404
406
  const recomputedRows = [];
405
407
  const reusedRows = [];
406
408
  const selectionDamagedRows = [];
@@ -408,6 +410,7 @@ export class TranscriptViewport {
408
410
  const frameRows = [...visible, ...dock].slice(0, height);
409
411
  const selectionRows = [...this.#selectionRows];
410
412
  let bottomHit = null;
413
+ let control;
411
414
  if (this.#maxScroll > 0 && !this.#followingEnd && frameRows.length > 0) {
412
415
  const genericLabel = " Jump to bottom (Ctrl+End) ↓ ";
413
416
  const countedLabel = this.#newMessages > 0
@@ -424,8 +427,10 @@ export class TranscriptViewport {
424
427
  const pointer = input.pointerPosition;
425
428
  const bottomHovered = pointer !== undefined && pointer.row === bottomHit.row
426
429
  && pointer.column >= bottomHit.columnStart && pointer.column <= bottomHit.columnEnd;
427
- frameRows[row] = overlaySpan(padRowPreservingBackground(frameRows[row] ?? "", width), left, left + labelWidth, `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}`);
428
- selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, label);
430
+ control = { row, left, text: `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}` };
431
+ frameRows[row] = overlaySpan(padRowPreservingBackground(frameRows[row] ?? "", width), left, left + labelWidth, control.text);
432
+ // Invariant: the control floats above selection; its cells are never selected or copied.
433
+ selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, " ".repeat(labelWidth));
429
434
  }
430
435
  }
431
436
  for (const covered of input.coveredCells ?? []) {
@@ -442,15 +447,22 @@ export class TranscriptViewport {
442
447
  const painted = frameRows[row] ?? "";
443
448
  const rowSelectionWidth = row < viewportHeight ? contentWidth : width;
444
449
  const range = selectionRangeForLine(orderedSelection, row, this.#selectionRows.length, rowSelectionWidth);
445
- const padded = row < viewportHeight || range !== null;
450
+ // Invariant: the reserved rail gutter is not a semantic selection or copy cell,
451
+ // but a range that reaches the content boundary paints through it so the rail
452
+ // floats above one continuous selection surface.
453
+ const paintRange = range !== null && row < viewportHeight
454
+ && contentWidth < width && range.to === contentWidth
455
+ ? { from: range.from, to: width }
456
+ : range;
457
+ const padded = row < viewportHeight || paintRange !== null;
446
458
  const base = cachedString(this.#baseRowCache, `${width}\u0000${contentWidth}\u0000${row < viewportHeight}\u0000${padded}\u0000${painted}`, cacheLimit, () => row < viewportHeight
447
459
  ? padViewportRowWithGutter(painted, contentWidth, width)
448
460
  : padded ? padRowPreservingBackground(painted, width) : painted);
449
461
  let rowRecomputed = row < viewportHeight && paintRecomputedRows.has(row) || !base.reused;
450
- const rangeKey = range === null ? "-" : `${range.from}:${range.to}`;
451
- const selected = range === null
462
+ const rangeKey = paintRange === null ? "-" : `${paintRange.from}:${paintRange.to}`;
463
+ const selected = paintRange === null
452
464
  ? { value: base.value, reused: true }
453
- : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, range.from, range.to));
465
+ : cachedString(this.#selectedRowCache, `${selectionPainterId}\u0000${rangeKey}\u0000${base.value}`, cacheLimit, () => theme.selection(base.value, paintRange.from, paintRange.to));
454
466
  rowRecomputed ||= !selected.reused;
455
467
  // Invariant: paint the rail after selection over the gutter's continued row
456
468
  // background. Source content and selection still end at the preceding cell.
@@ -471,7 +483,9 @@ export class TranscriptViewport {
471
483
  return overlayWidth === 0 ? withRail : overlaySpan(padRowPreservingBackground(withRail, width), overlayLimit - overlayWidth, overlayLimit, `${CONTROL_STYLE_RESET}${overlayText}`);
472
484
  });
473
485
  rowRecomputed ||= !final.reused;
474
- frameRows[row] = final.value;
486
+ frameRows[row] = control?.row === row && paintRange !== null && bottomHit !== null
487
+ ? overlaySpan(final.value, control.left, bottomHit.columnEnd, control.text)
488
+ : final.value;
475
489
  (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
476
490
  const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}\u0000${overlayText}`;
477
491
  visibleStates.push(state);
@@ -685,21 +699,23 @@ export class TranscriptViewport {
685
699
  head: { ...selection.head, line: projectedHead.line },
686
700
  };
687
701
  }
702
+ #visibleSelection() {
703
+ const rows = this.#selectionAnchors?.anchor.kind === "document" ? this.#viewportHeight : this.#selectionRows.length;
704
+ return visibleTextSelection(orderedTextSelection(this.#selection), rows, this.#firstSelectableRow);
705
+ }
688
706
  #updateCopyableSelection() {
689
- const selection = visibleTextSelection(orderedTextSelection(this.#selection), this.#selectionRows.length);
690
- this.#copyableSelection = selection !== undefined
691
- && textSelectionText(selection, this.#selectionRows, line => usefulTextLineContent(this.#selectionRows[line] ?? "")).length > 0;
707
+ this.#copyableSelection = this.selectedText() !== null;
692
708
  }
693
709
  }
694
- function visibleTextSelection(selection, rowCount) {
695
- if (selection === undefined || rowCount <= 0 || selection.end.line < 0 || selection.start.line >= rowCount)
710
+ function visibleTextSelection(selection, rowCount, firstRow) {
711
+ if (selection === undefined || rowCount <= firstRow || selection.end.line < firstRow || selection.start.line >= rowCount)
696
712
  return undefined;
697
- const startLine = Math.max(0, selection.start.line);
713
+ const startLine = Math.max(firstRow, selection.start.line);
698
714
  const endLine = Math.min(rowCount - 1, selection.end.line);
699
715
  return {
700
716
  start: {
701
717
  line: startLine,
702
- column: selection.start.line < 0 ? 0 : selection.start.column,
718
+ column: selection.start.line < firstRow ? 0 : selection.start.column,
703
719
  },
704
720
  end: {
705
721
  line: endLine,
@@ -46,6 +46,7 @@ Pending review preserves a possible external interface, not an obsolete private
46
46
  | `SOURCE_LEDGER_PATH`, `SOURCE_LEDGER_SCAN_ROOT`, `SOURCE_LEDGER_PORT_ROOT` | Source-ledger governance fixture overrides; produced by its tests and consumed by `check-pinned-pi-source-ledger.mjs`. |
47
47
  | `RELEASE_RUNNER_LABEL` | Release/full-regression workflows supply certification-runner provenance to `run-release-gates.mjs`. |
48
48
  | `TERMINAL_FIXTURE_INPUT`, `TERMINAL_FIXTURE_TOKEN` | Native terminal probe producer/consumer in `native/terminal-host/src/main.rs`; expected input and fixture token. |
49
+ | `TERMINAL_HOST_LOCAL_BUILD` | Developer shell sets exactly `1`; `native/terminal-host/build.rs` then permits a Windows build outside CI. |
49
50
  | `DOCS_AUTO_MERGE_POLL_ATTEMPTS`, `DOCS_AUTO_MERGE_POLL_MS` | Documentation automation polling controls, produced by governance tests and consumed by `manage-documentation-auto-merge.mjs`. |
50
51
  | `VALIDATION_JOB_STARTED_MS`, `RENDERING_JOB_STARTED_MS` | CI timing handoffs within `.github/workflows/ci.yml`; epoch milliseconds. |
51
52
  | `RUN_PROCESS_CONTAINMENT_INTEGRATION` | CI enables the real containment fixture with exactly `1`; platform containment tests consume it. |
@@ -32,6 +32,8 @@ Conservative Development retains every pull-request owner but does not schedule
32
32
  node scripts/release/generate-validation-ownership-ledger.mjs --output .artifacts/validation/ownership-ledger.json
33
33
  ```
34
34
 
35
+ The `terminal-host` owner is CI-only. A change under `native/terminal-host/` or to its run scripts selects the Windows x64 **Native terminal host (Windows)** job, which builds the crate with pinned Rust and Zig 0.15.2, runs `npm run test:terminal-host`, and uploads the debug executable. Local Windows builds fail in `build.rs` unless `TERMINAL_HOST_LOCAL_BUILD=1` is set, because Zig's package fetch unpacks test data that workstation antivirus quarantines along with the installed process guardian. See [`native/terminal-host/README.md`](../native/terminal-host/README.md).
36
+
35
37
  ## Failed-job reruns and attempt evidence
36
38
 
37
39
  Every modular outcome, content-free job envelope, and uploaded artifact name is qualified by `github.run_attempt`. Authority remains bound to the workflow run ID, exact head, complete selection identity, logical job, and platform/runtime target.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.583",
3
+ "version": "0.1.8-dev.588",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",