@timurproko/a1 0.2.6-dev.736 → 0.2.7-dev.746

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 (77) hide show
  1. package/bin/guardian.js +5 -0
  2. package/bin/tab-launch-context.d.ts +18 -0
  3. package/bin/tab-launch-context.js +36 -0
  4. package/bin/ui.js +14 -3
  5. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  6. package/dist/app/session-shell/helper-pool.js +3 -5
  7. package/dist/app/session-shell/session-shell-root.d.ts +6 -1
  8. package/dist/app/session-shell/session-shell-root.js +3 -2
  9. package/dist/app/session-shell/session-shell.js +41 -24
  10. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -1
  11. package/dist/app/session-shell/session-viewport-controller.js +3 -3
  12. package/dist/composition/owned-ui.d.ts +17 -0
  13. package/dist/composition/owned-ui.js +28 -1
  14. package/dist/features/owned-ui/resources/release-notes.json +4 -0
  15. package/dist/features/owned-ui/settings-app.js +3 -3
  16. package/dist/foundation/launch-context/index.d.ts +6 -0
  17. package/dist/foundation/launch-context/index.js +15 -0
  18. package/dist/foundation/launch-guardian/main.d.ts +5 -0
  19. package/dist/foundation/launch-guardian/main.js +2 -1
  20. package/dist/foundation/release/bootstrap.d.ts +4 -0
  21. package/dist/foundation/release/bootstrap.js +33 -6
  22. package/dist/foundation/release/cohort-state.js +5 -2
  23. package/dist/foundation/resident-tabs/bridge-client.d.ts +36 -0
  24. package/dist/foundation/resident-tabs/bridge-client.js +157 -0
  25. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +28 -0
  26. package/dist/foundation/resident-tabs/ensure-terminal-host.js +115 -0
  27. package/dist/foundation/resident-tabs/index.d.ts +5 -0
  28. package/dist/foundation/resident-tabs/index.js +3 -0
  29. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  30. package/dist/foundation/resident-tabs/protocol.js +195 -0
  31. package/dist/foundation/resident-tabs/routing.d.ts +10 -0
  32. package/dist/foundation/resident-tabs/routing.js +8 -0
  33. package/dist/foundation/resident-tabs/settings.d.ts +2 -0
  34. package/dist/foundation/resident-tabs/settings.js +17 -0
  35. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  36. package/dist/foundation/terminal-cleanup/index.js +1 -1
  37. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
  38. package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
  39. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  40. package/dist/integrations/pi/components/owned-editor-ux.js +2 -2
  41. package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
  42. package/dist/native/darwin-arm64/manifest.json +1 -1
  43. package/dist/native/linux-x64/manifest.json +1 -1
  44. package/dist/native/win32-x64/manifest.json +2 -2
  45. package/dist/native/win32-x64/process-guardian.exe +0 -0
  46. package/dist/product-identity.d.ts +1 -1
  47. package/dist/product-identity.js +1 -1
  48. package/dist/product-identity.json +5 -0
  49. package/dist/ui/components/transcript-viewport.d.ts +6 -3
  50. package/dist/ui/components/transcript-viewport.js +13 -15
  51. package/dist/ui/components/value-menu.d.ts +6 -3
  52. package/dist/ui/components/value-menu.js +6 -5
  53. package/dist/ui/settings/declarations.d.ts +13 -1
  54. package/dist/ui/settings/declarations.js +10 -1
  55. package/dist/ui/settings/index.d.ts +1 -1
  56. package/dist/ui/settings/index.js +1 -1
  57. package/dist/ui/settings/migrations.js +7 -0
  58. package/dist/ui/settings/resolution.d.ts +2 -0
  59. package/dist/ui/settings/resolution.js +13 -1
  60. package/docs/architecture/boundaries.md +1 -1
  61. package/docs/architecture/evidence/terminal-host-spike/manual-one-pane-terminal-host.md +70 -0
  62. package/docs/architecture/evidence/terminal-host-spike/manual-two-by-two-basic-terminal-host.md +32 -0
  63. package/docs/architecture/evidence/terminal-host-spike/milestone-baseline.json +115 -0
  64. package/docs/architecture/evidence/terminal-host-spike/owned-ui-foundation-prerequisite.json +48 -0
  65. package/docs/architecture/evidence/terminal-host-spike/terminal-host-provenance.json +125 -0
  66. package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-1.json +56 -0
  67. package/docs/architecture/evidence/terminal-host-spike/terminal-host-task-5-3.json +174 -0
  68. package/docs/architecture/evidence/terminal-host-spike/terminal-spike-acceptance-record.json +18 -0
  69. package/docs/architecture/internal-naming.md +1 -1
  70. package/docs/architecture/resident-tabs-requirements.md +813 -0
  71. package/docs/architecture/resident-tabs.md +502 -0
  72. package/docs/architecture/terminal-host-proof-gate.md +3 -1
  73. package/docs/architecture/terminal-host-spike-evidence.md +1 -1
  74. package/docs/openspec-archive-automation.md +12 -11
  75. package/docs/releases/0.2.6.md +44 -0
  76. package/docs/validation.md +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ /** Read only the routing flag before any UI or extension module is loaded. */
2
+ export declare function readResidentTabsSetting(configDir: string, profileId?: string): Promise<boolean>;
@@ -0,0 +1,17 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { parseOwnedUiSettingsDocument, resolveResidentTabsSetting } from "../../ui/settings/index.js";
4
+ const MAX_SETTINGS_BYTES = 256 * 1024;
5
+ /** Read only the routing flag before any UI or extension module is loaded. */
6
+ export async function readResidentTabsSetting(configDir, profileId = "a1") {
7
+ const file = resolve(configDir, "settings", `${profileId}.json`);
8
+ try {
9
+ const raw = await readFile(file, "utf8");
10
+ if (Buffer.byteLength(raw, "utf8") > MAX_SETTINGS_BYTES)
11
+ return false;
12
+ return resolveResidentTabsSetting(parseOwnedUiSettingsDocument(raw));
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
@@ -1,3 +1,3 @@
1
- export { EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
1
+ export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
2
2
  export { installFatalExit, writeFatalDiagnostic } from "./fatal-exit.js";
3
3
  export type { FatalExitOptions } from "./fatal-exit.js";
@@ -1,2 +1,2 @@
1
- export { EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
1
+ export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
2
2
  export { installFatalExit, writeFatalDiagnostic } from "./fatal-exit.js";
@@ -1,3 +1,4 @@
1
+ export declare const CLEAR_EXIT_INTERVAL_MS = 500;
1
2
  export declare const EMERGENCY_TERMINAL_RESET: string;
2
3
  export declare function restoreProcessTerminal(): void;
3
4
  export declare function restoreAfterOwnedExit(ownsTerminal: boolean, code: number | null, signal: string | null, restore?: () => void): number;
@@ -1,4 +1,5 @@
1
1
  import { writeSync } from "node:fs";
2
+ export const CLEAR_EXIT_INTERVAL_MS = 500;
2
3
  // Protocol: reset modes on both screens; setting keyboard flags to zero is idempotent,
3
4
  // unlike repeatedly popping a keyboard-protocol stack owned by a parent application.
4
5
  // Resetting parent-screen margins homes the cursor, so preserve its restored position.
@@ -44,7 +44,7 @@ export declare class OwnedEditorUxInterception {
44
44
  pasteClipboard(): boolean;
45
45
  }
46
46
  export interface PromptSelectionUxOptions {
47
- readonly copyText: (text: string) => void;
47
+ readonly copyText: (text: string, cut: boolean) => void;
48
48
  readonly readClipboardContent: () => Promise<PiShellClipboardContent | null>;
49
49
  readonly beginClipboardPaste?: () => PiShellPasteReservation;
50
50
  readonly beginTextPaste?: (text: string) => PiShellPasteReservation;
@@ -88,12 +88,12 @@ class PromptSelectionInterceptor {
88
88
  return;
89
89
  }
90
90
  if (this.keybindings.matches(data, "tui.input.copy") && this.hasSelection()) {
91
- this.options.copyText(this.options.expandCopiedText(this.#selectedText()));
91
+ this.options.copyText(this.options.expandCopiedText(this.#selectedText()), false);
92
92
  this.#collapseCopiedSelection();
93
93
  return;
94
94
  }
95
95
  if (this.keybindings.matches(data, "owned.editor.cut") && this.hasSelection()) {
96
- this.options.copyText(this.options.expandCopiedText(this.#selectedText()));
96
+ this.options.copyText(this.options.expandCopiedText(this.#selectedText()), true);
97
97
  if (!this.#deleteSelection())
98
98
  this.#deleteAtomicFocus();
99
99
  return;
@@ -185,7 +185,7 @@ export interface PiShellEditorOptions {
185
185
  readonly onFollowUp?: (() => void) | undefined;
186
186
  readonly onDequeue?: (() => void) | undefined;
187
187
  readonly onPromptSuggestionAccepted?: (text: string) => void;
188
- readonly onCopyText?: (text: string) => void;
188
+ readonly onCopyText?: (text: string, cut: boolean) => void;
189
189
  readonly readClipboardContent?: (signal?: AbortSignal) => Promise<PiShellClipboardContent | null>;
190
190
  readonly beginClipboardPaste?: () => PiShellPasteReservation;
191
191
  readonly beginTextPaste?: (text: string) => PiShellPasteReservation;
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-10T09:15:37.918Z",
8
+ "builtAt": "2026-10-12T06:07:21.173Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "51b0b2dc1c9ae577f2586c4217066929278726c2c15308d9144eb6f538e118ff",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-10T09:15:10.349Z",
8
+ "builtAt": "2026-10-12T06:06:54.072Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "ef62679e83bc1fad3d38f9f21675d9256f08d20e67fd4791571032115ca1b0e9",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-10-10T09:16:06.991Z",
8
+ "builtAt": "2026-10-12T06:07:34.911Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "c6e2cb23d012d779478fd1bff750f17687a4c5466bad7621b1b3c200a0557101",
11
+ "sha256": "3f4e2d9c37a775b8a12f15539af5421e64f11149293ab2adc1bf3b06ce85b66d",
12
12
  "size": 288256
13
13
  },
14
14
  "provenance": {
@@ -1,4 +1,4 @@
1
- declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "terminalSessionId"];
1
+ declare const ENVIRONMENT_KEYS: readonly ["clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath", "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId"];
2
2
  declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
3
3
  declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "sessionRepositoryDirectory", "sessionWorktreeClaimDirectory"];
4
4
  declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
@@ -1,7 +1,7 @@
1
1
  import rawIdentity from "./product-identity.json" with { type: "json" };
2
2
  const ENVIRONMENT_KEYS = [
3
3
  "clipboardDiagnostics", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "exitNoticePath",
4
- "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "terminalSessionId",
4
+ "paneId", "probeTrace", "processGuardianPath", "prFooterPreview", "profileHome", "runtimeDir", "sessionRuntimeId", "skipVersionCheck", "startupTrace", "suggestionDiagnostics", "tabEndpoint", "tabId", "tabToken", "tabsEndpoint", "terminalHostPath", "terminalSessionId",
5
5
  ];
6
6
  const FILESYSTEM_KEYS = ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
7
7
  const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile", "sessionRepositoryDirectory", "sessionWorktreeClaimDirectory"];
@@ -28,6 +28,11 @@
28
28
  "skipVersionCheck": "A1_SKIP_VERSION_CHECK",
29
29
  "startupTrace": "A1_STARTUP_TRACE",
30
30
  "suggestionDiagnostics": "A1_SUGGESTION_DIAGNOSTICS",
31
+ "tabEndpoint": "A1_TAB_ENDPOINT",
32
+ "tabId": "A1_TAB_ID",
33
+ "tabToken": "A1_TAB_TOKEN",
34
+ "tabsEndpoint": "A1_TABS_ENDPOINT",
35
+ "terminalHostPath": "A1_TERMINAL_HOST_PATH",
31
36
  "terminalSessionId": "A1_TERMINAL_SESSION_ID"
32
37
  },
33
38
  "state": {
@@ -36,6 +36,7 @@ export interface TranscriptViewportFrameInput {
36
36
  /** Final transient rows to bottom-align with unused viewport space while content fits. */
37
37
  readonly bottomAlignedTailRowCount?: number;
38
38
  readonly dockRows: readonly string[];
39
+ readonly explicitCopyDockRows?: number;
39
40
  readonly promptAnchors: readonly TranscriptPromptAnchor[];
40
41
  readonly width: number;
41
42
  readonly height: number;
@@ -141,6 +142,7 @@ export interface TranscriptViewportFrame {
141
142
  readonly cacheEntries: number;
142
143
  };
143
144
  }
145
+ type SelectionCopyIntent = "automatic" | "explicit";
144
146
  /**
145
147
  * A1-owned viewport state. It knows only rows and semantic prompt anchors; Pi
146
148
  * components and transcript payloads remain outside this neutral boundary.
@@ -170,9 +172,9 @@ export declare class TranscriptViewport {
170
172
  extendSelection(column: number, frameRow: number, now?: number, autoScroll?: boolean): boolean;
171
173
  releaseSelection(): boolean;
172
174
  clearSelection(): boolean;
173
- selectedText(): string | null;
175
+ selectedText(intent?: SelectionCopyIntent): string | null;
174
176
  /** Captures visible selected text, not off-screen history. */
175
- captureSelectedText(): SelectionCopySnapshot | null;
177
+ captureSelectedText(intent?: SelectionCopyIntent): SelectionCopySnapshot | null;
176
178
  scrollBy(lines: number, now?: number): boolean;
177
179
  scrollTo(position: number, now?: number): boolean;
178
180
  scrollToEnd(now?: number): boolean;
@@ -184,6 +186,7 @@ export declare class TranscriptViewport {
184
186
  clearTransient(): void;
185
187
  compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
186
188
  /** Reuses an established transcript frame when only same-height dock rows changed. */
187
- composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
189
+ composeDockOnly(dockRows: readonly string[], width: number, height: number, explicitCopyDockRows?: number): TranscriptViewportFrame | null;
188
190
  }
189
191
  export declare function assertTranscriptViewportFrameDescriptor(descriptor: TranscriptViewportFrameDescriptor): void;
192
+ export {};
@@ -63,6 +63,7 @@ export class TranscriptViewport {
63
63
  #contentWidth = 0;
64
64
  #selectionWidth = 0;
65
65
  #viewportHeight = 0;
66
+ #explicitCopyDockRows = 0;
66
67
  #firstSelectableRow = 0;
67
68
  #selectionRowAnchors = [];
68
69
  #frameId = 0;
@@ -178,22 +179,19 @@ export class TranscriptViewport {
178
179
  this.#selectionRevision += 1;
179
180
  return true;
180
181
  }
181
- selectedText() {
182
+ selectedText(intent = "automatic") {
182
183
  const selection = this.#visibleSelection();
183
184
  if (selection === undefined)
184
185
  return null;
185
- // Invariant: the blue range spans the complete frame, but automatic and Ctrl+C frame copy
186
- // admit only persistent transcript rows. Transient status, suggestions, input, and footer
187
- // chrome remain visually selectable without becoming clipboard payload.
188
186
  const copiedLines = Array.from({ length: Math.max(0, selection.end.line - selection.start.line + 1) }, (_value, index) => selection.start.line + index).filter(line => {
189
187
  const anchor = this.#selectionRowAnchors[line];
190
- return anchor?.kind === "document" && anchor.row < this.#selectableDocumentRowCount;
188
+ return anchor?.kind === "document" && anchor.row < this.#selectableDocumentRowCount
189
+ || intent === "explicit" && anchor?.kind === "dock" && anchor.fromBottom < this.#explicitCopyDockRows;
191
190
  });
192
191
  if (copiedLines.length === 0)
193
192
  return null;
194
- // Rationale: a prompt selected by itself uses semantic prompt text. Once surrounding transcript text
195
- // participates, preserve the prompt's visible prefix, alignment, and timestamp in the bulk range.
196
- const prompt = this.#promptAnchors.find(candidate => copiedLines.every(line => {
193
+ const transcriptLines = copiedLines.filter(line => this.#selectionRowAnchors[line]?.kind === "document");
194
+ const prompt = transcriptLines.length === 0 ? undefined : this.#promptAnchors.find(candidate => transcriptLines.every(line => {
197
195
  const anchor = this.#selectionRowAnchors[line];
198
196
  return anchor?.kind === "document" && anchor.row >= candidate.firstRow && anchor.row <= candidate.lastRow;
199
197
  }));
@@ -208,11 +206,11 @@ export class TranscriptViewport {
208
206
  return text.length === 0 ? null : text;
209
207
  }
210
208
  /** Captures visible selected text, not off-screen history. */
211
- captureSelectedText() {
212
- if (!this.#copyableSelection)
209
+ captureSelectedText(intent = "automatic") {
210
+ if (intent === "automatic" && !this.#copyableSelection)
213
211
  return null;
214
212
  const started = performance.now();
215
- let text = this.selectedText();
213
+ let text = this.selectedText(intent);
216
214
  if (text === null)
217
215
  return null;
218
216
  text = text.trim();
@@ -315,6 +313,7 @@ export class TranscriptViewport {
315
313
  this.#contentWidth = 0;
316
314
  this.#selectionWidth = 0;
317
315
  this.#viewportHeight = 0;
316
+ this.#explicitCopyDockRows = 0;
318
317
  this.clearTransient();
319
318
  this.#frame = null;
320
319
  }
@@ -330,6 +329,7 @@ export class TranscriptViewport {
330
329
  const theme = input.theme ?? PLAIN_THEME;
331
330
  const previousDescriptor = this.#frame?.descriptor ?? null;
332
331
  const dock = input.dockRows.length > height ? input.dockRows.slice(-height) : [...input.dockRows];
332
+ this.#explicitCopyDockRows = clamp(input.explicitCopyDockRows ?? 0, 0, dock.length);
333
333
  const viewportHeight = Math.max(0, height - dock.length);
334
334
  const bottomAlignedTailRowCount = clamp(input.bottomAlignedTailRowCount ?? 0, 0, input.documentRows.length);
335
335
  const transientAlignmentGapRows = bottomAlignedTailRowCount > 0
@@ -414,8 +414,6 @@ export class TranscriptViewport {
414
414
  visible[0] = quiet && !this.#stickyHovered ? theme.quietSticky(sticky) : sticky;
415
415
  visibleSource[0] = sourceRow;
416
416
  }
417
- // Invariant: selection uses exact visible source rows within the rendered
418
- // content boundary; paint-only guards and the rail gutter stay presentation-only.
419
417
  this.#selectionRows = [
420
418
  ...visibleSource.map(row => truncateToWidth(row, contentWidth)),
421
419
  ...dock,
@@ -426,7 +424,6 @@ export class TranscriptViewport {
426
424
  ? viewportSelectionAnchor(documentRow, documentRows[documentRow] ?? "", this.#selectableDocumentRowCount, documentRows.length)
427
425
  : { kind: "screen", row, source: this.#selectionRows[row] ?? "" };
428
426
  });
429
- // Invariant: a pinned prompt is chrome over its hidden source row, which clips like off-screen rows.
430
427
  this.#firstSelectableRow = stickyActive && viewportHeight > 0 ? 1 : 0;
431
428
  this.#selectionRowAnchors = [
432
429
  ...visibleAnchors,
@@ -609,7 +606,7 @@ export class TranscriptViewport {
609
606
  return frame;
610
607
  }
611
608
  /** Reuses an established transcript frame when only same-height dock rows changed. */
612
- composeDockOnly(dockRows, width, height) {
609
+ composeDockOnly(dockRows, width, height, explicitCopyDockRows = 0) {
613
610
  const previous = this.#frame;
614
611
  const boundedWidth = Math.max(1, width);
615
612
  const boundedHeight = Math.max(0, height);
@@ -645,6 +642,7 @@ export class TranscriptViewport {
645
642
  };
646
643
  assertTranscriptViewportFrameDescriptor(descriptor);
647
644
  this.#selectionRows = [...this.#selectionRows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight);
645
+ this.#explicitCopyDockRows = clamp(explicitCopyDockRows, 0, dock.length);
648
646
  const frame = {
649
647
  rows: [...previous.rows.slice(0, viewportHeight), ...dock].slice(0, boundedHeight),
650
648
  contentWidth: previous.contentWidth,
@@ -1,8 +1,8 @@
1
1
  import type { UiTheme } from "./theme.js";
2
2
  /**
3
3
  * The menu a value opens: anchored to the row it was opened from, marking what
4
- * is in effect, highlighting nothing until something is picked, and flipping
5
- * above its anchor only when it would otherwise run off the bottom.
4
+ * is in effect, highlighting nothing until something is picked, and laid over
5
+ * its anchor so the value in effect stays where it was read.
6
6
  */
7
7
  export interface ValueMenuState {
8
8
  /** Values offered, as they should read. */
@@ -37,7 +37,10 @@ export interface ValueMenuLayout {
37
37
  /** Columns reserved at the right edge, such as a scrollbar rail. */
38
38
  readonly reservedRight: number;
39
39
  }
40
- /** Where the menu is placed: below its anchor where there is room, else above. */
40
+ /**
41
+ * Where the menu is placed: over its anchor, so the value in effect sits on the very
42
+ * row it was read from, shifted only as far as the body edges require.
43
+ */
41
44
  export declare function valueMenuFrame(state: ValueMenuState, anchor: ValueMenuAnchor, layout: ValueMenuLayout): ValueMenuFrame;
42
45
  /** Draws the menu over the rows behind it, leaving them otherwise untouched. */
43
46
  export declare function renderValueMenu(lines: readonly string[], state: ValueMenuState, frame: ValueMenuFrame, theme: UiTheme): readonly string[];
@@ -1,13 +1,14 @@
1
1
  import { overlaySpan } from "./spans.js";
2
2
  import { displayWidth, padToWidth } from "./text.js";
3
- /** Where the menu is placed: below its anchor where there is room, else above. */
3
+ /**
4
+ * Where the menu is placed: over its anchor, so the value in effect sits on the very
5
+ * row it was read from, shifted only as far as the body edges require.
6
+ */
4
7
  export function valueMenuFrame(state, anchor, layout) {
5
8
  const bodyTop = layout.bodyTop ?? 0;
6
9
  const bodyBottom = bodyTop + layout.bodyHeight;
7
- const below = anchor.screenRow + 1;
8
- const top = below + state.choices.length <= bodyBottom
9
- ? below
10
- : Math.max(bodyTop, anchor.screenRow - state.choices.length);
10
+ const currentIndex = Math.max(0, state.current === null ? 0 : state.choices.indexOf(state.current));
11
+ const top = Math.max(bodyTop, Math.min(anchor.screenRow - currentIndex, bodyBottom - state.choices.length));
11
12
  const previewWidth = Math.max(0, ...(state.previews ?? []).map(displayWidth));
12
13
  const previewColumns = previewWidth === 0 ? 0 : previewWidth + 1;
13
14
  const width = Math.max(...state.choices.map(choice => displayWidth(choice) + previewColumns + 4), 6);
@@ -1,4 +1,4 @@
1
- export declare const OWNED_UI_SETTINGS_VERSION = 13;
1
+ export declare const OWNED_UI_SETTINGS_VERSION = 14;
2
2
  export type OwnedUiSettingValue = string | number | boolean;
3
3
  export type OwnedUiSettingApplication = "live" | "restart";
4
4
  export interface OwnedUiSettingDeclaration {
@@ -164,6 +164,18 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
164
164
  defaultValue: 8;
165
165
  allowedValues: readonly [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
166
166
  }>;
167
+ readonly residentTabs: Readonly<{
168
+ id: "residentTabs";
169
+ label: "Resident tabs";
170
+ section: Readonly<{
171
+ id: "generic";
172
+ title: "Generic";
173
+ }>;
174
+ description: "Keep bare A1 running when its terminal closes and reattach on the next launch. Resident tabs have no stop command yet; turning this off prevents new resident launches but does not stop an existing tab.";
175
+ application: "restart";
176
+ defaultValue: false;
177
+ allowedValues: readonly [true, false];
178
+ }>;
167
179
  }>;
168
180
  export type OwnedSettingId = keyof typeof OWNED_SETTING_DECLARATIONS;
169
181
  /** The value type a setting's declaration allows: a literal union for a choice, `number` for a stepped range. */
@@ -1,4 +1,4 @@
1
- export const OWNED_UI_SETTINGS_VERSION = 13;
1
+ export const OWNED_UI_SETTINGS_VERSION = 14;
2
2
  const UI_ACCENT_COLOR_CHOICES = Object.freeze([
3
3
  "purple", "blue", "cyan", "green", "orange", "pink",
4
4
  ]);
@@ -124,6 +124,15 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
124
124
  defaultValue: 8,
125
125
  allowedValues: Object.freeze([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
126
126
  }),
127
+ residentTabs: Object.freeze({
128
+ id: "residentTabs",
129
+ label: "Resident tabs",
130
+ section: GENERIC_SECTION,
131
+ description: "Keep bare A1 running when its terminal closes and reattach on the next launch. Resident tabs have no stop command yet; turning this off prevents new resident launches but does not stop an existing tab.",
132
+ application: "restart",
133
+ defaultValue: false,
134
+ allowedValues: Object.freeze([true, false]),
135
+ }),
127
136
  });
128
137
  /** The declarations in declaration order, for resolution, persistence, and the settings screen. */
129
138
  export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze(Object.values(OWNED_SETTING_DECLARATIONS));
@@ -2,7 +2,7 @@ export { OWNED_SETTING_DECLARATIONS, OWNED_UI_SETTINGS_VERSION, OWNED_UI_SETTING
2
2
  export type { OwnedSettingId, OwnedSettingValueOf, OwnedUiSettingApplication, OwnedUiSettingDeclaration, OwnedUiSettingValue } from "./declarations.js";
3
3
  export { OWNED_UI_SETTINGS_MIGRATIONS, assertOwnedUiSettingsMigrations, migrationsFrom } from "./migrations.js";
4
4
  export type { OwnedUiSettingsMigration } from "./migrations.js";
5
- export { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, settingValue } from "./resolution.js";
5
+ export { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, resolveResidentTabsSetting, settingValue } from "./resolution.js";
6
6
  export type { OwnedUiResolvedSetting, OwnedUiSettingSource, OwnedUiSettingsDocument, OwnedUiSettingsNotice, OwnedUiSettingsNoticeCode, OwnedUiSettingsResolution, ResolveOwnedUiSettingsInput, } from "./resolution.js";
7
7
  export { AGENT_SECTION_ID, buildOwnedUiSettingsSections, findOwnedUiSettingsEntry } from "./sections.js";
8
8
  export type { AgentSettingsSnapshot, BuildOwnedUiSettingsSectionsInput, OwnedUiSettingsBackend, OwnedUiSettingsEntry, OwnedUiSettingsSection, } from "./sections.js";
@@ -1,5 +1,5 @@
1
1
  export { OWNED_SETTING_DECLARATIONS, OWNED_UI_SETTINGS_VERSION, OWNED_UI_SETTING_DECLARATIONS, assertOwnedUiSettingDeclarations, findOwnedUiSettingDeclaration, isOwnedSettingId, } from "./declarations.js";
2
2
  export { OWNED_UI_SETTINGS_MIGRATIONS, assertOwnedUiSettingsMigrations, migrationsFrom } from "./migrations.js";
3
- export { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, settingValue } from "./resolution.js";
3
+ export { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, resolveResidentTabsSetting, settingValue } from "./resolution.js";
4
4
  export { AGENT_SECTION_ID, buildOwnedUiSettingsSections, findOwnedUiSettingsEntry } from "./sections.js";
5
5
  export { OwnedSettingsManager } from "./manager.js";
@@ -104,6 +104,13 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
104
104
  return { ...values };
105
105
  },
106
106
  }),
107
+ Object.freeze({
108
+ to: 14,
109
+ description: "Introduce the opt-in resident tabs preview with a disabled default.",
110
+ migrate(values) {
111
+ return { ...values };
112
+ },
113
+ }),
107
114
  ]);
108
115
  export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
109
116
  const firstProduced = currentVersion - migrations.length + 1;
@@ -42,4 +42,6 @@ export interface ResolveOwnedUiSettingsInput {
42
42
  */
43
43
  export declare function resolveOwnedUiSettings(input: ResolveOwnedUiSettingsInput): OwnedUiSettingsResolution;
44
44
  export declare function settingValue(resolution: OwnedUiSettingsResolution, id: string): OwnedUiSettingValue | null;
45
+ /** Resolve the launch-routing flag without constructing the UI settings manager. */
46
+ export declare function resolveResidentTabsSetting(document: OwnedUiSettingsDocument | null): boolean;
45
47
  export declare function documentFrom(resolution: OwnedUiSettingsResolution): OwnedUiSettingsDocument;
@@ -1,4 +1,4 @@
1
- import { OWNED_UI_SETTINGS_VERSION, findOwnedUiSettingDeclaration, } from "./declarations.js";
1
+ import { OWNED_SETTING_DECLARATIONS, OWNED_UI_SETTINGS_VERSION, findOwnedUiSettingDeclaration, } from "./declarations.js";
2
2
  import { migrationsFrom } from "./migrations.js";
3
3
  export function parseOwnedUiSettingsDocument(raw) {
4
4
  let parsed;
@@ -122,6 +122,18 @@ export function resolveOwnedUiSettings(input) {
122
122
  export function settingValue(resolution, id) {
123
123
  return resolution.settings.find(setting => setting.declaration.id === id)?.value ?? null;
124
124
  }
125
+ /** Resolve the launch-routing flag without constructing the UI settings manager. */
126
+ export function resolveResidentTabsSetting(document) {
127
+ if (document === null || document.version !== OWNED_UI_SETTINGS_VERSION)
128
+ return false;
129
+ const resolution = resolveOwnedUiSettings({
130
+ declarations: [OWNED_SETTING_DECLARATIONS.residentTabs],
131
+ migrations: [],
132
+ document,
133
+ currentVersion: OWNED_UI_SETTINGS_VERSION,
134
+ });
135
+ return settingValue(resolution, "residentTabs") === true;
136
+ }
125
137
  export function documentFrom(resolution) {
126
138
  const values = { ...resolution.preserved };
127
139
  for (const setting of resolution.settings) {
@@ -53,7 +53,7 @@ The engine host removes A1's own obstacles to several Pi sessions in one process
53
53
 
54
54
  ## Planned multi-agent boundaries
55
55
 
56
- The multi-agent workspace feature, its dependency-free contracts, the structured-agent runtime, and the Node side of the terminal-host protocol were removed from `develop` while the multi-agent plan is on hold; their last state is preserved on the `archive/multi-agent-workspace` branch. A future implementation starts from the current codebase rather than from that copy. The boundary rules below remain the design intent for that work.
56
+ The multi-agent workspace feature, its dependency-free contracts, the structured-agent runtime, and the Node side of the terminal-host protocol were removed from `develop`; their last state is commit `243eb7a7` in `develop` history. That structured-workspace plan was retired on 2026-10-10 in favor of `add-persistent-multi-agent-tabs`, where every tab runs the complete A1 UI under the native terminal host. A future implementation starts from the current codebase rather than from that copy. The boundary rules below remain the design intent for that work.
57
57
 
58
58
  The workspace feature may depend on workspace contracts and the structured/terminal-host foundations. Those foundations may depend only on dependency-free workspace contracts, never on product features, launch profiles, or each other unless an approved capability adds the dependency. Launch profiles do not import, initialize, launch, or connect to composed terminal-host infrastructure.
59
59
 
@@ -0,0 +1,70 @@
1
+ # Manual one-pane terminal-host result
2
+
3
+ ## First manual artifact
4
+
5
+ - Artifact: `native/terminal-host/target/debug/addone-terminal-host.exe`
6
+ - SHA-256: `8d81db199687a9b8f195b4e40cb4b235de3ea00ffeb9cf55f9c64b1b3582cbba`
7
+ - Size: 2,214,912 bytes
8
+ - User-controlled run: one fullscreen `cmd.exe` pane inside Windows Terminal
9
+
10
+ The user reported that typing commands, cursor movement, colors, Windows Terminal resize, and fast output worked correctly.
11
+
12
+ The first artifact had one finding: `Ctrl+Shift+Q` did not exit.
13
+
14
+ ## Corrective artifact
15
+
16
+ - Corrective artifact SHA-256: `1025a0823114477b7ab311ef55098647fc337b98a9194cf389fc124a047360bf`
17
+ - Size: 2,227,712 bytes
18
+ - Correction: accept `Ctrl+Q` or `Ctrl+Shift+Q`, including uppercase key delivery.
19
+ - Correction: resolve Windows command shims through generic PATH/PATHEXT lookup; `pi` resolves to `C:\Users\tprokopiev\AppData\Roaming\npm\pi.cmd`.
20
+ - Automated non-interactive result after correction: build, version/provenance, terminal model, PTY start, process exit, and cleanup passed.
21
+
22
+ ## Follow-up artifact
23
+
24
+ - Artifact SHA-256: `b083af60eb8c4ca90438e4022c4e6d10b1410dda5554f554a8b814ba2cdee291`
25
+ - Size: 2,238,464 bytes
26
+ - Removed the host-owned quit shortcut so `Ctrl+Q` no longer intercepts input.
27
+ - Added generic calling-shell ancestry detection; this Git Bash environment resolves to `C:\Program Files\Git\usr\bin\bash.exe --login -i`.
28
+ - Disabled mouse capture by default so the outer terminal can provide native selection.
29
+ - Added `Shift+PageUp`/`Shift+PageDown` and `Shift+Up`/`Shift+Down` host scrollback.
30
+ - Non-interactive terminal model and PTY cleanup probes pass.
31
+
32
+ ## Theme-preservation artifact
33
+
34
+ - Artifact SHA-256: `9989373d1ee28ac42bf7a6b1be02d68b92c23b71f14336db493a93d76df48198`
35
+ - Size: 2,238,464 bytes
36
+ - The frame composer now emits the outer terminal's default foreground/background for unstyled cells instead of forcing black.
37
+ - The automated probe asserts the default-background sequence and passes.
38
+
39
+ ## Palette-preservation artifact
40
+
41
+ - Artifact SHA-256: `203219f0ee79f3086467dccb5479b5749b09f8bac79623f84682545d374ba2f4`
42
+ - Size: 2,238,976 bytes
43
+ - Palette-indexed child colors are emitted as palette indexes (`38;5;n` / `48;5;n`) instead of being converted through Ghostty's default RGB palette.
44
+ - The automated probe asserts default-background and palette-passthrough sequences and passes.
45
+
46
+ ## Scrollback artifact
47
+
48
+ - Artifact SHA-256: `6280786d713abe37eb979e309d27b7ed1eb1f1e1e76e485b0bc5f9c357391ca6`
49
+ - Size: 2,246,656 bytes
50
+ - Mouse capture is enabled so wheel events reach the host; wheel and Shift+navigation scroll the retained terminal viewport.
51
+ - Viewport changes mark the frame dirty before composition.
52
+ - The non-interactive probe verifies retained scrollback, viewport movement, and repainted earlier content.
53
+ - Use the outer terminal's modifier-based selection, usually Shift+drag in Windows Terminal, while pointer capture is active.
54
+
55
+ ## Selection and exit artifact
56
+
57
+ - Artifact SHA-256: `fc6d9d1ecbaae4718b850a42b5e6868758d1f4c8c8e1a7645237115d1f1d883c`
58
+ - Size: 2,266,624 bytes
59
+ - Plain left-drag selects terminal content with one uniform white background and black foreground; release emits an OSC 52 clipboard write.
60
+ - Double-click selects a word; triple-click selects a line.
61
+ - Ctrl+C clears an active host selection before reaching the child; without selection, interrupt keys remain child-owned.
62
+ - Mouse wheel scrolls the retained viewport. Home, End, PageUp, PageDown, and arrow keys remain child-owned; no host keyboard scroll binding is active.
63
+ - PTY output-channel closure exits the host when the child session ends.
64
+ - Non-interactive probes pass for terminal rendering, scrollback, selection, and PTY cleanup.
65
+
66
+ Manual retest should verify plain drag selection, double/triple click, mouse wheel, and Ctrl+C selection clearing. No terminal was launched or driven by automation on the active workstation.
67
+
68
+ ## User acceptance
69
+
70
+ The user manually confirmed that the vanilla-Pi terminal-host proof launches and works after the interaction corrections. The accepted one-pane pipeline includes theme/palette preservation, retained scrollback, mouse wheel scrolling, plain drag selection, word/line click selection, selection clearing with Ctrl+C, and child-owned interrupt behavior. Task 5.1 is accepted; the next gate is the 2×2 multi-pane proof.
@@ -0,0 +1,32 @@
1
+ # Manual 2×2 terminal-host basic result
2
+
3
+ ## Exact artifact
4
+
5
+ - Recorded at: `2026-08-14T16:13:20.217Z`
6
+ - Source commit: `aeb7421ff5cc82bc5da04cfb0d356e8f8041b6cb`
7
+ - Artifact: `native/terminal-host/target/debug/addone-terminal-host.exe`
8
+ - SHA-256: `896e1b5d2e2730dfd8e591fe3bd565a2aa283fb3ec6f64e8345745c6afcf024d`
9
+ - Size: 2,367,488 bytes
10
+ - Invocation: user-controlled `npm run proof:terminal-host`
11
+ - Surface: one existing outer terminal, one fixed tab, four independently PTY-backed Pi sessions
12
+
13
+ ## User-reported result
14
+
15
+ The user manually confirmed all of the following against the exact artifact:
16
+
17
+ - all four panes launched Pi;
18
+ - the four-pane presentation worked without observed flicker;
19
+ - pane focus and input routing worked;
20
+ - changing or clicking the focused pane preserved content in every pane after the repaint correction.
21
+
22
+ The user accepted this basic multi-pane proof result.
23
+
24
+ ## Explicit product disposition
25
+
26
+ The fixed 2×2 grid and dashed pane outlines are disposable proof scaffolding, not an accepted production layout or UI design. This result proves the native multi-pane pipeline only. It does not approve carrying the fixed geometry, border treatment, or proof interaction model into the product.
27
+
28
+ Production tabs and panes are expected later to use authoritative arbitrary layouts selected through the AddOne UI. The proof implementation may be removed or replaced after the exact-artifact 2×2 gate is finished; it must not become the default production workspace by inertia.
29
+
30
+ ## Gate scope
31
+
32
+ This record does not complete the formal tasks 5.3–5.6 acceptance gate. High-rate output, broader terminal modes and interactions, resize/terminal-size measurements, resource observations, abnormal exits, isolated-worker requirements, and the final stop/go decision remain separately required. No automated physical terminal control was performed on the active workstation.