@timurproko/a1 0.2.6 → 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 (61) 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/foundation/launch-context/index.d.ts +6 -0
  15. package/dist/foundation/launch-context/index.js +15 -0
  16. package/dist/foundation/launch-guardian/main.d.ts +5 -0
  17. package/dist/foundation/launch-guardian/main.js +2 -1
  18. package/dist/foundation/release/bootstrap.d.ts +4 -0
  19. package/dist/foundation/release/bootstrap.js +33 -6
  20. package/dist/foundation/resident-tabs/bridge-client.d.ts +36 -0
  21. package/dist/foundation/resident-tabs/bridge-client.js +157 -0
  22. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +28 -0
  23. package/dist/foundation/resident-tabs/ensure-terminal-host.js +115 -0
  24. package/dist/foundation/resident-tabs/index.d.ts +5 -0
  25. package/dist/foundation/resident-tabs/index.js +3 -0
  26. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  27. package/dist/foundation/resident-tabs/protocol.js +195 -0
  28. package/dist/foundation/resident-tabs/routing.d.ts +10 -0
  29. package/dist/foundation/resident-tabs/routing.js +8 -0
  30. package/dist/foundation/resident-tabs/settings.d.ts +2 -0
  31. package/dist/foundation/resident-tabs/settings.js +17 -0
  32. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  33. package/dist/foundation/terminal-cleanup/index.js +1 -1
  34. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
  35. package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
  36. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  37. package/dist/integrations/pi/components/owned-editor-ux.js +2 -2
  38. package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
  39. package/dist/native/darwin-arm64/manifest.json +1 -1
  40. package/dist/native/linux-x64/manifest.json +1 -1
  41. package/dist/native/win32-x64/manifest.json +2 -2
  42. package/dist/native/win32-x64/process-guardian.exe +0 -0
  43. package/dist/product-identity.d.ts +1 -1
  44. package/dist/product-identity.js +1 -1
  45. package/dist/product-identity.json +5 -0
  46. package/dist/ui/components/transcript-viewport.d.ts +6 -3
  47. package/dist/ui/components/transcript-viewport.js +13 -15
  48. package/dist/ui/settings/declarations.d.ts +13 -1
  49. package/dist/ui/settings/declarations.js +10 -1
  50. package/dist/ui/settings/index.d.ts +1 -1
  51. package/dist/ui/settings/index.js +1 -1
  52. package/dist/ui/settings/migrations.js +7 -0
  53. package/dist/ui/settings/resolution.d.ts +2 -0
  54. package/dist/ui/settings/resolution.js +13 -1
  55. package/docs/architecture/internal-naming.md +1 -1
  56. package/docs/architecture/resident-tabs-requirements.md +813 -0
  57. package/docs/architecture/resident-tabs.md +502 -0
  58. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  59. package/docs/openspec-archive-automation.md +12 -11
  60. package/docs/validation.md +1 -1
  61. package/package.json +1 -1
package/bin/guardian.js CHANGED
@@ -16,8 +16,13 @@ const profileId = readLaunchContext(process.env, "profile").launchProfile;
16
16
  if (releaseRoot === undefined || profileId === undefined) throw new Error("A1 launch context is incomplete");
17
17
 
18
18
  const sessionSelection = parseSessionSelection(process.argv.slice(2));
19
+ const rootCommand = launchContext.residentRootExecutable === undefined ? undefined : {
20
+ executable: launchContext.residentRootExecutable,
21
+ arguments: JSON.parse(launchContext.residentRootArguments ?? "[]"),
22
+ };
19
23
  Promise.resolve().then(() => runLaunchGuardian({
20
24
  ...(sessionSelection === undefined ? {} : { sessionSelection }),
25
+ ...(rootCommand === undefined ? {} : { rootCommand }),
21
26
  profileId,
22
27
  releaseRoot,
23
28
  uiEntry: resolve(releaseRoot, "bin", "ui.js"),
@@ -0,0 +1,18 @@
1
+ export interface TabLaunchCredentials {
2
+ readonly tabId: string;
3
+ readonly token: string;
4
+ readonly endpoint: string;
5
+ }
6
+
7
+ export function formatResidentFallbackNotice(reason: string): string;
8
+
9
+ export function assignFreshSessionRuntimeId(
10
+ environment: Record<string, string | undefined>,
11
+ name: string,
12
+ createId: () => string,
13
+ ): string;
14
+
15
+ export function consumeTabLaunchContext(
16
+ arguments_: readonly string[],
17
+ environment: Record<string, string | undefined>,
18
+ ): { readonly uiArguments: string[]; readonly tabCredentials: TabLaunchCredentials | undefined };
@@ -0,0 +1,36 @@
1
+ const TAB_ENVIRONMENT_NAMES = ["A1_TAB_ID", "A1_TAB_TOKEN", "A1_TAB_ENDPOINT"];
2
+
3
+ /** @param {string} reason */
4
+ export function formatResidentFallbackNotice(reason) {
5
+ return `Resident tabs unavailable (${reason}); running a single agent.`;
6
+ }
7
+
8
+ /**
9
+ * @param {Record<string, string | undefined>} environment
10
+ * @param {string} name
11
+ * @param {() => string} createId
12
+ */
13
+ export function assignFreshSessionRuntimeId(environment, name, createId) {
14
+ const id = createId();
15
+ if (typeof id !== "string" || id.length === 0 || id.includes("\0")) throw new Error("Session runtime identity generation failed");
16
+ environment[name] = id;
17
+ return id;
18
+ }
19
+
20
+ /**
21
+ * Security: consume bridge credentials into caller-private memory and erase their inheritable
22
+ * environment entries before any extension-capable module loads.
23
+ * @param {readonly string[]} arguments_
24
+ * @param {Record<string, string | undefined>} environment
25
+ */
26
+ export function consumeTabLaunchContext(arguments_, environment) {
27
+ const uiArguments = [...arguments_];
28
+ const tabArgument = uiArguments.indexOf("--tab");
29
+ const tabMode = tabArgument !== -1;
30
+ if (tabMode) uiArguments.splice(tabArgument, 1);
31
+ const [tabId, token, endpoint] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
32
+ for (const name of TAB_ENVIRONMENT_NAMES) delete environment[name];
33
+ if (!tabMode) return { uiArguments, tabCredentials: undefined };
34
+ if (!tabId || !token || !endpoint) throw new Error("A1 tab mode requires bridge credentials");
35
+ return { uiArguments, tabCredentials: { tabId, token, endpoint } };
36
+ }
package/bin/ui.js CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Security: this helper has no A1, Pi, tool, or extension dependency and erases credentials before those modules load.
4
+ const { assignFreshSessionRuntimeId, consumeTabLaunchContext, formatResidentFallbackNotice } = await import("./tab-launch-context.js");
5
+ const { uiArguments, tabCredentials } = consumeTabLaunchContext(process.argv.slice(2), process.env);
6
+
3
7
  // Invariant: one terminal module identity per process, decided before any A1 or Pi module loads.
4
8
  const { installPinnedPiTuiResolver } = await import("./module-resolver.js");
5
9
  installPinnedPiTuiResolver(new URL("..", import.meta.url).pathname.replace(/^\/(.:)/, "$1").replace(/\/$/, ""));
@@ -14,7 +18,7 @@ if (profile === "a1") {
14
18
  import("node:crypto"),
15
19
  import("../dist/product-identity.js"),
16
20
  ]);
17
- process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId] = randomUUID();
21
+ assignFreshSessionRuntimeId(process.env, PRODUCT_IDENTITY.environment.sessionRuntimeId, randomUUID);
18
22
  }
19
23
  const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/fatal-exit.js");
20
24
  const { resolveProductPaths } = await import("../dist/foundation/lifecycle/paths.js");
@@ -51,8 +55,11 @@ const [
51
55
  identity.assertSinglePiTuiModuleAtLaunch(packageRootPath, (/** @type {string} */ message) => process.stderr.write(message));
52
56
  await startup.markStartupPhase(process.env, "ui-modules-loaded");
53
57
 
54
- const sessionSelection = parseSessionSelection(process.argv.slice(2));
55
- Promise.resolve().then(() => {
58
+ const sessionSelection = parseSessionSelection(uiArguments);
59
+ Promise.resolve().then(async () => {
60
+ const tab = tabCredentials === undefined ? undefined : {
61
+ bridge: new (await import("../dist/foundation/resident-tabs/bridge-client.js")).TabBridgeClient({ credentials: tabCredentials }),
62
+ };
56
63
  if (sessionSelection && profile !== "a1") throw new Error("session selection requires the normal A1 profile");
57
64
  return runSelectedInteractiveRuntime(profile, {
58
65
  ownedUi: async (profileId, ownedSurfaces) => {
@@ -69,6 +76,10 @@ Promise.resolve().then(() => {
69
76
  projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
70
77
  sessionForkPrompt: createConsoleSessionForkPrompt(),
71
78
  ...(sessionSelection === undefined ? {} : { sessionSelection }),
79
+ ...(tab === undefined ? {} : { tab }),
80
+ ...(launchContext.residentFallback === undefined ? {} : {
81
+ residentFallbackNotice: formatResidentFallbackNotice(launchContext.residentFallback),
82
+ }),
72
83
  });
73
84
  runningApplication = application;
74
85
  return await runOwnedUi({ application, ...(settings === null ? {} : { settings }) });
@@ -12,16 +12,14 @@ export interface HelperPoolOptions {
12
12
  readonly stop: (child: ChildProcess) => void;
13
13
  readonly idleMs?: number;
14
14
  }
15
- /**
16
- * Keeps one forked IPC helper ready so a request takes a live child instead of paying fork and module
17
- * load on its own critical path. The spare stays idle on its channel until it is taken; a spare that
18
- * exits or idles too long is dropped, and the next request forks cold and warms a replacement afterwards.
19
- */
15
+ /** Holds one idle IPC helper; requests take it, and exits, expiry, or use trigger replacement. */
20
16
  export declare class HelperPool {
21
17
  #private;
22
18
  constructor(options: HelperPoolOptions);
23
19
  /** Whether a spare is currently held (ready or still starting). */
24
20
  get warmed(): boolean;
21
+ /** Whether the held spare has announced that module loading is complete. */
22
+ get ready(): boolean;
25
23
  /** Opts the pool in and forks the first spare; from here on `replenish` keeps one ready after each request. */
26
24
  warm(): void;
27
25
  /** Forks the next spare after a request released its child; a no-op until the owner warmed the pool. A fork failure leaves the pool empty. */
@@ -1,10 +1,6 @@
1
1
  /** How long an unused spare helper may idle before it is stopped so a background session holds no extra process. */
2
2
  export const HELPER_SPARE_IDLE_MS = 5 * 60_000;
3
- /**
4
- * Keeps one forked IPC helper ready so a request takes a live child instead of paying fork and module
5
- * load on its own critical path. The spare stays idle on its channel until it is taken; a spare that
6
- * exits or idles too long is dropped, and the next request forks cold and warms a replacement afterwards.
7
- */
3
+ /** Holds one idle IPC helper; requests take it, and exits, expiry, or use trigger replacement. */
8
4
  export class HelperPool {
9
5
  #fork;
10
6
  #stop;
@@ -19,6 +15,8 @@ export class HelperPool {
19
15
  }
20
16
  /** Whether a spare is currently held (ready or still starting). */
21
17
  get warmed() { return this.#spare !== undefined; }
18
+ /** Whether the held spare has announced that module loading is complete. */
19
+ get ready() { return this.#spare?.ready === true; }
22
20
  /** Opts the pool in and forks the first spare; from here on `replenish` keeps one ready after each request. */
23
21
  warm() {
24
22
  if (this.#disposed)
@@ -43,6 +43,11 @@ export interface OwnedUiShellEngineOptions {
43
43
  };
44
44
  /** Bare A1 selects the owned bounded viewport; comparison profiles stay pinned. */
45
45
  readonly sessionLayout?: "pinned" | "custom-viewport";
46
+ /** Resident tab quit routes detach the outer client without stopping this engine session. */
47
+ readonly residentTab?: {
48
+ requestDetach(): Promise<boolean>;
49
+ readonly unavailableNotice?: string;
50
+ };
46
51
  }
47
52
  /** Terminal, startup, viewport, and presentation-cadence seams; production leaves most at their defaults. */
48
53
  export interface OwnedUiShellPresentationOptions {
@@ -182,7 +187,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
182
187
  readonly onPromptSuggestionAccepted?: (text: string) => void;
183
188
  readonly onInputSurfaceChanged?: () => void;
184
189
  readonly onPromptSuggestionEligibilityChanged?: () => void;
185
- readonly onCopyText?: (text: string) => void;
190
+ readonly onCopyText?: (text: string, cut: boolean) => void;
186
191
  readonly readClipboardContent?: (signal?: AbortSignal) => Promise<PiShellClipboardContent | null>;
187
192
  readonly captureClipboardPaste?: () => PasteSource;
188
193
  readonly pasteDiagnostics?: (event: PasteEvent) => void;
@@ -458,7 +458,7 @@ export class OwnedUiSessionShellRoot {
458
458
  && snapshot.inputSurface === inputSurface
459
459
  && snapshot.viewportRevision === viewportRevision
460
460
  && snapshot.selectionRevision === selectionRevision
461
- ? this.#viewportController.composeDockOnly(dockRows, width, height)
461
+ ? this.#viewportController.composeDockOnly(dockRows, width, height, dock.footerRows)
462
462
  : null;
463
463
  if (frame === null) {
464
464
  frame = this.#viewportController.compose({
@@ -474,6 +474,7 @@ export class OwnedUiSessionShellRoot {
474
474
  ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
475
475
  bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
476
476
  dockRows,
477
+ explicitCopyDockRows: dock.footerRows,
477
478
  promptAnchors: document.promptAnchors,
478
479
  width,
479
480
  height,
@@ -603,7 +604,6 @@ export class OwnedUiSessionShellRoot {
603
604
  const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
604
605
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
605
606
  const input = this.#inputSurface.render(width);
606
- // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
607
607
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
608
608
  const belowWidgets = this.#renderWidgets("belowEditor", width);
609
609
  const footer = this.#renderFooter(width);
@@ -614,6 +614,7 @@ export class OwnedUiSessionShellRoot {
614
614
  inputOffset,
615
615
  editorOffset: inputOffset + (body?.rowOffset ?? 0),
616
616
  inputRows: body?.rowCount ?? input.length,
617
+ footerRows: footer.length,
617
618
  };
618
619
  }
619
620
  #renderCopyAcknowledgement(width) {
@@ -3,7 +3,7 @@ import { createResponseCopyExecutor, hasAsyncClipboardOutput, responseCopyDestin
3
3
  import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
4
4
  import { PromptHistoryController } from "./prompt-history-controller.js";
5
5
  import { PRODUCT_TEXT } from "../../product-identity.js";
6
- import { boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
6
+ import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
7
7
  import { assertOwnedUiCommand, OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
8
8
  import { assertPromptImages, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
9
9
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
@@ -34,6 +34,7 @@ export class OwnedUiSessionPresenter {
34
34
  #host;
35
35
  #terminal;
36
36
  #cwd;
37
+ #residentTabOptions;
37
38
  #listeners = new Set();
38
39
  #unsubscribe;
39
40
  #unsubscribePromptSuggestions;
@@ -104,6 +105,7 @@ export class OwnedUiSessionPresenter {
104
105
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
105
106
  this.#suggestionModelKey = modelKey(this.backend.session.view());
106
107
  this.#cwd = cwd;
108
+ this.#residentTabOptions = options.engine.residentTab;
107
109
  this.#routeHost = routeHost ?? null;
108
110
  this.#startupRoute = options.engine.startupRoute;
109
111
  this.#customViewport = sessionLayout === "custom-viewport";
@@ -218,9 +220,9 @@ export class OwnedUiSessionPresenter {
218
220
  promptSuggestionController?.invalidate();
219
221
  this.#promptHistory?.synchronize();
220
222
  },
221
- onCopyText: text => {
223
+ onCopyText: (text, cut) => {
222
224
  if (this.#responseCopy !== null) {
223
- void this.#responseCopy.submitText(text);
225
+ this.#trackCopy(this.#responseCopy.submitText(text), cut ? undefined : text.length);
224
226
  return;
225
227
  }
226
228
  const write = () => {
@@ -626,21 +628,21 @@ export class OwnedUiSessionPresenter {
626
628
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
627
629
  if (routed.copySelection !== undefined) {
628
630
  const snapshot = routed.copySelection;
629
- const acknowledgement = copyAcknowledgement(snapshot);
630
- const intent = ++this.#copyIntentSequence;
631
- void this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite).then(result => {
632
- if (this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
633
- || result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
634
- return;
635
- if (acknowledgement.hasNonWhitespace) {
636
- this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
637
- }
638
- });
631
+ this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
639
632
  }
640
633
  if (!routed.consumed)
641
634
  return routed.data === data ? undefined : { data: routed.data };
642
635
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
643
636
  }
637
+ #trackCopy(delivery, count) {
638
+ const intent = ++this.#copyIntentSequence;
639
+ void delivery?.then(({ outcome }) => {
640
+ if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
641
+ || outcome !== "delivered" && outcome !== "submitted-unverified")
642
+ return;
643
+ this.root.showCopyAcknowledgement(`copied ${count} chars to clipboard`);
644
+ });
645
+ }
644
646
  #promptSuggestionPresentationBlockReason(identity) {
645
647
  const view = this.view();
646
648
  if (this.#disposed)
@@ -795,7 +797,7 @@ export class OwnedUiSessionPresenter {
795
797
  }
796
798
  async clearOrExit(now = Date.now()) {
797
799
  this.#promptSuggestions?.abortPending();
798
- if (now - this.#lastClearTime < 500)
800
+ if (now - this.#lastClearTime < CLEAR_EXIT_INTERVAL_MS)
799
801
  return this.shutdown();
800
802
  this.root.editor.setText("");
801
803
  this.#lastClearTime = now;
@@ -1149,10 +1151,23 @@ export class OwnedUiSessionPresenter {
1149
1151
  this.#presenters.openSettingsSelector(this.#dialogHost);
1150
1152
  }
1151
1153
  shutdown() {
1152
- this.#shutdownPromise ??= this.#shutdown();
1154
+ this.#shutdownPromise ??= this.#shutdown().finally(() => {
1155
+ // Invariant: a rejected resident detach is retryable; ordinary shutdown has disposed the shell already.
1156
+ if (!this.#disposed)
1157
+ this.#shutdownPromise = undefined;
1158
+ });
1153
1159
  return this.#shutdownPromise;
1154
1160
  }
1155
1161
  async #shutdown() {
1162
+ const residentTab = this.#residentTab();
1163
+ if (residentTab !== undefined) {
1164
+ const detached = await residentTab.requestDetach().catch(() => false);
1165
+ if (detached)
1166
+ return { outcome: "completed", diagnostic: null };
1167
+ this.root.appendWorkflowStatus(residentTab.unavailableNotice ?? "Press Ctrl+C twice to leave; this agent keeps running.");
1168
+ this.#terminal.requestRender();
1169
+ return rejected("resident tab detach is unavailable");
1170
+ }
1156
1171
  try {
1157
1172
  const quit = this.backend.workflows.executeWorkflow({ command: "quit", argument: "" });
1158
1173
  // Invariant: a slow or hung engine quit never holds the terminal past the cleanup deadline. The
@@ -1168,6 +1183,9 @@ export class OwnedUiSessionPresenter {
1168
1183
  this.#resolveStoppedLifecycle();
1169
1184
  }
1170
1185
  }
1186
+ #residentTab() {
1187
+ return this.#residentTabOptions;
1188
+ }
1171
1189
  async runWorkflow(request) {
1172
1190
  if (request.command === "quit")
1173
1191
  return this.shutdown();
@@ -1920,16 +1938,15 @@ export class OwnedUiSessionShell extends OwnedUiSessionPresenter {
1920
1938
  function isCoalescedStreamBlock(kind) {
1921
1939
  return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
1922
1940
  }
1923
- function copyAcknowledgement(snapshot) {
1941
+ function copyCharacters(snapshot) {
1924
1942
  if (snapshot.literal !== true)
1925
- return { characters: snapshot.sourceUnits, hasNonWhitespace: true };
1926
- let characters = Math.max(0, snapshot.rows.length - 1);
1927
- let hasNonWhitespace = false;
1928
- for (const row of snapshot.rows) {
1929
- characters += row.text.length;
1930
- hasNonWhitespace ||= /\S/u.test(row.text);
1931
- }
1932
- return { characters, hasNonWhitespace };
1943
+ return snapshot.sourceUnits;
1944
+ let count = Math.max(0, snapshot.rows.length - 1), hasContent = false;
1945
+ for (const { text } of snapshot.rows) {
1946
+ count += text.length;
1947
+ hasContent ||= /\S/u.test(text);
1948
+ }
1949
+ return hasContent ? count : undefined;
1933
1950
  }
1934
1951
  export function formatSessionResumeCommand(metadata) {
1935
1952
  const args = [PRODUCT_TEXT.commandName];
@@ -64,7 +64,7 @@ export declare class SessionViewportController {
64
64
  setOverlaySurfaces(surfaces: readonly PiTuiPointerSurface[] | null): void;
65
65
  get inputGeometryReady(): boolean;
66
66
  compose(input: Omit<TranscriptViewportFrameInput, "pointerPosition">): TranscriptViewportFrame;
67
- composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
67
+ composeDockOnly(dockRows: readonly string[], width: number, height: number, explicitCopyDockRows?: number): TranscriptViewportFrame | null;
68
68
  /** Returns true when document wrapping may have changed. */
69
69
  setConfig(config: OwnedUiViewportSettings): boolean;
70
70
  resumeFollowing(): void;
@@ -165,8 +165,8 @@ export class SessionViewportController {
165
165
  }
166
166
  return frame;
167
167
  }
168
- composeDockOnly(dockRows, width, height) {
169
- return this.#viewport.composeDockOnly(dockRows, width, height);
168
+ composeDockOnly(dockRows, width, height, explicitCopyDockRows = 0) {
169
+ return this.#viewport.composeDockOnly(dockRows, width, height, explicitCopyDockRows);
170
170
  }
171
171
  /** Returns true when document wrapping may have changed. */
172
172
  setConfig(config) {
@@ -307,7 +307,7 @@ export class SessionViewportController {
307
307
  this.#requestRender();
308
308
  }
309
309
  else if (this.#viewport.hasSelection) {
310
- const copySelection = this.#viewport.captureSelectedText();
310
+ const copySelection = this.#viewport.captureSelectedText("explicit");
311
311
  this.#viewport.clearSelection();
312
312
  this.#stopSelectionAutoScroll();
313
313
  this.#requestRender();
@@ -10,6 +10,7 @@ import type { OwnedUiApplicationPort, PresentationTerminalPort } from "../contra
10
10
  import type { AgentSettingsPort } from "../contracts/agent-engine/index.js";
11
11
  import type { OwnedUiPromptSuggestionGeneratorPort, UiAccentColor } from "../contracts/owned-ui/index.js";
12
12
  import type { ReleaseNoteCatalog } from "../features/owned-ui/release-notes.js";
13
+ import type { TabBridgeClient } from "../foundation/resident-tabs/bridge-client.js";
13
14
  /**
14
15
  * The session backend composition wires: the owned-UI port, the prompt-suggestion generator, and
15
16
  * the agent settings port. `settingsPort` stays outside the owned-UI contract because its type
@@ -55,11 +56,27 @@ export interface OwnedUiCompositionOptions {
55
56
  readonly checkForNewerRelease?: (options: StartupReleaseCheckOptions) => Promise<AvailableRelease | null>;
56
57
  /** Exact native process inspection injected by the shipped entry; absent SDK seams do not publish claims. */
57
58
  readonly inspectProcess?: (pid: number) => Promise<NativeProcessIdentity | null>;
59
+ /** Private credentials were consumed by the entry before composition or extensions loaded. */
60
+ readonly tab?: {
61
+ readonly bridge: TabBridgeClient;
62
+ };
63
+ /** One prompt-adjacent notice explaining why an opted-in resident launch fell back. */
64
+ readonly residentFallbackNotice?: string;
58
65
  }
59
66
  export interface OwnedUiComposition {
60
67
  readonly application: OwnedUiApplicationPort;
61
68
  /** Present when a profile was supplied, so the caller can resolve settings before start. */
62
69
  readonly settings: OwnedSettingsManager | null;
63
70
  }
71
+ export declare function residentStartupPresentation(resident: boolean, fallbackNotice?: string): {
72
+ readonly startup?: {
73
+ readonly quiet?: boolean;
74
+ readonly notices?: readonly [{
75
+ readonly kind: "warning";
76
+ readonly message: string;
77
+ }];
78
+ };
79
+ };
64
80
  export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
65
81
  export declare function composeOwnedUi(options?: OwnedUiCompositionOptions): Promise<OwnedUiComposition>;
82
+ export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session">): Promise<() => void>;
@@ -19,6 +19,11 @@ import { createOwnedRouteHost } from "./settings-route-host.js";
19
19
  import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
20
20
  import { nativeHyperlinkStyle } from "../ui/components/spans.js";
21
21
  const SESSION_RUNTIME_REFRESH_MS = 30_000;
22
+ export function residentStartupPresentation(resident, fallbackNotice) {
23
+ if (resident)
24
+ return { startup: { quiet: true } };
25
+ return fallbackNotice === undefined ? {} : { startup: { notices: [{ kind: "warning", message: fallbackNotice }] } };
26
+ }
22
27
  export async function composeOwnedUiApplication(options = {}) {
23
28
  return (await composeOwnedUi(options)).application;
24
29
  }
@@ -120,6 +125,8 @@ export async function composeOwnedUi(options = {}) {
120
125
  await host.dispose();
121
126
  throw error;
122
127
  }
128
+ const tabBridge = options.tab?.bridge ?? null;
129
+ const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend);
123
130
  let releaseNotes = null;
124
131
  let releaseNotesFailure;
125
132
  if (ownedSurfaces) {
@@ -228,13 +235,15 @@ export async function composeOwnedUi(options = {}) {
228
235
  },
229
236
  }),
230
237
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
238
+ ...(tabBridge === null ? {} : { residentTab: { requestDetach: () => tabBridge.requestDetach() } }),
231
239
  },
232
240
  presentation: {
233
241
  ...(options.terminal === undefined ? {} : { terminal: createPiTerminalBridge(options.terminal) }),
242
+ ...residentStartupPresentation(tabBridge !== null, options.residentFallbackNotice),
234
243
  ...(clipboardDiagnostics === null ? {} : { input: { onEvent: event => clipboardDiagnostics.runtime(event) } }),
235
244
  ...(viewportSettings === null ? {} : { viewportSettings }),
236
245
  ...(backgroundSettings === null ? {} : { backgroundSettings }),
237
- ...(settings === null || !ownedSurfaces ? {} : {
246
+ ...(settings === null || !ownedSurfaces || tabBridge !== null ? {} : {
238
247
  quitOutro: { snapshot: () => quitOutroSettingsSnapshot(settings), interactive: process.stdout.isTTY === true },
239
248
  }),
240
249
  },
@@ -323,6 +332,8 @@ export async function composeOwnedUi(options = {}) {
323
332
  await releaseNoteClaim?.release();
324
333
  suggestionDiagnostics?.dispose();
325
334
  clipboardDiagnostics?.dispose();
335
+ unsubscribeTabBridge();
336
+ tabBridge?.close();
326
337
  // Invariant: the host outlives the application; disposing it cancels host-level probes last.
327
338
  await host.dispose();
328
339
  }
@@ -330,6 +341,22 @@ export async function composeOwnedUi(options = {}) {
330
341
  };
331
342
  return { application, settings };
332
343
  }
344
+ export async function bindResidentTabBridge(tabBridge, backend) {
345
+ await tabBridge.start().catch(() => false);
346
+ tabBridge.status("starting");
347
+ const announceSession = () => {
348
+ const resume = backend.identity.currentSessionResumeMetadata();
349
+ tabBridge.session(resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`, backend.session.view().status.footer?.sessionName ?? null);
350
+ };
351
+ announceSession();
352
+ return backend.session.onEvent(event => {
353
+ if (event.type === "session-lifecycle") {
354
+ tabBridge.status(event.lifecycle === "busy" ? "working" : event.lifecycle === "starting" ? "starting" : "idle");
355
+ }
356
+ if (event.type === "session-view")
357
+ announceSession();
358
+ });
359
+ }
333
360
  async function readPackageVersion() {
334
361
  const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
335
362
  const version = manifest?.version;
@@ -7,6 +7,9 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
10
+ residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE";
11
+ residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS";
12
+ residentFallback: "LAUNCH_CONTEXT_RESIDENT_FALLBACK";
10
13
  }>;
11
14
  /**
12
15
  * Keys published by releases that predate the neutral cutover. They are read when
@@ -31,6 +34,9 @@ export interface LaunchContext {
31
34
  readonly releaseLayers?: string;
32
35
  readonly launchProfile?: "a1" | "pi";
33
36
  readonly immutableWarmup?: "1";
37
+ readonly residentRootExecutable?: string;
38
+ readonly residentRootArguments?: string;
39
+ readonly residentFallback?: string;
34
40
  }
35
41
  export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
36
42
  /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
@@ -7,6 +7,9 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
9
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
10
+ residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE",
11
+ residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS",
12
+ residentFallback: "LAUNCH_CONTEXT_RESIDENT_FALLBACK",
10
13
  });
11
14
  /**
12
15
  * Keys published by releases that predate the neutral cutover. They are read when
@@ -63,6 +66,18 @@ export function readLaunchContext(environment, requirement = "optional", platfor
63
66
  }
64
67
  if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
65
68
  throw invalidContext("immutableWarmup", "invalid permission flag");
69
+ if ((values.residentRootExecutable === undefined) !== (values.residentRootArguments === undefined))
70
+ throw invalidContext("residentRootExecutable", "resident root command is incomplete");
71
+ if (values.residentRootArguments !== undefined) {
72
+ try {
73
+ const argumentsValue = JSON.parse(values.residentRootArguments);
74
+ if (!Array.isArray(argumentsValue) || argumentsValue.some(value => typeof value !== "string"))
75
+ throw new Error("not string arguments");
76
+ }
77
+ catch {
78
+ throw invalidContext("residentRootArguments", "invalid JSON argument list");
79
+ }
80
+ }
66
81
  if (values.releaseDigest !== undefined && !/^[a-f0-9]{64}$/.test(values.releaseDigest))
67
82
  throw invalidContext("releaseDigest", "invalid digest");
68
83
  if (requirement === "profile")
@@ -16,11 +16,16 @@ interface GuardianControl {
16
16
  off(event: "disconnect", listener: () => void): unknown;
17
17
  close(): void;
18
18
  }
19
+ export interface LaunchGuardianRootCommand {
20
+ readonly executable: string;
21
+ readonly arguments: readonly string[];
22
+ }
19
23
  export interface LaunchGuardianOptions {
20
24
  readonly profileId: LaunchProfileId;
21
25
  readonly releaseRoot: string;
22
26
  readonly uiEntry: string;
23
27
  readonly sessionSelection?: SessionSelection;
28
+ readonly rootCommand?: LaunchGuardianRootCommand;
24
29
  readonly environment?: NodeJS.ProcessEnv;
25
30
  readonly cwd?: string;
26
31
  readonly helperPath?: string;
@@ -44,7 +44,8 @@ export async function runLaunchGuardian(options) {
44
44
  });
45
45
  let handle;
46
46
  try {
47
- handle = await containment.spawn(process.execPath, [options.uiEntry, ...sessionArgs], {
47
+ const rootCommand = options.rootCommand ?? { executable: process.execPath, arguments: [options.uiEntry, ...sessionArgs] };
48
+ handle = await containment.spawn(rootCommand.executable, [...rootCommand.arguments], {
48
49
  cwd: options.cwd ?? process.cwd(),
49
50
  environment: environmentEntries(environment),
50
51
  ...(environment.TERM ? { terminalType: environment.TERM } : {}),
@@ -2,6 +2,7 @@ import { type SupervisorEndpointMetadata } from "./cohort-state.js";
2
2
  import { resolveProductPaths, type SessionSelection, type LaunchProfileId, type SupervisorStartupAttemptIdentity } from "../lifecycle/index.js";
3
3
  import { cleanupProvenIdleOwner } from "./process-cleanup.js";
4
4
  import { type MaterializedRelease, type VerifyMaterializedReleaseOptions } from "./release-store.js";
5
+ import { type EnsureTerminalHostOptions, type EnsureTerminalHostResult } from "../resident-tabs/index.js";
5
6
  export interface BootstrapOptions {
6
7
  readonly packageRoot: string;
7
8
  readonly launchIntent?: {
@@ -23,6 +24,9 @@ export interface BootstrapOptions {
23
24
  readonly releaseIsLaunchable?: (releaseRoot: string) => boolean;
24
25
  /** Test seam for bounded maintenance scheduled after stale endpoint sweeping. */
25
26
  readonly scheduleMaintenance?: (dataDir: string, paths: ReturnType<typeof resolveProductPaths>) => Promise<void>;
27
+ /** Test seams for resident launch routing before the guardian starts. */
28
+ readonly readResidentTabs?: (configDir: string, profileId: string) => Promise<boolean>;
29
+ readonly ensureTerminalHost?: (options: EnsureTerminalHostOptions) => Promise<EnsureTerminalHostResult>;
26
30
  }
27
31
  export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
28
32
  export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;