@timurproko/a1 0.2.7-dev.809 → 0.2.7-dev.820

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 (99) hide show
  1. package/README.md +4 -2
  2. package/dist/app/session-shell/resident-tab-coordinator.d.ts +6 -9
  3. package/dist/app/session-shell/resident-tab-coordinator.js +9 -52
  4. package/dist/app/session-shell/response-copy-transport.d.ts +13 -0
  5. package/dist/app/session-shell/response-copy-transport.js +16 -7
  6. package/dist/app/session-shell/session-shell-root.d.ts +1 -2
  7. package/dist/app/session-shell/session-shell.js +2 -2
  8. package/dist/app/session-shell/session-viewport-controller.js +6 -4
  9. package/dist/composition/owned-ui.js +1 -2
  10. package/dist/features/owned-ui/settings-app.js +1 -1
  11. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  12. package/dist/foundation/release/progress-palette.generated.js +2 -2
  13. package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
  14. package/dist/foundation/resident-tabs/bridge-client.js +1 -1
  15. package/dist/foundation/resident-tabs/settings.d.ts +0 -1
  16. package/dist/foundation/resident-tabs/settings.js +0 -2
  17. package/dist/foundation/startup/startup-descriptor.js +2 -2
  18. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +2 -2
  19. package/dist/integrations/pi/components/resources/builtin-themes.js +2 -2
  20. package/dist/integrations/pi/components/shell-extension-ui.js +79 -60
  21. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  22. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +1 -1
  23. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  24. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  25. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  26. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  27. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  28. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  29. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  30. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  31. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  32. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  33. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  34. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  35. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  36. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  37. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  38. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  39. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  40. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +1 -1
  41. package/dist/integrations/pi/components/upstream/components/status-indicator.js +1 -1
  42. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  43. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  44. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  45. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  46. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +2 -4
  47. package/dist/integrations/pi/components/upstream/components/tool-execution.js +77 -98
  48. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  49. package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
  50. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  51. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  52. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +1 -1
  53. package/dist/integrations/pi/components/upstream/history/editor-core.js +1 -1
  54. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  55. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  56. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  57. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  58. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  59. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  60. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  61. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  62. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  63. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  64. package/dist/integrations/pi/components/upstream/theme/theme.js +1 -1
  65. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  66. package/dist/integrations/pi/startup-public.js +164 -150
  67. package/dist/integrations/pi/startup-public.manifest.json +351 -310
  68. package/dist/integrations/pi/tui-runtime/adapter.js +4 -0
  69. package/dist/integrations/pi/tui-runtime/contracts.d.ts +5 -0
  70. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  71. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  72. package/dist/native/darwin-arm64/manifest.json +1 -1
  73. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  74. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  75. package/dist/native/darwin-x64/manifest.json +1 -1
  76. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  77. package/dist/native/darwin-x64/terminal-host.node +0 -0
  78. package/dist/native/linux-x64/manifest.json +1 -1
  79. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  80. package/dist/native/linux-x64/terminal-host.node +0 -0
  81. package/dist/native/win32-x64/manifest.json +2 -2
  82. package/dist/native/win32-x64/process-guardian.node +0 -0
  83. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  84. package/dist/native/win32-x64/terminal-host.node +0 -0
  85. package/dist/runtime-payload-inventory.json +5181 -5369
  86. package/dist/ui/components/dialog-panel.js +1 -1
  87. package/dist/ui/components/prompt-input.d.ts +2 -2
  88. package/dist/ui/components/prompt-input.js +2 -2
  89. package/dist/ui/components/submitted-prompt.d.ts +2 -2
  90. package/dist/ui/components/submitted-prompt.js +3 -3
  91. package/dist/ui/components/transcript-viewport.d.ts +1 -1
  92. package/dist/ui/components/transcript-viewport.js +151 -43
  93. package/dist/ui/settings/declarations.d.ts +13 -9
  94. package/dist/ui/settings/declarations.js +10 -7
  95. package/dist/ui/settings/migrations.js +7 -0
  96. package/docs/architecture/resident-tabs.md +6 -6
  97. package/docs/architecture/response-copy-delivery.md +1 -1
  98. package/docs/architecture/toolchain.md +2 -2
  99. package/package.json +5 -5
@@ -69,7 +69,7 @@ export function renderSteppedDialogPanel(state, width, theme) {
69
69
  });
70
70
  const input = state.input === undefined
71
71
  ? []
72
- : [...renderInputRow(state.input, width, { ruled: false, promptGlyph: "> ", theme }).lines, ""];
72
+ : [...renderInputRow(state.input, width, { ruled: false, promptGlyph: "", theme }).lines, ""];
73
73
  const step = `(step ${state.step}/${state.steps})`;
74
74
  const title = `${state.title} ${step}`;
75
75
  const paintedTitle = `${theme.bold(theme.fg("accent", state.title))} ${theme.fg("dim", step)}`;
@@ -1,6 +1,6 @@
1
1
  import { type UiTheme } from "./theme.js";
2
- export declare const PROMPT_GLYPH = "\u276F ";
3
- export declare const PROMPT_PREFIX_WIDTH = 2;
2
+ export declare const PROMPT_GLYPH = "";
3
+ export declare const PROMPT_PREFIX_WIDTH = 0;
4
4
  type RuleTheme = Pick<UiTheme, "fg">;
5
5
  export declare function promptRuleText(text: string, theme: RuleTheme): string;
6
6
  export declare function promptRule(width: number, theme: RuleTheme): string;
@@ -1,7 +1,7 @@
1
1
  import { displayWidth, truncateToWidth } from "./text.js";
2
2
  import { PLAIN_THEME } from "./theme.js";
3
- export const PROMPT_GLYPH = "❯ ";
4
- export const PROMPT_PREFIX_WIDTH = 2;
3
+ export const PROMPT_GLYPH = "";
4
+ export const PROMPT_PREFIX_WIDTH = 0;
5
5
  export function promptRuleText(text, theme) {
6
6
  return theme.fg("border", text);
7
7
  }
@@ -1,8 +1,8 @@
1
1
  export type UiTimeFormat = "20:00" | "8:00 PM";
2
2
  /** Deterministic local hour/minute presentation shared by A1-owned time labels. */
3
3
  export declare function formatLocalTime(source: Date | number, format?: UiTimeFormat): string | null;
4
- /** Visible width of the submitted-prompt prefix (`❯ `). */
5
- export declare const SUBMITTED_PROMPT_PREFIX_WIDTH = 2;
4
+ /** Visible width of the submitted-prompt prefix (` ❯ `). */
5
+ export declare const SUBMITTED_PROMPT_PREFIX_WIDTH = 3;
6
6
  export interface SubmittedPromptStyle {
7
7
  readonly prefix?: (text: string) => string;
8
8
  readonly timestamp?: (text: string) => string;
@@ -10,8 +10,8 @@ export function formatLocalTime(source, format = "8:00 PM") {
10
10
  return `${String(hour).padStart(2, "0")}:${minute}`;
11
11
  return `${hour % 12 || 12}:${minute} ${hour < 12 ? "AM" : "PM"}`;
12
12
  }
13
- /** Visible width of the submitted-prompt prefix (`❯ `). */
14
- export const SUBMITTED_PROMPT_PREFIX_WIDTH = 2;
13
+ /** Visible width of the submitted-prompt prefix (` ❯ `). */
14
+ export const SUBMITTED_PROMPT_PREFIX_WIDTH = 3;
15
15
  const TIMESTAMP_MARGIN = 3;
16
16
  const MIN_USEFUL_CONTENT_WIDTH = 8;
17
17
  /** Formats transcript metadata, never render time, in the active A1 time format. */
@@ -42,7 +42,7 @@ export function composeSubmittedPromptRows(contentRows, width, source, style = {
42
42
  if (width <= 0)
43
43
  return [];
44
44
  const layout = submittedPromptLayout(width, source, format);
45
- const prefix = (style.prefix ?? identity)("❯ ");
45
+ const prefix = (style.prefix ?? identity)(" ❯ ");
46
46
  const rows = (contentRows.length === 0 ? [""] : contentRows).map((row, index) => {
47
47
  const content = truncateToWidth(row, layout.contentWidth);
48
48
  return `${index === 0 ? prefix : " ".repeat(SUBMITTED_PROMPT_PREFIX_WIDTH)}${content}`;
@@ -178,7 +178,7 @@ export declare class TranscriptViewport {
178
178
  releaseSelection(): boolean;
179
179
  clearSelection(): boolean;
180
180
  selectedText(): string | null;
181
- /** Captures visible selected text, not off-screen history. */
181
+ /** Captures selected text, including document rows of a visible selection scrolled off-screen. */
182
182
  captureSelectedText(): SelectionCopySnapshot | null;
183
183
  scrollBy(lines: number, now?: number): boolean;
184
184
  scrollTo(position: number, now?: number): boolean;
@@ -4,6 +4,10 @@ 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
+ // Protocol: upstream OSC 133 zones are layout annotations, not fullscreen cursor commands.
8
+ // Styling/padding can move them away from the row prefix Pi strips; 133;A then advances a real
9
+ // terminal parser to a fresh line. The custom viewport owns prompt anchors separately.
10
+ const VIEWPORT_PROMPT_ZONES = /\u001b\]133;[ABC](?:\u0007|\u001b\\)/gu;
7
11
  // Invariant: scrollbar chrome closes source links and decoration without clearing
8
12
  // the row surface painted beneath its dedicated, non-semantic gutter.
9
13
  const GUTTER_DECORATION_RESET = "\u001b]8;;\u001b\\\u001b[22;23;24;25;27;28;29;39;54;55m";
@@ -11,6 +15,19 @@ const IDENTITY_ROW = (row) => row;
11
15
  function documentSelectionAnchor(row, source, liveFromBottom) {
12
16
  return { kind: "document", row, source, identity: stripAnsi(source).trimEnd(), liveFromBottom };
13
17
  }
18
+ function sameSelectionRowAnchor(left, right) {
19
+ if (left.kind !== right.kind)
20
+ return false;
21
+ if (left.kind === "document" && right.kind === "document") {
22
+ return left.row === right.row && left.source === right.source && left.liveFromBottom === right.liveFromBottom;
23
+ }
24
+ if (left.kind === "screen" && right.kind === "screen") {
25
+ return left.row === right.row && left.source === right.source;
26
+ }
27
+ if (left.kind === "transient" && right.kind === "transient")
28
+ return left.fromBottom === right.fromBottom;
29
+ return left.kind === "dock" && right.kind === "dock" && left.fromBottom === right.fromBottom;
30
+ }
14
31
  function viewportSelectionAnchor(row, source, selectableDocumentRowCount, documentRowCount) {
15
32
  return row < selectableDocumentRowCount
16
33
  ? documentSelectionAnchor(row, source)
@@ -136,7 +153,12 @@ export class TranscriptViewport {
136
153
  this.scrollBy(1, now);
137
154
  else if (autoScroll && frameRow <= 1 && this.#scrollTop > 0)
138
155
  this.scrollBy(-1, now);
139
- const line = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
156
+ const pointerLine = clamp(frameRow - 1, 0, this.#selectionRows.length - 1);
157
+ // Invariant: a pinned prompt alias is chrome; an upward drag reaching it keeps the pointer
158
+ // column on the first visible content row.
159
+ const line = pointerLine < this.#firstSelectableRow && pointerLine < selection.anchor.line
160
+ ? this.#firstSelectableRow
161
+ : pointerLine;
140
162
  const selectionWidth = line < (this.#frame?.hits.viewportHeight ?? 0)
141
163
  ? this.#contentWidth
142
164
  : this.#selectionWidth;
@@ -147,12 +169,13 @@ export class TranscriptViewport {
147
169
  ? { line, column: targetColumn }
148
170
  : textSelectionPointAt(line, targetColumn, this.#selectionRows[line] ?? "");
149
171
  const next = extendTextSelection(selection, point);
150
- if (next !== selection) {
151
- const headAnchor = this.#selectionAnchorAt(line);
152
- if (headAnchor === undefined || this.#selectionAnchors === undefined) {
153
- this.clearSelection();
154
- return true;
155
- }
172
+ const headAnchor = this.#selectionAnchorAt(line);
173
+ if (headAnchor === undefined || this.#selectionAnchors === undefined) {
174
+ this.clearSelection();
175
+ return true;
176
+ }
177
+ // Invariant: edge auto-scroll changes the document row under a still pointer; that advances the head.
178
+ if (next !== selection || !sameSelectionRowAnchor(this.#selectionAnchors.head, headAnchor)) {
156
179
  this.#selection = next;
157
180
  this.#selectionAnchors = { ...this.#selectionAnchors, head: headAnchor };
158
181
  this.#selectionRevision += 1;
@@ -180,25 +203,66 @@ export class TranscriptViewport {
180
203
  return true;
181
204
  }
182
205
  selectedText() {
183
- const selection = this.#visibleSelection();
184
- if (selection === undefined)
206
+ const visible = this.#visibleSelection();
207
+ if (visible === undefined)
185
208
  return null;
186
- const copiedLines = Array.from({ length: Math.max(0, selection.end.line - selection.start.line + 1) }, (_value, index) => selection.start.line + index).filter(line => {
209
+ const offscreen = offscreenSelectionRows(this.#selection, this.#selectionAnchors, this.#scrollTop, this.#firstSelectableRow, this.#viewportHeight, this.#documentRows.length);
210
+ // Rationale: edge auto-scroll grows a drag beyond one screen. Paint stays clipped to the
211
+ // frame, while copy reads the selected document rows past either viewport edge and beneath
212
+ // the floating bottom control.
213
+ const selectionRows = [];
214
+ const rowAnchors = [];
215
+ const controlLine = (this.#frame?.hits.bottom?.row ?? 0) - 1;
216
+ const pushDocumentRow = (row) => {
217
+ const source = this.#documentRows[row] ?? "";
218
+ selectionRows.push(truncateToWidth(source, this.#contentWidth));
219
+ rowAnchors.push(documentSelectionAnchor(row, source));
220
+ };
221
+ const pushBelow = () => {
222
+ for (let row = offscreen.below?.from ?? 0; row <= (offscreen.below?.to ?? -1); row += 1)
223
+ pushDocumentRow(row);
224
+ };
225
+ for (let row = offscreen.above?.from ?? 0; row <= (offscreen.above?.to ?? -1); row += 1)
226
+ pushDocumentRow(row);
227
+ if (offscreen.belowFirst)
228
+ pushBelow();
229
+ for (let line = visible.start.line; line <= visible.end.line; line += 1) {
187
230
  const anchor = this.#selectionRowAnchors[line];
231
+ if (line === controlLine && anchor?.kind === "document")
232
+ pushDocumentRow(anchor.row);
233
+ else {
234
+ selectionRows.push(this.#selectionRows[line] ?? "");
235
+ rowAnchors.push(anchor);
236
+ }
237
+ }
238
+ if (!offscreen.belowFirst)
239
+ pushBelow();
240
+ const offscreenStart = offscreen.above !== undefined || offscreen.belowFirst === true;
241
+ const offscreenEnd = offscreen.below !== undefined && !offscreen.belowFirst;
242
+ const selection = {
243
+ ...visible,
244
+ start: { line: 0, column: offscreenStart ? offscreen.startColumn ?? 0 : visible.start.column },
245
+ end: {
246
+ line: selectionRows.length - 1,
247
+ column: offscreenEnd ? offscreen.endColumn ?? Number.MAX_SAFE_INTEGER : visible.end.column,
248
+ },
249
+ };
250
+ const copiedLines = Array.from({ length: Math.max(0, selection.end.line - selection.start.line + 1) }, (_value, index) => selection.start.line + index).filter(line => {
251
+ const anchor = rowAnchors[line];
188
252
  return anchor?.kind === "document" && anchor.row < this.#selectableDocumentRowCount
189
253
  || anchor?.kind === "dock" && this.#copyableDockRows.has(anchor.fromBottom);
190
254
  });
191
255
  if (copiedLines.length === 0)
192
256
  return null;
193
- const transcriptLines = copiedLines.filter(line => this.#selectionRowAnchors[line]?.kind === "document");
257
+ const transcriptLines = copiedLines.filter(line => rowAnchors[line]?.kind === "document");
194
258
  const prompt = transcriptLines.length === 0 ? undefined : this.#promptAnchors.find(candidate => transcriptLines.every(line => {
195
- const anchor = this.#selectionRowAnchors[line];
259
+ const anchor = rowAnchors[line];
196
260
  return anchor?.kind === "document" && anchor.row >= candidate.firstRow && anchor.row <= candidate.lastRow;
197
261
  }));
198
262
  const included = new Set(copiedLines);
199
- const text = textSelectionText(selection, this.#selectionRows, line => {
200
- const row = this.#selectionRows[line] ?? "";
201
- const anchor = this.#selectionRowAnchors[line];
263
+ const text = textSelectionText(selection, selectionRows, line => {
264
+ const row = selectionRows[line] ?? "";
265
+ const anchor = rowAnchors[line];
202
266
  const dock = anchor?.kind === "dock" ? this.#copyableDockRows.get(anchor.fromBottom) : undefined;
203
267
  if (prompt !== undefined && anchor?.kind === "document") {
204
268
  const first = anchor.row === prompt.firstRow;
@@ -207,11 +271,12 @@ export class TranscriptViewport {
207
271
  : row;
208
272
  return selectionCopyLineContent(copyRow, first ? "first" : "continuation");
209
273
  }
210
- return dock?.prompt !== undefined ? selectionCopyLineContent(row, dock.prompt) : usefulTextLineContent(row);
274
+ // Invariant: the dock editor has no prefix; transcript prompts still carry chrome.
275
+ return dock?.prompt !== undefined ? usefulTextLineContent(row, 1) : usefulTextLineContent(row);
211
276
  }, line => included.has(line));
212
277
  return text.length === 0 ? null : text;
213
278
  }
214
- /** Captures visible selected text, not off-screen history. */
279
+ /** Captures selected text, including document rows of a visible selection scrolled off-screen. */
215
280
  captureSelectedText() {
216
281
  if (!this.#copyableSelection)
217
282
  return null;
@@ -362,7 +427,7 @@ export class TranscriptViewport {
362
427
  contentLength: documentRows.length,
363
428
  viewportHeight,
364
429
  scroll: this.#scrollTop,
365
- // Compatibility: the session rail deliberately starts one line below the viewport top.
430
+ // Invariant: the rail keeps its top gap; the shell owns the spacing above the dock.
366
431
  trackHeight: Math.max(0, viewportHeight - 1),
367
432
  });
368
433
  const presentation = scrollbarPresentation({
@@ -466,7 +531,7 @@ export class TranscriptViewport {
466
531
  const bottomHovered = pointer !== undefined && pointer.row === bottomHit.row
467
532
  && pointer.column >= bottomHit.columnStart && pointer.column <= bottomHit.columnEnd;
468
533
  control = { row, left, text: `${CONTROL_STYLE_RESET}${theme.bottomControl(label, bottomHovered)}` };
469
- // Invariant: the control floats above selection; its cells are never selected or copied.
534
+ // Invariant: the control floats above selection; copy restores the text beneath it.
470
535
  selectionRows[row] = overlaySpan(padRowPreservingBackground(selectionRows[row] ?? "", width), left, left + labelWidth, " ".repeat(labelWidth));
471
536
  }
472
537
  }
@@ -504,7 +569,7 @@ export class TranscriptViewport {
504
569
  // Invariant: paint the rail after selection over the gutter's continued row
505
570
  // background. Source content and selection still end at the preceding cell.
506
571
  let railCell = "";
507
- if (row < viewportHeight && presentation.visible && geometry !== null && row > 0) {
572
+ if (row > 0 && row < viewportHeight && presentation.visible && geometry !== null) {
508
573
  const trackRow = row - 1;
509
574
  const thumb = isThumbRow(geometry, trackRow);
510
575
  const glyph = thumb ? presentation.thumbGlyph : presentation.trackGlyph;
@@ -526,7 +591,7 @@ export class TranscriptViewport {
526
591
  return overlayWidth === 0 ? withControl : overlaySpan(padRowPreservingBackground(withControl, width), overlayLimit - overlayWidth, overlayLimit, `${GUTTER_DECORATION_RESET}${overlayText}`, { inheritStartStyle: true });
527
592
  });
528
593
  rowRecomputed ||= !final.reused;
529
- frameRows[row] = final.value;
594
+ frameRows[row] = final.value.replace(VIEWPORT_PROMPT_ZONES, "");
530
595
  (rowRecomputed ? recomputedRows : reusedRows).push(row + 1);
531
596
  const state = `${width}\u0000${row}\u0000${painted}\u0000${rangeKey}\u0000${range === null ? "" : selectionPainterId}\u0000${railCell}\u0000${overlayText}\u0000${rowControl?.text ?? ""}`;
532
597
  visibleStates.push(state);
@@ -627,19 +692,10 @@ export class TranscriptViewport {
627
692
  || !sameRectangle(previous.descriptor.transcript, expectedTranscript))
628
693
  return null;
629
694
  const descriptor = {
695
+ ...previous.descriptor,
630
696
  frameId: ++this.#frameId,
631
- width: boundedWidth,
632
- height: boundedHeight,
633
- transcript: expectedTranscript,
634
- dock: expectedDock,
635
697
  previousDocumentRange: previous.descriptor.nextDocumentRange,
636
- nextDocumentRange: previous.descriptor.nextDocumentRange,
637
698
  previousFollowingEnd: previous.followingEnd,
638
- followingEnd: previous.followingEnd,
639
- transientRowCount: previous.descriptor.transientRowCount,
640
- transientAlignmentGapRows: previous.descriptor.transientAlignmentGapRows,
641
- bottomAlignedTailRowCount: previous.descriptor.bottomAlignedTailRowCount,
642
- liveTailRows: previous.descriptor.liveTailRows,
643
699
  verticalShiftRows: 0,
644
700
  safeVerticalShift: false,
645
701
  selectionRevision: this.#selectionRevision,
@@ -650,12 +706,8 @@ export class TranscriptViewport {
650
706
  this.#selectionRows = [...this.#selectionRows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight);
651
707
  this.#copyableDockRows = copyableDockRowsBySuffix(dockRows, copyableDockRows);
652
708
  const frame = {
653
- rows: [...previous.rows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight),
654
- contentWidth: previous.contentWidth,
655
- scrollTop: previous.scrollTop,
656
- maxScroll: previous.maxScroll,
657
- followingEnd: previous.followingEnd,
658
- hits: previous.hits,
709
+ ...previous,
710
+ rows: [...previous.rows.slice(0, viewportHeight), ...dock.map(row => row.replace(VIEWPORT_PROMPT_ZONES, ""))].slice(0, boundedHeight),
659
711
  descriptor,
660
712
  selectionDamage: {
661
713
  revision: this.#selectionRevision,
@@ -710,7 +762,7 @@ export class TranscriptViewport {
710
762
  const anchors = this.#selectionAnchors;
711
763
  if (selection === undefined || anchors === undefined)
712
764
  return;
713
- const project = (anchor) => {
765
+ const project = (anchor, moving) => {
714
766
  if (anchor.kind === "dock") {
715
767
  const dockIndex = dockRows.length - anchor.fromBottom - 1;
716
768
  return dockIndex < 0 || dockIndex >= dockRows.length
@@ -752,7 +804,7 @@ export class TranscriptViewport {
752
804
  // Concurrency: a streaming chunk may replace or reflow the pointer's current live-tail
753
805
  // row before the next motion report. During the held gesture only, retain its distance
754
806
  // from the persistent tail; release restores identity-only projection.
755
- if (selection.selecting && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
807
+ if (selection.selecting && moving && anchor.liveFromBottom !== undefined && this.#liveTailStartRow !== undefined) {
756
808
  const documentRow = this.#selectableDocumentRowCount - anchor.liveFromBottom - 1;
757
809
  if (documentRow >= this.#liveTailStartRow && documentRow < this.#selectableDocumentRowCount) {
758
810
  const visible = this.#selectionRowAnchors.findIndex(candidate => candidate?.kind === "document" && candidate.row === documentRow);
@@ -764,10 +816,18 @@ export class TranscriptViewport {
764
816
  }
765
817
  }
766
818
  }
819
+ // Concurrency: streaming may restyle a held endpoint's row; it keeps its document index.
820
+ // Following the tail would drag the press row toward the newest output.
821
+ if (selection.selecting && anchor.row >= 0 && anchor.row < documentRows.length) {
822
+ return {
823
+ line: anchor.row - this.#scrollTop,
824
+ anchor: documentSelectionAnchor(anchor.row, documentRows[anchor.row] ?? "", anchor.liveFromBottom),
825
+ };
826
+ }
767
827
  return undefined;
768
828
  };
769
- const projectedAnchor = project(anchors.anchor);
770
- const projectedHead = project(anchors.head);
829
+ const projectedAnchor = project(anchors.anchor, false);
830
+ const projectedHead = project(anchors.head, true);
771
831
  if (projectedAnchor === undefined || projectedHead === undefined) {
772
832
  this.clearSelection();
773
833
  return;
@@ -787,11 +847,59 @@ export class TranscriptViewport {
787
847
  // Invariant: mixed viewport/dock ranges project across the same complete frame regardless
788
848
  // of origin. Document-only ranges remain clipped to the scrollable viewport when off-screen.
789
849
  const rows = crossesDock ? this.#selectionRows.length : this.#viewportHeight;
790
- return visibleTextSelection(orderedTextSelection(this.#selection), rows, this.#firstSelectableRow);
850
+ const ordered = orderedTextSelection(dockOrderedSelection(this.#selection, anchors, this.#viewportHeight));
851
+ return visibleTextSelection(ordered, rows, this.#firstSelectableRow);
791
852
  }
792
853
  #updateCopyableSelection() {
793
- this.#copyableSelection = this.selectedText() !== null;
854
+ // Performance: off-screen rows count as copyable by range alone; capture trims whitespace later.
855
+ const offscreen = offscreenSelectionRows(this.#selection, this.#selectionAnchors, this.#scrollTop, this.#firstSelectableRow, this.#viewportHeight, this.#documentRows.length);
856
+ const selectable = [offscreen.above, offscreen.below]
857
+ .some(range => range !== undefined && range.from < this.#selectableDocumentRowCount);
858
+ this.#copyableSelection = selectable && this.#visibleSelection() !== undefined || this.selectedText() !== null;
859
+ }
860
+ }
861
+ /**
862
+ * Invariant: dock rows follow the whole transcript, so a document endpoint scrolled below the
863
+ * viewport orders before every dock row instead of at its numeric frame line among them.
864
+ */
865
+ function dockOrderedSelection(selection, anchors, viewportHeight) {
866
+ if (selection === undefined || anchors === undefined || anchors.anchor.kind !== "dock" && anchors.head.kind !== "dock") {
867
+ return selection;
794
868
  }
869
+ const place = (point, kind) => kind === "document" && point.line >= viewportHeight ? { line: viewportHeight, column: 1, from: 0, to: 0 } : point;
870
+ return { ...selection, anchor: place(selection.anchor, anchors.anchor.kind), head: place(selection.head, anchors.head.kind) };
871
+ }
872
+ /** Document rows of a selection whose endpoints lie beyond the visible viewport edges. */
873
+ function offscreenSelectionRows(selection, anchors, scrollTop, firstSelectableRow, viewportHeight, documentRowCount) {
874
+ const ordered = orderedTextSelection(dockOrderedSelection(selection, anchors, viewportHeight));
875
+ if (selection === undefined || ordered === undefined || anchors === undefined)
876
+ return {};
877
+ const placed = dockOrderedSelection(selection, anchors, viewportHeight);
878
+ const anchorFirst = placed.anchor.line < placed.head.line
879
+ || placed.anchor.line === placed.head.line && placed.anchor.column <= placed.head.column;
880
+ const top = anchorFirst ? anchors.anchor : anchors.head;
881
+ const bottom = anchorFirst ? anchors.head : anchors.anchor;
882
+ const topPoint = anchorFirst ? selection.anchor : selection.head;
883
+ const bottomPoint = anchorFirst ? selection.head : selection.anchor;
884
+ const firstVisibleRow = scrollTop + firstSelectableRow;
885
+ const pastVisibleRow = scrollTop + viewportHeight;
886
+ const columns = {
887
+ startColumn: selection.fullRow ? 0 : topPoint.from ?? Math.max(0, topPoint.column - 1),
888
+ endColumn: selection.fullRow ? Number.MAX_SAFE_INTEGER : bottomPoint.to ?? bottomPoint.column,
889
+ };
890
+ if (top.kind === "document" && bottom.kind === "dock" && top.row >= pastVisibleRow) {
891
+ return { ...columns, below: { from: top.row, to: documentRowCount - 1 }, belowFirst: true };
892
+ }
893
+ return {
894
+ ...columns,
895
+ ...(top.kind === "document" && top.row < firstVisibleRow && ordered.start.line < firstSelectableRow
896
+ ? { above: { from: top.row, to: firstVisibleRow - 1 } }
897
+ : {}),
898
+ ...(top.kind === "document" && bottom.kind === "document" && bottom.row >= pastVisibleRow
899
+ && ordered.end.line >= viewportHeight
900
+ ? { below: { from: pastVisibleRow, to: bottom.row } }
901
+ : {}),
902
+ };
795
903
  }
796
904
  function visibleTextSelection(selection, rowCount, firstRow) {
797
905
  if (selection === undefined || rowCount <= firstRow || selection.end.line < firstRow || selection.start.line >= rowCount)
@@ -1,11 +1,9 @@
1
- export declare const OWNED_UI_SETTINGS_VERSION = 17;
1
+ export declare const OWNED_UI_SETTINGS_VERSION = 18;
2
2
  export type OwnedUiSettingValue = string | number | boolean;
3
3
  export type OwnedUiSettingApplication = "live" | "restart";
4
4
  export interface OwnedUiSettingDeclaration {
5
5
  readonly id: string;
6
- /** Optional presentation label; the id remains the persistence key. */
7
6
  readonly label?: string;
8
- /** Optional settings-screen group for related owned controls. */
9
7
  readonly section?: {
10
8
  readonly id: string;
11
9
  readonly title: string;
@@ -15,10 +13,6 @@ export interface OwnedUiSettingDeclaration {
15
13
  readonly defaultValue: OwnedUiSettingValue;
16
14
  readonly allowedValues: readonly OwnedUiSettingValue[];
17
15
  }
18
- /**
19
- * Every A1 setting, keyed by its id. The table is the one declaration: ids, defaults, and allowed
20
- * values are read from it, and `OwnedSettingValueOf` derives each getter's type from it.
21
- */
22
16
  export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
23
17
  readonly updateCheck: Readonly<{
24
18
  id: "updateCheck";
@@ -176,6 +170,18 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
176
170
  defaultValue: 8;
177
171
  allowedValues: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
178
172
  }>;
173
+ readonly tabsPosition: Readonly<{
174
+ id: "tabsPosition";
175
+ label: "Position";
176
+ section: Readonly<{
177
+ id: "tabs";
178
+ title: "Tabs";
179
+ }>;
180
+ description: "Place the resident tab bar at the top or bottom of the terminal.";
181
+ application: "live";
182
+ defaultValue: "top";
183
+ allowedValues: readonly ["top", "bottom"];
184
+ }>;
179
185
  readonly tabsAutoName: Readonly<{
180
186
  id: "tabsAutoName";
181
187
  label: "Auto name";
@@ -190,9 +196,7 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
190
196
  }>;
191
197
  }>;
192
198
  export type OwnedSettingId = keyof typeof OWNED_SETTING_DECLARATIONS;
193
- /** The value type a setting's declaration allows: a literal union for a choice, `number` for a stepped range. */
194
199
  export type OwnedSettingValueOf<Id extends OwnedSettingId> = (typeof OWNED_SETTING_DECLARATIONS)[Id]["allowedValues"][number];
195
- /** The declarations in declaration order, for resolution, persistence, and the settings screen. */
196
200
  export declare const OWNED_UI_SETTING_DECLARATIONS: readonly OwnedUiSettingDeclaration[];
197
201
  export declare function isOwnedSettingId(id: string): id is OwnedSettingId;
198
202
  export declare function assertOwnedUiSettingDeclarations(declarations: readonly OwnedUiSettingDeclaration[]): void;
@@ -1,19 +1,14 @@
1
- export const OWNED_UI_SETTINGS_VERSION = 17;
1
+ export const OWNED_UI_SETTINGS_VERSION = 18;
2
2
  const UI_ACCENT_COLOR_CHOICES = Object.freeze([
3
3
  "purple", "blue", "cyan", "green", "orange", "pink",
4
4
  ]);
5
5
  const MAX_ID_LENGTH = 64;
6
6
  const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
7
- /** Declared first so the settings screen opens on it; sections follow first-declaration order. */
8
7
  const GENERIC_SECTION = Object.freeze({ id: "generic", title: "Generic" });
9
8
  const APPEARANCE_SECTION = Object.freeze({ id: "appearance", title: "Appearance" });
10
9
  const SCROLL_SECTION = Object.freeze({ id: "scroll", title: "Scroll" });
11
10
  const AGENT_SECTION = Object.freeze({ id: "agent", title: "Agent" });
12
11
  const TABS_SECTION = Object.freeze({ id: "tabs", title: "Tabs" });
13
- /**
14
- * Every A1 setting, keyed by its id. The table is the one declaration: ids, defaults, and allowed
15
- * values are read from it, and `OwnedSettingValueOf` derives each getter's type from it.
16
- */
17
12
  export const OWNED_SETTING_DECLARATIONS = Object.freeze({
18
13
  updateCheck: Object.freeze({
19
14
  id: "updateCheck",
@@ -136,6 +131,15 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
136
131
  defaultValue: 8,
137
132
  allowedValues: Object.freeze([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
138
133
  }),
134
+ tabsPosition: Object.freeze({
135
+ id: "tabsPosition",
136
+ label: "Position",
137
+ section: TABS_SECTION,
138
+ description: "Place the resident tab bar at the top or bottom of the terminal.",
139
+ application: "live",
140
+ defaultValue: "top",
141
+ allowedValues: Object.freeze(["top", "bottom"]),
142
+ }),
139
143
  tabsAutoName: Object.freeze({
140
144
  id: "tabsAutoName",
141
145
  label: "Auto name",
@@ -146,7 +150,6 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
146
150
  allowedValues: Object.freeze([true, false]),
147
151
  }),
148
152
  });
149
- /** The declarations in declaration order, for resolution, persistence, and the settings screen. */
150
153
  export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze(Object.values(OWNED_SETTING_DECLARATIONS));
151
154
  export function isOwnedSettingId(id) {
152
155
  return Object.hasOwn(OWNED_SETTING_DECLARATIONS, id);
@@ -137,6 +137,13 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
137
137
  return { ...values };
138
138
  },
139
139
  }),
140
+ Object.freeze({
141
+ to: 18,
142
+ description: "Introduce the top resident tab-bar position default.",
143
+ migrate(values) {
144
+ return { ...values };
145
+ },
146
+ }),
140
147
  ]);
141
148
  /** Shape-selected compatibility for the metadata-free product object. */
142
149
  export function normalizeOwnedUiSettingsValues(source) {
@@ -229,7 +229,7 @@ The channels carry bounded generation-1 frames: a u32 little-endian length follo
229
229
 
230
230
  ### 6. Rendering, input, and outer-terminal fidelity
231
231
 
232
- - **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
232
+ - **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. On Windows, ConPTY re-renders the child's cells on its own schedule and relays the DEC 2026 markers when it parses them, so a render pass can land between a frame's markers and the rest of its cells can arrive after the end marker; the holder therefore treats a relayed end marker as "frame parsed", keeps the state private until the following render pass has arrived and settled for 2 ms, and publishes anyway after a 40 ms bound. A begin marker that arrives while that pass is still pending does not publish the torn state. Unix PTYs relay bytes verbatim, so their markers still delimit frames exactly. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
233
233
  - **Outer modes.** The client negotiates the outer terminal's modes: raw mode, alternate screen, bracketed paste, focus events, SGR mouse, kitty keyboard protocol where available, and synchronized output.
234
234
  - Child-requested modes are tracked in the holder's model. Input is re-encoded per child mode by libghostty-vt, which the proof already does.
235
235
  - Mouse rows are offset by the strip. Row 0 belongs to the client.
@@ -303,7 +303,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
303
303
  | Tab child exits or crashes | holder sees the child exit | If the exit was not requested: `crashed`. The holder respawns `ui.js --tab --session <file>` with backoff 1 s, 5 s, 30 s. After 3 restarts in 10 min: `failed`, with a banner `✗ <reason> — [r] retry · [f] start fresh · [alt+w] close`. An interrupted prompt (reported via the bridge, or detected as a trailing user entry) is offered back into the editor and never resent. Child stderr, if separately available, is a private content-bearing recovery artifact, never an ordinary diagnostic log. |
304
304
  | Holder crash | pipe EOF plus verified process identity/exit | Pseudoterminal loss is not proof that every descendant exited. Verify the old writer/tree is gone and acquire its writer lease before resuming in a new holder; otherwise keep the tab blocked without duplicate writers. |
305
305
  | Holder hang | holder control heartbeat every 5 s, 3 missed over a healthy supervision path, then one heartbeat of grace for the holder-local watchdog record | Terminate only the verified holder/tree within bounded deadlines, then follow the lease-gated recovery path. |
306
- | A1 child hang | missing child event-loop heartbeat over a healthy bridge path | Warn at 30 s; restart at `tabsUnresponsiveRestartSeconds` (default 120), preserving journal/screen. A server/bridge transport outage alone is not proof of a child hang. |
306
+ | A1 child hang | missing child event-loop heartbeat over a healthy bridge path | Record `unresponsive` at 30 s without changing the chip or painting a notice; restart at `tabsUnresponsiveRestartSeconds` (default 120), preserving journal/screen. A server/bridge transport outage alone is not proof of a child hang. |
307
307
  | Server crash | clients and holders see the pipe EOF | Holders keep running. A client or holder starts a replacement, which acquires the released writer lease, increments the epoch, derives expected credentials, and re-admits identity-verified holders. A live but unresponsive recorded owner is terminated only after native identity verification; an unverifiable owner blocks replacement. After 3 starts in 60 s, clients show `✗ tab host stopped — [r] restart · [q] quit`. |
308
308
  | Attach client crash or terminal close | the server sees the pipe EOF | Nothing happens to tabs. Pending requests stay `needs-input`. |
309
309
  | Reboot or logout | the platform boot identity differs, or no verified holders exist for a new OS session | Tabs are not restored in this version. The server moves the previous tab set to registry history, and bare `a1` starts with one new tab. Every previous session stays resumable through the normal session picker or `a1 --session`. A pending journaled prompt is offered when its session is next opened in a tab, for up to seven days. Restoring tabs after reboot is a follow-up change. |
@@ -377,7 +377,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
377
377
  Internal resources remain bounded without imposing a user-visible tab-count quota.
378
378
 
379
379
  - The only user-facing Tabs setting during resident launches is `tabsAutoName` (default on). Legacy `tabsMax` data is preserved but ignored. Tab enablement is not a setting. Direct `a1 solo` and `a1 pi` launches omit the Tabs section entirely.
380
- - Unresponsive restart and stall-notice timing are internal fixed behavior at 120 and 300 seconds; concurrent starts use internal default 2/hard cap 8 and prewarm uses internal count/hard cap 1.
380
+ - Unresponsive restart timing is internal fixed behavior at 120 seconds; there is no progress-stall notice; concurrent starts use internal default 2/hard cap 8 and prewarm uses internal count/hard cap 1.
381
381
  - Every open tab keeps its holder and A1 process until explicit close. There is no idle-suspension setting, timer, lifecycle, or glyph.
382
382
  - Scrollback is capped at 10 MiB per holder, and the replay after a server restart is the retained model itself.
383
383
  - The server exits after 10 minutes with no attached client and no live user tab; the prewarmed standby does not keep it alive and is stopped first.
@@ -458,7 +458,7 @@ The v2 daemon was unreliable for structural reasons, and a forensic pass over it
458
458
  | Synchronous `taskkill` loops blocked the daemon for 30–60 s | Blocking kills on the event loop | Kills run asynchronously and concurrently, with deadlines. The loop keeps answering heartbeats. |
459
459
  | Blocked ConPTY writes stalled the loop, and keystrokes or pastes were silently dropped ("newest wins") | No flow control | A dedicated writer per holder with a bounded queue. Reserve capacity for a complete paste before delivery; reject an over-capacity paste visibly without delivering a prefix. After admission, preserve byte order and report delivery uncertainty if a child or process fails; never silently truncate or retry accepted bytes. |
460
460
  | Terminal queries (DA/DSR/CPR) were answered by the busy shared emulator, so children stalled | One emulator loop for all agents | Each holder answers its own tab's queries from its own model. |
461
- | Repaints arrived in fragments ("ghost frames"), patched with timing heuristics and two screen sources | Emulation reconciled with bridge surfaces by timers | One source of truth, the holder model. Frames are published at synchronized-output boundaries, with a bounded coalescing window. |
461
+ | Repaints arrived in fragments ("ghost frames"), patched with timing heuristics and two screen sources | Emulation reconciled with bridge surfaces by timers | One source of truth, the holder model. Frames are published at synchronized-output boundaries, with a bounded coalescing window; where ConPTY relays those boundaries ahead of the cells it re-renders, publication waits for the render pass instead of the marker. |
462
462
  | A late exit event poisoned the replacement child | Events were not tied to an incarnation | Incarnation IDs on every event. Stale events are discarded. |
463
463
  | Resizes were lost in transit | Edge-triggered resize | Size is desired state, reconciled until observed. |
464
464
  | Restore spawned duplicate agents | No restore gate | A single start gate for crash restarts, at most one start in flight per tab, and conditional revisions. The duplicate-start invariant is checked in simulation. |
@@ -479,8 +479,8 @@ The v2 daemon was unreliable for structural reasons, and a forensic pass over it
479
479
  **Agents that stop making progress.** Liveness and progress are separate signals:
480
480
 
481
481
  - Holder heartbeat: a Rust main-loop tick every 5 s seen by the server; three missed ticks over an otherwise healthy supervision path plus one heartbeat of grace trigger verified holder recovery. The grace keeps external termination behind the holder's 15-second process-local watchdog so its own crash evidence wins despite scheduler jitter.
482
- - A1 heartbeat: the child's Node event loop, seen through the bridge. After 30 s it shows `unresponsive`; after `tabsUnresponsiveRestartSeconds` (default 120) the tab restarts from its session with its journal and last screen preserved. A missing server or bridge transport degrades status, not proof of a child event-loop stall; reacquire liveness evidence before destructive recovery.
483
- - Agent progress: model or tool events. After `tabsStallNoticeSeconds` (default 300) it shows `stalled` with interrupt and restart actions. It is never auto-killed, because a legitimate tool can run for a long time.
482
+ - A1 heartbeat: the child's Node event loop, seen through the bridge. After 30 s the server records `unresponsive` in the registry and counters, without changing the presented status, chip glyph, or colour and without a notice row or restart key; after `tabsUnresponsiveRestartSeconds` (default 120) the tab restarts from its session with its journal and last screen preserved. A missing server or bridge transport degrades status, not proof of a child event-loop stall; reacquire liveness evidence before destructive recovery.
483
+ - Agent progress: not supervised. A working tab without model or tool events keeps its working status indefinitely, gains no transcript notice, and is never auto-killed, because a legitimate tool can run for a long time.
484
484
 
485
485
  **Data-loss envelope.** Distinguish process termination, power loss, and loss of both holder and child:
486
486
 
@@ -19,7 +19,7 @@ A generated probe on Windows Node 24.16.0 copied the same eight-character range
19
19
  - Copy: 5-second admission deadline, one executing request and one newest pending request, at most 16 MiB UTF-8 text. Source snapshots retain only selected rows, capped at 65,536 rows and 32 Mi UTF-16 code units; they do not keep an obsolete whole-transcript graph.
20
20
  - Paste: eight distinct admitted requests, a 5-second acquisition budget including the captured write wait, and a 15-second deadline through insertion. A canceled executor counts against capacity until its stop fence. Image conversion has one process-wide slot. Copy supersession is deliberately not used for paste edits.
21
21
  - Text transfer: 16 MiB UTF-8 cap; IPC fragments are at most 16,384 code units and preserve surrogate pairs. The receiver acknowledges each fragment. Prepared large text is stored behind the existing compact chip rather than inserted as millions of editor characters. Existing source-image, pixel, encoded-output and attachment-count limits remain unchanged.
22
- - Clipboard executors have a 250 ms stop grace. A helper that has already reported a complete prepared result and lingers past that grace is reaped, not canceled: it is still terminated, but its prepared text is delivered exactly rather than replaced with an empty payload. Only an explicit cancellation, a protocol violation, or an incomplete byte count discards prepared text. POSIX command fallbacks share the paste helper's isolated process group; strict command reads use a forceful kill signal. Unconfirmed executor termination retains its capacity instead of allowing unbounded replacements or late A1-controlled writes. No helper inherits the UI terminal or places clipboard payloads in argv, temporary files, or logs.
22
+ - Clipboard executors have a 250 ms stop grace. A helper that has already reported a complete prepared result and lingers past that grace is reaped, not canceled: it is still terminated, but its prepared text is delivered exactly rather than replaced with an empty payload. Only an explicit cancellation, a protocol violation, or an incomplete byte count discards prepared text. POSIX command fallbacks share the paste helper's isolated process group; strict command reads use a forceful kill signal. Native POSIX copy writers (`wl-copy`, `xclip`, `pbcopy`) start detached in their own process group, as in pinned Pi 1.4, so a writer that daemonizes to keep serving the clipboard is not stopped when Ctrl+Z suspends A1. Unconfirmed executor termination retains its capacity instead of allowing unbounded replacements or late A1-controlled writes. No helper inherits the UI terminal or places clipboard payloads in argv, temporary files, or logs.
23
23
  - Local copy prefers native delivery outside the terminal. OSC 52 uses the existing writer only on a non-blocking supported output route and is capped at a complete 64 KiB control, or a lower advertised limit. Submission is not clipboard acknowledgment. Remote/multiplexed sessions never silently read or write a different host's clipboard. Native Ctrl+V acquisition is unavailable when only the remote terminal owns the desired clipboard; normal terminal-provided paste remains supported.
24
24
  - URL-chip decoration: at most 65,536 UTF-8 bytes of added OSC 8 metadata per editor render pass across rows and repeated occurrences, including opening/closing pairs and cleanup closes. Targets that do not fit are omitted whole before control construction; a UTF-16 length precheck avoids scanning huge backing values. The budget resets each pass. Full chip data, atomic editing and exact copy/history/submission expansion remain intact; ordinary in-budget links retain their targets. Terminal automatic URL recognition is separate from explicit metadata.
25
25
  - Path-list presentation: the isolated classifier budgets 4,096 UTF-16 units per paste for existing individual tags plus 20 suffix-reserve units per occurrence. Over-budget lists use one existing text-paste chip backed by all classified full paths concatenated in order, including repetitions, without extra separators or re-normalization. No individual members are adopted into the UI before compaction. Small lists retain their file/folder/image-file chips; failed/timed-out classification retains the existing original-text fallback. Existing draft/history chips and the synchronous compatibility classifier are unchanged.
@@ -34,9 +34,9 @@ It contains no executable built from A1 sources, separate session-lock addon, br
34
34
 
35
35
  ## Native addon layer
36
36
 
37
- `npm run doctor` reports this contract against the current machine—Node range, npm major, git, the GitHub CLI, Rust/Cargo, and whether `node_modules` matches the lockfile—and names the install command for anything missing. `npm run build` runs the same check as its first step, so an unmet prerequisite fails by name rather than inside a module resolver or Cargo. Unmet tool prerequisites block; a Node or npm version outside the declared contract is reported as an advisory without failing the build.
37
+ `npm run doctor` reports this contract against the current machine—Node range, npm major, git, the GitHub CLI, Rust/Cargo, Windows Microsoft C++ readiness where applicable, and whether `node_modules` matches the lockfile—and names the explicit setup action for anything missing. `npm run build` resolves the same native environment and runs the check before cleaning or compiling, so an unmet prerequisite fails by name rather than inside a module resolver, Cargo, or the linker. Unmet tool prerequisites block; a Node or npm version outside the declared contract is reported as an advisory without failing the build.
38
38
 
39
- Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
39
+ Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. On Windows, the resolver honors explicit `CARGO` and `RUSTC` executables, then the launching `PATH`, then `%USERPROFILE%\.cargo\bin`; the fallback changes only build-child state. Windows native linking also requires Visual Studio or Visual Studio Build Tools with the Desktop development with C++ workload, MSVC x64/x86 tools, and a Windows SDK. Visual Studio Code is not a compiler prerequisite. An already valid developer environment is retained; otherwise the resolver uses the installed stable Visual Studio locator and `vcvars64.bat`, verifies that `cl.exe` and Microsoft's `link.exe` come from the selected VC tools, and passes the captured environment only to child processes. This prevents Git Bash's GNU `link.exe` from shadowing MSVC without changing persistent `PATH`. A1 never installs these tools, elevates privileges, or accepts licenses automatically. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
40
40
 
41
41
  `npm run build` compiles `native/process-guardian` as the host platform's Node-API 9 addon, places `process-guardian.node` under `dist/native/<platform>-<architecture>/`, loads it to observe its version identity, and writes the v2 integrity manifest. It also builds the current platform's release-profile `terminal-host.node`, whose v2 manifest binds the addon, `dist/foundation/resident-tabs/host-entry.js`, and notices, so the ordinary `npm run build && ./scripts/dev` handoff exercises resident tabs without deployment. On Windows the normal build reuses a compatible ignored terminal-library cache or downloads the newest compatible library from the current branch or a successful CI run belonging to a pull request already merged into `develop`, then links it under the crate's manifest verification; it never silently invokes the quarantined Zig package fetch. An unavailable or mismatched prerequisite fails the build instead of leaving a nominally successful single-agent-only tree. The npm `prepare` lifecycle and validation preflight use the explicit core build because selected native lanes assemble and certify terminal hosts separately.
42
42