@timurproko/a1 0.2.7-dev.767 → 0.2.7-dev.779

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 (122) hide show
  1. package/README.md +30 -0
  2. package/bin/cli.js +4 -0
  3. package/bin/tab-launch-context.d.ts +3 -0
  4. package/bin/tab-launch-context.js +52 -3
  5. package/bin/ui.js +6 -3
  6. package/dist/app/session-shell/session-shell-root.d.ts +2 -1
  7. package/dist/app/session-shell/session-shell-root.js +2 -2
  8. package/dist/app/session-shell/session-shell.js +4 -8
  9. package/dist/app/session-shell/shell-host.d.ts +2 -0
  10. package/dist/app/session-shell/shell-host.js +20 -1
  11. package/dist/cli/dispatch.d.ts +11 -1
  12. package/dist/cli/dispatch.js +79 -5
  13. package/dist/cli/tabs-request.d.ts +7 -0
  14. package/dist/cli/tabs-request.js +1 -0
  15. package/dist/cli/tabs.d.ts +14 -0
  16. package/dist/cli/tabs.js +140 -0
  17. package/dist/composition/owned-ui.d.ts +8 -2
  18. package/dist/composition/owned-ui.js +109 -12
  19. package/dist/features/launch/index.d.ts +1 -1
  20. package/dist/features/launch/intent.d.ts +3 -1
  21. package/dist/features/launch/intent.js +7 -4
  22. package/dist/features/resident-tab/auto-name.d.ts +11 -0
  23. package/dist/features/resident-tab/auto-name.js +42 -0
  24. package/dist/features/resident-tab/index.d.ts +3 -0
  25. package/dist/features/resident-tab/index.js +3 -0
  26. package/dist/features/resident-tab/shortcuts.d.ts +10 -0
  27. package/dist/features/resident-tab/shortcuts.js +35 -0
  28. package/dist/features/resident-tab/status-deriver.d.ts +9 -0
  29. package/dist/features/resident-tab/status-deriver.js +15 -0
  30. package/dist/foundation/launch-context/index.d.ts +4 -0
  31. package/dist/foundation/launch-context/index.js +10 -0
  32. package/dist/foundation/release/bootstrap.d.ts +4 -2
  33. package/dist/foundation/release/bootstrap.js +11 -8
  34. package/dist/foundation/release/cohort-state.d.ts +1 -1
  35. package/dist/foundation/release/cohort-state.js +1 -1
  36. package/dist/foundation/release/release-gc.d.ts +3 -0
  37. package/dist/foundation/release/release-gc.js +44 -8
  38. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +16 -0
  39. package/dist/foundation/resident-tabs/bounded-host-command.js +35 -0
  40. package/dist/foundation/resident-tabs/bridge-client.d.ts +18 -3
  41. package/dist/foundation/resident-tabs/bridge-client.js +87 -7
  42. package/dist/foundation/resident-tabs/doctor.d.ts +29 -0
  43. package/dist/foundation/resident-tabs/doctor.js +166 -0
  44. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +2 -1
  45. package/dist/foundation/resident-tabs/ensure-terminal-host.js +33 -7
  46. package/dist/foundation/resident-tabs/index.d.ts +7 -1
  47. package/dist/foundation/resident-tabs/index.js +4 -1
  48. package/dist/foundation/resident-tabs/maintenance.d.ts +42 -0
  49. package/dist/foundation/resident-tabs/maintenance.js +78 -0
  50. package/dist/foundation/resident-tabs/release-holds.d.ts +27 -0
  51. package/dist/foundation/resident-tabs/release-holds.js +77 -0
  52. package/dist/foundation/resident-tabs/routing.d.ts +4 -3
  53. package/dist/foundation/resident-tabs/routing.js +6 -4
  54. package/dist/foundation/resident-tabs/settings.d.ts +7 -2
  55. package/dist/foundation/resident-tabs/settings.js +17 -16
  56. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +18 -0
  57. package/dist/foundation/resident-tabs/terminal-host-artifact.js +53 -0
  58. package/dist/foundation/resident-tabs/zip-archive.d.ts +9 -0
  59. package/dist/foundation/resident-tabs/zip-archive.js +56 -0
  60. package/dist/foundation/session-writer-lock/index.d.ts +5 -0
  61. package/dist/foundation/session-writer-lock/index.js +13 -9
  62. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  63. package/dist/integrations/pi/components/shell-footer-status.js +2 -2
  64. package/dist/integrations/pi/components/shell-hotkey-sections.js +1 -1
  65. package/dist/integrations/pi/components/shell-presenters-info.d.ts +4 -2
  66. package/dist/integrations/pi/components/shell-presenters-info.js +38 -4
  67. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +2 -1
  68. package/dist/integrations/pi/components/upstream/components/session-footer.js +7 -4
  69. package/dist/integrations/pi/engine/adapter.d.ts +1 -0
  70. package/dist/integrations/pi/engine/adapter.js +3 -0
  71. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  72. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  73. package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
  74. package/dist/native/darwin-arm64/manifest.json +2 -2
  75. package/dist/native/darwin-arm64/process-guardian +0 -0
  76. package/dist/native/darwin-arm64/terminal-host +0 -0
  77. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  78. package/dist/native/darwin-arm64/terminal-host.manifest.json +44 -0
  79. package/dist/native/darwin-x64/a1_session_lock.node +0 -0
  80. package/dist/native/darwin-x64/manifest.json +20 -0
  81. package/dist/native/darwin-x64/process-guardian +0 -0
  82. package/dist/native/darwin-x64/terminal-host +0 -0
  83. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  84. package/dist/native/darwin-x64/terminal-host.manifest.json +44 -0
  85. package/dist/native/linux-x64/a1_session_lock.node +0 -0
  86. package/dist/native/linux-x64/manifest.json +2 -2
  87. package/dist/native/linux-x64/process-guardian +0 -0
  88. package/dist/native/linux-x64/terminal-host +0 -0
  89. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  90. package/dist/native/linux-x64/terminal-host.manifest.json +44 -0
  91. package/dist/native/win32-x64/a1_session_lock.node +0 -0
  92. package/dist/native/win32-x64/manifest.json +2 -2
  93. package/dist/native/win32-x64/process-guardian.exe +0 -0
  94. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  95. package/dist/native/win32-x64/terminal-host.exe +0 -0
  96. package/dist/native/win32-x64/terminal-host.manifest.json +44 -0
  97. package/dist/ui/settings/declarations.d.ts +19 -43
  98. package/dist/ui/settings/declarations.js +19 -35
  99. package/dist/ui/settings/index.d.ts +1 -1
  100. package/dist/ui/settings/index.js +1 -1
  101. package/dist/ui/settings/manager.d.ts +1 -0
  102. package/dist/ui/settings/manager.js +9 -1
  103. package/dist/ui/settings/migrations.js +12 -0
  104. package/dist/ui/settings/resolution.d.ts +0 -2
  105. package/dist/ui/settings/resolution.js +1 -13
  106. package/dist/ui/settings/sections.js +0 -4
  107. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  108. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  109. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  110. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  111. package/docs/architecture/internal-naming.md +2 -0
  112. package/docs/architecture/process-guardian-provenance.md +2 -0
  113. package/docs/architecture/resident-tabs-requirements.md +1 -563
  114. package/docs/architecture/resident-tabs.md +166 -94
  115. package/docs/architecture/terminal-host-provenance.md +13 -3
  116. package/docs/architecture/toolchain.md +1 -1
  117. package/docs/ci-release-runbook.md +2 -2
  118. package/docs/features/launch-profiles.md +11 -8
  119. package/docs/manual-resident-tabs-acceptance.md +51 -0
  120. package/docs/openspec-archive-automation.md +15 -3
  121. package/docs/validation.md +9 -1
  122. package/package.json +5 -1
package/README.md CHANGED
@@ -57,6 +57,12 @@ Launch A1 (profile: `~/.a1/agent`):
57
57
  a1
58
58
  ```
59
59
 
60
+ Launch A1 in single agent mode:
61
+
62
+ ```text
63
+ a1 solo
64
+ ```
65
+
60
66
  Show all commands:
61
67
 
62
68
  ```text
@@ -77,6 +83,30 @@ a1 pi
77
83
 
78
84
  <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
79
85
 
86
+ ## Tabs
87
+
88
+ Tabs keep running after their terminal closes. List them, or stop one; its session stays resumable:
89
+
90
+ ```text
91
+ a1 tabs
92
+ a1 tabs stop <id>
93
+ ```
94
+
95
+ Stop every tab and the resident host, for example before going back to single agent mode with `a1 solo`:
96
+
97
+ ```text
98
+ a1 tabs host stop
99
+ ```
100
+
101
+ Show host health, or write a redacted diagnostic bundle:
102
+
103
+ ```text
104
+ a1 tabs host status
105
+ a1 tabs doctor
106
+ ```
107
+
108
+ <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
109
+
80
110
  ## Update
81
111
 
82
112
  Install the release:
package/bin/cli.js CHANGED
@@ -69,6 +69,10 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
69
69
  },
70
70
  });
71
71
  },
72
+ tabs: async request => {
73
+ const { runTabsCommand } = await import("../dist/cli/tabs.js");
74
+ return await runTabsCommand(request, { packageRoot: fileURLToPath(packageRoot) });
75
+ },
72
76
  }, {
73
77
  stdout: message => process.stdout.write(message),
74
78
  stderr: message => process.stderr.write(message),
@@ -12,6 +12,8 @@ export function assignFreshSessionRuntimeId(
12
12
  createId: () => string,
13
13
  ): string;
14
14
 
15
+ export function waitForStandbyPromotion(input: NodeJS.ReadStream, promotionFile?: string): Promise<void>;
16
+
15
17
  export function consumeTabLaunchContext(
16
18
  arguments_: readonly string[],
17
19
  environment: Record<string, string | undefined>,
@@ -20,4 +22,5 @@ export function consumeTabLaunchContext(
20
22
  readonly tabCredentials: TabLaunchCredentials | undefined;
21
23
  readonly tabDataDir?: string;
22
24
  readonly tabIncarnation?: number;
25
+ readonly tabStandby: boolean;
23
26
  };
@@ -1,8 +1,14 @@
1
+ import { existsSync } from "node:fs";
2
+
1
3
  const TAB_ENVIRONMENT_NAMES = ["A1_TAB_ID", "A1_TAB_TOKEN", "A1_TAB_ENDPOINT"];
2
4
 
3
5
  /** @param {string} reason */
4
6
  export function formatResidentFallbackNotice(reason) {
5
- return `Resident tabs unavailable (${reason}); running a single agent.`;
7
+ // Protocol: a host of another generation is never replaced automatically; only the user stops it.
8
+ if (reason === "incompatible generation") {
9
+ return "A1 normal mode unavailable (running tabs use another version); running in single agent mode. Run `a1 tabs host stop` to restart them on this version.";
10
+ }
11
+ return `A1 normal mode unavailable (${reason}); running in single agent mode.`;
6
12
  }
7
13
 
8
14
  /**
@@ -17,6 +23,46 @@ export function assignFreshSessionRuntimeId(environment, name, createId) {
17
23
  return id;
18
24
  }
19
25
 
26
+ /**
27
+ * Keep the standby below the session/runtime boundary until its authenticated holder promotes it.
28
+ * Platform: Windows ConPTY may drop the in-band marker, so the holder also creates the promotion
29
+ * file inside the tab's private data directory; whichever arrives first promotes the standby.
30
+ * @param {NodeJS.ReadStream} input
31
+ * @param {string} [promotionFile]
32
+ */
33
+ export function waitForStandbyPromotion(input, promotionFile) {
34
+ return /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
35
+ const marker = Buffer.from("\x1b]a1;standby-promote\x07");
36
+ let buffered = Buffer.alloc(0);
37
+ /** @type {NodeJS.Timeout | undefined} */
38
+ let poll;
39
+ const cleanup = () => {
40
+ if (poll !== undefined) clearInterval(poll);
41
+ input.off("data", onData);
42
+ input.off("error", onError);
43
+ input.pause();
44
+ if (input.isTTY) input.setRawMode(false);
45
+ };
46
+ /** @param {Error} error */
47
+ const onError = error => { cleanup(); reject(error); };
48
+ /** @param {Buffer | string} chunk */
49
+ const onData = chunk => {
50
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
51
+ buffered = Buffer.concat([buffered, bytes]).subarray(-marker.length);
52
+ if (buffered.equals(marker)) { cleanup(); resolve(undefined); }
53
+ };
54
+ if (input.isTTY) input.setRawMode(true);
55
+ input.on("data", onData);
56
+ input.on("error", onError);
57
+ input.resume();
58
+ if (promotionFile !== undefined) {
59
+ poll = setInterval(() => {
60
+ if (existsSync(promotionFile)) { cleanup(); resolve(undefined); }
61
+ }, 10);
62
+ }
63
+ }));
64
+ }
65
+
20
66
  /**
21
67
  * Security: consume bridge credentials into caller-private memory and erase their inheritable
22
68
  * environment entries before any extension-capable module loads.
@@ -28,6 +74,9 @@ export function consumeTabLaunchContext(arguments_, environment) {
28
74
  const tabArgument = uiArguments.indexOf("--tab");
29
75
  const tabMode = tabArgument !== -1;
30
76
  if (tabMode) uiArguments.splice(tabArgument, 1);
77
+ const standbyArgument = uiArguments.indexOf("--tab-standby");
78
+ const tabStandby = standbyArgument !== -1;
79
+ if (tabStandby) uiArguments.splice(standbyArgument, 1);
31
80
  const dataArgument = uiArguments.indexOf("--tab-data-dir");
32
81
  const tabDataDir = dataArgument === -1 ? undefined : uiArguments[dataArgument + 1];
33
82
  if (dataArgument !== -1) uiArguments.splice(dataArgument, 2);
@@ -36,10 +85,10 @@ export function consumeTabLaunchContext(arguments_, environment) {
36
85
  if (incarnationArgument !== -1) uiArguments.splice(incarnationArgument, 2);
37
86
  const [tabId, token, endpoint] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
38
87
  for (const name of TAB_ENVIRONMENT_NAMES) delete environment[name];
39
- if (!tabMode) return { uiArguments, tabCredentials: undefined };
88
+ if (!tabMode) return { uiArguments, tabCredentials: undefined, tabStandby: false };
40
89
  if (!tabId || !token || !endpoint || !tabDataDir || tabDataDir.includes("\0")
41
90
  || typeof tabIncarnation !== "number" || !Number.isSafeInteger(tabIncarnation) || tabIncarnation < 1) {
42
91
  throw new Error("A1 tab mode requires bridge credentials, recovery directory, and incarnation");
43
92
  }
44
- return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation };
93
+ return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation, tabStandby };
45
94
  }
package/bin/ui.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
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, tabDataDir, tabIncarnation } = consumeTabLaunchContext(process.argv.slice(2), process.env);
4
+ const { assignFreshSessionRuntimeId, consumeTabLaunchContext, formatResidentFallbackNotice, waitForStandbyPromotion } = await import("./tab-launch-context.js");
5
+ const { uiArguments, tabCredentials, tabDataDir, tabIncarnation, tabStandby } = consumeTabLaunchContext(process.argv.slice(2), process.env);
6
6
 
7
7
  // Invariant: one terminal module identity per process, decided before any A1 or Pi module loads.
8
8
  const { installPinnedPiTuiResolver } = await import("./module-resolver.js");
9
9
  installPinnedPiTuiResolver(new URL("..", import.meta.url).pathname.replace(/^\/(.:)/, "$1").replace(/\/$/, ""));
10
10
  const startup = await import("../dist/foundation/startup/startup-runtime.js");
11
11
  startup.enableEnvironmentCompileCache(process.env);
12
- const { readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
12
+ const { hidesTabSettings, readLaunchContext } = await import("../dist/foundation/launch-context/index.js");
13
13
  const launchContext = readLaunchContext(process.env, "profile");
14
14
  const profile = launchContext.launchProfile;
15
15
  if (profile === undefined) throw new Error("A1 launch profile is missing");
@@ -52,6 +52,8 @@ const [
52
52
  { composeOwnedUi },
53
53
  ] = await modules;
54
54
 
55
+ if (tabStandby) await waitForStandbyPromotion(process.stdin, tabDataDir === undefined ? undefined : join(tabDataDir, "standby-promote"));
56
+
55
57
  // Compatibility: before the composition uses pinned Pi's terminal stack: confirm A1 and Pi
56
58
  // resolve it to the same copy, so extensions and the owned UI share one module identity.
57
59
  identity.assertSinglePiTuiModuleAtLaunch(packageRootPath, (/** @type {string} */ message) => process.stderr.write(message));
@@ -76,6 +78,7 @@ Promise.resolve().then(async () => {
76
78
  cwd: process.cwd(),
77
79
  profileId,
78
80
  ownedSurfaces,
81
+ ...(hidesTabSettings(launchContext) ? { hideTabSettings: true } : {}),
79
82
  ...(processInspector === null ? {} : { inspectProcess: pid => processInspector.observe(pid) }),
80
83
  projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
81
84
  sessionForkPrompt: createConsoleSessionForkPrompt(),
@@ -46,7 +46,6 @@ export interface OwnedUiShellEngineOptions {
46
46
  /** Resident tab quit routes detach the outer client without stopping this engine session. */
47
47
  readonly residentTab?: {
48
48
  requestDetach(): Promise<boolean>;
49
- requestRestart?(): Promise<boolean>;
50
49
  reportStatus?(status: "working" | "idle" | "stalled"): void;
51
50
  readonly stallNoticeSeconds?: number;
52
51
  admit?(input: {
@@ -218,6 +217,8 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
218
217
  readonly keybindings?: PiKeybindingsHost;
219
218
  readonly promptImageLimit?: () => number;
220
219
  readonly skillsPresentation?: () => "collapse" | "expand";
220
+ /** A resident tab chip already shows the session name, so its footer omits the duplicate. */
221
+ readonly footerSessionName?: boolean;
221
222
  }, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: OwnedUiTranscriptRendererPort, sessionLayout?: "pinned" | "custom-viewport", imageAssets?: PiShellImageAssetResolver);
222
223
  setEditorPaddingX(padding: number): void;
223
224
  setAutocompleteMaxVisible(maxVisible: number): void;
@@ -119,7 +119,7 @@ export class OwnedUiSessionShellRoot {
119
119
  frame: progressStatusFrame,
120
120
  }, handlers);
121
121
  this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
122
- this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel);
122
+ this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi", () => this.#footerLevel, handlers.footerSessionName ?? true);
123
123
  this.editor = createPiShellEditor({
124
124
  ...handlers,
125
125
  keybindingProfile: this.#customViewport ? "a1" : "pi",
@@ -916,7 +916,7 @@ export class OwnedUiSessionShellRoot {
916
916
  if (result.command === "hotkeys" && result.outcome === "completed") {
917
917
  this.#lastWorkflowStatusId = undefined;
918
918
  const presentation = this.hotkeysPresentation();
919
- const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile);
919
+ const hotkeys = createPiShellHotkeys(presentation.bindings, presentation.getShortcuts, presentation.profile, presentation.residentTabs);
920
920
  this.#appendAnchoredWorkflowComponent(width => hotkeys.render(width), () => hotkeys.dispose?.());
921
921
  return;
922
922
  }
@@ -188,6 +188,7 @@ export class OwnedUiSessionPresenter {
188
188
  replacementSurfaceActive: !this.root.usesDefaultInputSurface(),
189
189
  }),
190
190
  enableDockInputReuse: inputPresentation?.viewportReuse !== false,
191
+ footerSessionName: this.#residentTabOptions === undefined,
191
192
  persistentHistory: this.#customViewport && promptHistory !== undefined,
192
193
  ...(promptHistory === undefined ? {} : { historyEditor: promptHistory.editor }),
193
194
  onSubmit: text => { void this.submit(text).catch(() => this.#reportSubmissionError()); },
@@ -1276,7 +1277,7 @@ export class OwnedUiSessionPresenter {
1276
1277
  this.#residentStalled = true;
1277
1278
  resident.reportStatus?.("stalled");
1278
1279
  const minutes = Math.max(1, Math.round(seconds / 60));
1279
- this.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · /restart-tab restarts`);
1280
+ this.root.appendWorkflowStatus(`No progress for ${minutes} min — Esc interrupts · use the tab restart action to restart`);
1280
1281
  this.#terminal.requestRender();
1281
1282
  }, seconds * 1_000);
1282
1283
  this.#residentProgressTimer.unref?.();
@@ -1759,7 +1760,8 @@ export class OwnedUiSessionPresenter {
1759
1760
  this.#dialogId = dialog.id;
1760
1761
  }
1761
1762
  hotkeysPresentation() {
1762
- return this.root.hotkeysPresentation();
1763
+ const presentation = this.root.hotkeysPresentation();
1764
+ return this.#residentTabOptions === undefined ? presentation : { ...presentation, residentTabs: true };
1763
1765
  }
1764
1766
  async #slashCommand(text) {
1765
1767
  const body = text.slice(1).trim();
@@ -1768,12 +1770,6 @@ export class OwnedUiSessionPresenter {
1768
1770
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
1769
1771
  if (this.#routeHost?.claims(name))
1770
1772
  return this.#openOwnedRoute(name);
1771
- if (name === "restart-tab" && this.#residentTabOptions?.requestRestart !== undefined) {
1772
- const accepted = await this.#residentTabOptions.requestRestart().catch(() => false);
1773
- this.root.appendWorkflowStatus(accepted ? "Restarting resident tab…" : "Resident tab restart is unavailable.");
1774
- this.#terminal.requestRender();
1775
- return accepted ? { outcome: "completed", diagnostic: null } : rejected("resident tab restart is unavailable");
1776
- }
1777
1773
  if (this.#isWorkflowRoute(name))
1778
1774
  return this.runWorkflow({ command: name, argument });
1779
1775
  if (this.#skillsCollapsed()) {
@@ -98,6 +98,8 @@ export declare class OwnedUiTerminalHost implements OwnedUiTerminalHostContract<
98
98
  active(): OwnedUiHostedPresenter | null;
99
99
  requestRender(from: OwnedUiHostedPresenter, force?: boolean): void;
100
100
  start(): void;
101
+ /** Hidden resident tabs retain engine/status work but stop producing terminal animation frames. */
102
+ setPresentationVisible(visible: boolean): void;
101
103
  /** Closes one presenter. Closing the last one ends the terminal. */
102
104
  close(presenter: OwnedUiHostedPresenter): Promise<void>;
103
105
  dispose(): Promise<void>;
@@ -42,6 +42,8 @@ export class OwnedUiTerminalHost {
42
42
  #focused = false;
43
43
  #pointerReporting = false;
44
44
  #started = false;
45
+ #presentationVisible = true;
46
+ #releaseVisibilityHold;
45
47
  #ending;
46
48
  constructor(options) {
47
49
  this.#customViewport = options.layout === "custom-viewport";
@@ -147,7 +149,7 @@ export class OwnedUiTerminalHost {
147
149
  return this.#active;
148
150
  }
149
151
  requestRender(from, force = false) {
150
- if (from === this.#active)
152
+ if (this.#presentationVisible && from === this.#active)
151
153
  this.runtime.requestRender(force);
152
154
  }
153
155
  start() {
@@ -155,9 +157,26 @@ export class OwnedUiTerminalHost {
155
157
  return;
156
158
  this.#started = true;
157
159
  this.runtime.start();
160
+ if (!this.#presentationVisible)
161
+ this.#releaseVisibilityHold = this.runtime.beginPresentationHold();
158
162
  if (this.#customViewport)
159
163
  this.#setPointerReporting(true);
160
164
  }
165
+ /** Hidden resident tabs retain engine/status work but stop producing terminal animation frames. */
166
+ setPresentationVisible(visible) {
167
+ if (this.#presentationVisible === visible)
168
+ return;
169
+ this.#presentationVisible = visible;
170
+ if (!this.#started || this.#ending !== undefined)
171
+ return;
172
+ if (visible) {
173
+ this.#releaseVisibilityHold?.();
174
+ this.#releaseVisibilityHold = undefined;
175
+ }
176
+ else if (this.#releaseVisibilityHold === undefined) {
177
+ this.#releaseVisibilityHold = this.runtime.beginPresentationHold();
178
+ }
179
+ }
161
180
  /** Closes one presenter. Closing the last one ends the terminal. */
162
181
  close(presenter) {
163
182
  if (this.#ending !== undefined || !this.#presenters.has(presenter))
@@ -1,7 +1,8 @@
1
- import { type InteractiveLaunchIntent, type LaunchProfileId, type SessionSelection } from "../features/launch/index.js";
1
+ import { type InteractiveLaunchIntent, type InteractiveLaunchMode, type LaunchProfileId, type SessionSelection } from "../features/launch/index.js";
2
2
  import type { CliCapabilities } from "./capabilities.js";
3
3
  import type { PackageCommandRequest } from "./packages.js";
4
4
  import type { SessionContextRequest } from "./session-context.js";
5
+ import type { TabsRequest } from "./tabs-request.js";
5
6
  import { type PackageCliVerb, type PackageCommandNamespace, type PackageSyntaxDiagnostic } from "./package-messages.js";
6
7
  export type UpdateChannel = "stable" | "next";
7
8
  export interface CliHandlers {
@@ -10,6 +11,7 @@ export interface CliHandlers {
10
11
  readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
11
12
  readonly packages: (request: PackageCommandRequest) => Promise<number>;
12
13
  readonly sessionContext: (request: SessionContextRequest) => Promise<number>;
14
+ readonly tabs: (request: TabsRequest) => Promise<number>;
13
15
  }
14
16
  export interface CliOutput {
15
17
  readonly stdout: (message: string) => void;
@@ -17,6 +19,8 @@ export interface CliOutput {
17
19
  }
18
20
  export declare function cliUsage(capabilities: CliCapabilities): string;
19
21
  export declare function cliHelp(capabilities: CliCapabilities): string;
22
+ /** Focused help for the resident-tab maintenance forms; printing it contacts no host. */
23
+ export declare function tabsHelp(): string;
20
24
  export declare function dispatchCli(arguments_: readonly string[], handlers: CliHandlers, output: CliOutput, capabilities: CliCapabilities): Promise<number>;
21
25
  export type CliCommand = {
22
26
  readonly kind: "noop";
@@ -33,6 +37,7 @@ export type CliCommand = {
33
37
  } | {
34
38
  readonly kind: "launch";
35
39
  readonly profileId: LaunchProfileId;
40
+ readonly launchMode: InteractiveLaunchMode;
36
41
  readonly sessionSelection?: SessionSelection;
37
42
  } | {
38
43
  readonly kind: "version";
@@ -46,6 +51,11 @@ export type CliCommand = {
46
51
  } | {
47
52
  readonly kind: "session-context";
48
53
  readonly request: SessionContextRequest;
54
+ } | {
55
+ readonly kind: "tabs";
56
+ readonly request: TabsRequest;
57
+ } | {
58
+ readonly kind: "tabs-help";
49
59
  } | {
50
60
  readonly kind: "error";
51
61
  readonly message: string;
@@ -6,6 +6,7 @@ export function cliUsage(capabilities) {
6
6
  "",
7
7
  "--session <path|id>",
8
8
  "--session-dir <dir> --session <path|id>",
9
+ "solo",
9
10
  ...(capabilities.developmentComparison ? ["pi"] : []),
10
11
  "help",
11
12
  "version",
@@ -21,6 +22,10 @@ export function cliUsage(capabilities) {
21
22
  "session worktrees",
22
23
  "session link-worktree <path>",
23
24
  "session unlink-worktree",
25
+ "tabs",
26
+ "tabs stop <id>|--all",
27
+ "tabs host status|stop",
28
+ "tabs doctor",
24
29
  ]);
25
30
  }
26
31
  export function cliHelp(capabilities) {
@@ -30,6 +35,7 @@ export function cliHelp(capabilities) {
30
35
  ` ${command}`,
31
36
  ` ${command} --session <path|id>`,
32
37
  ` ${command} --session-dir <dir> --session <path|id>`,
38
+ ` ${command} solo`,
33
39
  ...(capabilities.developmentComparison ? [` ${command} pi`] : []),
34
40
  ` ${command} help`,
35
41
  ` ${command} version`,
@@ -52,9 +58,32 @@ export function cliHelp(capabilities) {
52
58
  ` ${command} session link-worktree <path>`,
53
59
  ` ${command} session unlink-worktree`,
54
60
  "",
61
+ "Tabs:",
62
+ ` ${command} tabs`,
63
+ ` ${command} tabs stop <id>|--all`,
64
+ ` ${command} tabs host status|stop`,
65
+ ` ${command} tabs doctor`,
66
+ "",
67
+ ].join("\n");
68
+ }
69
+ /** Focused help for the resident-tab maintenance forms; printing it contacts no host. */
70
+ export function tabsHelp() {
71
+ const command = PRODUCT_TEXT.commandName;
72
+ return [
73
+ "Resident tabs:",
74
+ ` ${command} tabs List running tabs`,
75
+ ` ${command} tabs stop <id> Stop one tab; its session stays resumable`,
76
+ ` ${command} tabs stop --all Stop every tab and keep the host`,
77
+ ` ${command} tabs host status Show host identity, limits, and health counters`,
78
+ ` ${command} tabs host stop Stop every tab and the host`,
79
+ ` ${command} tabs doctor Write a redacted diagnostic bundle`,
80
+ "",
81
+ "Options:",
82
+ " --json Print the versioned machine-readable report",
83
+ "",
55
84
  ].join("\n");
56
85
  }
57
- const PROFILE_WORDS = new Set(["pi"]);
86
+ const PROFILE_WORDS = new Set(["pi", "solo"]);
58
87
  export async function dispatchCli(arguments_, handlers, output, capabilities) {
59
88
  const command = parseCliCommand(arguments_, capabilities);
60
89
  if (command.kind === "noop")
@@ -76,23 +105,29 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
76
105
  return 2;
77
106
  }
78
107
  if (command.kind === "launch")
79
- return await handlers.launch(interactiveLaunchIntent(command.profileId, command.sessionSelection));
108
+ return await handlers.launch(interactiveLaunchIntent(command.profileId, command.sessionSelection, command.launchMode));
80
109
  if (command.kind === "version")
81
110
  return await handlers.version();
82
111
  if (command.kind === "packages")
83
112
  return await handlers.packages(command.request);
84
113
  if (command.kind === "session-context")
85
114
  return await handlers.sessionContext(command.request);
115
+ if (command.kind === "tabs-help") {
116
+ output.stdout(tabsHelp());
117
+ return 0;
118
+ }
119
+ if (command.kind === "tabs")
120
+ return await handlers.tabs(command.request);
86
121
  return await handlers.update(command.channel, command.target);
87
122
  }
88
123
  export function parseCliCommand(arguments_, capabilities) {
89
124
  if (arguments_.length === 0)
90
- return { kind: "launch", profileId: "a1" };
125
+ return { kind: "launch", profileId: "a1", launchMode: "tabs" };
91
126
  const [command, ...rest] = arguments_;
92
127
  if (command === "--session" || command === "--session-dir") {
93
128
  try {
94
129
  const sessionSelection = parseSessionSelection(arguments_);
95
- return { kind: "launch", profileId: "a1", ...(sessionSelection === undefined ? {} : { sessionSelection }) };
130
+ return { kind: "launch", profileId: "a1", launchMode: "tabs", ...(sessionSelection === undefined ? {} : { sessionSelection }) };
96
131
  }
97
132
  catch (error) {
98
133
  return { kind: "error", message: PRODUCT_TEXT.diagnostic(`could not parse session launch: ${error instanceof Error ? error.message : String(error)}`) };
@@ -108,18 +143,57 @@ export function parseCliCommand(arguments_, capabilities) {
108
143
  if (command === "pi") {
109
144
  if (rest.length > 0)
110
145
  return parsePackageCommand(rest, "pi");
111
- return capabilities.developmentComparison ? { kind: "launch", profileId: "pi" } : { kind: "noop" };
146
+ return capabilities.developmentComparison ? { kind: "launch", profileId: "pi", launchMode: "solo" } : { kind: "noop" };
112
147
  }
148
+ if (command === "solo")
149
+ return withoutArguments(rest, { kind: "launch", profileId: "a1", launchMode: "solo" });
113
150
  if (command === "update")
114
151
  return parseUpdate(rest);
115
152
  if (command === "session")
116
153
  return parseSessionContext(rest);
154
+ if (command === "tabs")
155
+ return parseTabs(rest);
117
156
  if (command?.startsWith("update:"))
118
157
  return { kind: "noop" };
119
158
  // Rationale: unsupported and reserved command spaces are deliberately quiet. Help is
120
159
  // explicit, and a typo must never start an interactive or maintenance path.
121
160
  return { kind: "noop" };
122
161
  }
162
+ /** Parses `a1 tabs` before any I/O; a malformed form fails with one line and contacts no host. */
163
+ function parseTabs(rest) {
164
+ if (rest.some(value => value === "--help" || value === "-h"))
165
+ return { kind: "tabs-help" };
166
+ const json = rest.includes("--json");
167
+ const words = rest.filter(value => value !== "--json");
168
+ const fail = (detail) => ({ kind: "error", message: PRODUCT_TEXT.diagnostic(`could not parse tabs: ${detail}`) });
169
+ const [action, value, extra] = words;
170
+ if (action === undefined)
171
+ return { kind: "tabs", request: { action: "list", json } };
172
+ if (action === "stop") {
173
+ if (value === undefined)
174
+ return fail("stop requires a tab id or --all.");
175
+ if (extra !== undefined)
176
+ return fail("stop accepts one tab id or --all.");
177
+ if (value === "--all")
178
+ return { kind: "tabs", request: { action: "stop-all", json } };
179
+ if (value.startsWith("-"))
180
+ return fail(`unknown option ${value}.`);
181
+ return { kind: "tabs", request: { action: "stop", tabId: value, json } };
182
+ }
183
+ if (action === "host") {
184
+ if (extra !== undefined || (value !== "status" && value !== "stop"))
185
+ return fail("host accepts status or stop.");
186
+ return { kind: "tabs", request: { action: value === "status" ? "host-status" : "host-stop", json } };
187
+ }
188
+ if (action === "doctor") {
189
+ if (value !== undefined)
190
+ return fail("doctor does not accept arguments.");
191
+ return { kind: "tabs", request: { action: "doctor", json } };
192
+ }
193
+ if (action.startsWith("-"))
194
+ return fail(`unknown option ${action}.`);
195
+ return fail(`unknown subcommand ${action}; use list, stop, host, or doctor.`);
196
+ }
123
197
  function parseSessionContext(rest) {
124
198
  const [action, value, extra] = rest;
125
199
  if (action === "link-worktree") {
@@ -0,0 +1,7 @@
1
+ export type TabsAction = "list" | "stop" | "stop-all" | "host-status" | "host-stop" | "doctor";
2
+ /** One parsed `a1 tabs` form. Parsing finishes before any host is contacted. */
3
+ export interface TabsRequest {
4
+ readonly action: TabsAction;
5
+ readonly tabId?: string;
6
+ readonly json: boolean;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { runTerminalHostMaintenance, writeDoctorBundle } from "../foundation/resident-tabs/index.js";
2
+ import type { TabsRequest } from "./tabs-request.js";
3
+ export type { TabsRequest } from "./tabs-request.js";
4
+ export interface TabsCommandOptions {
5
+ /** The running package root; its terminal host is a candidate after the active release. */
6
+ readonly packageRoot?: string;
7
+ readonly environment?: NodeJS.ProcessEnv;
8
+ readonly stdout?: (message: string) => void;
9
+ readonly stderr?: (message: string) => void;
10
+ readonly maintenance?: typeof runTerminalHostMaintenance;
11
+ readonly doctor?: typeof writeDoctorBundle;
12
+ }
13
+ /** `a1 tabs`: inspect or stop resident tabs without attaching, and never start a host to report state. */
14
+ export declare function runTabsCommand(request: TabsRequest, options?: TabsCommandOptions): Promise<number>;