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

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 (115) 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 +0 -1
  7. package/dist/app/session-shell/session-shell-root.js +1 -1
  8. package/dist/app/session-shell/session-shell.js +3 -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-hotkey-sections.js +1 -1
  63. package/dist/integrations/pi/components/shell-presenters-info.d.ts +4 -2
  64. package/dist/integrations/pi/components/shell-presenters-info.js +38 -4
  65. package/dist/integrations/pi/engine/adapter.d.ts +1 -0
  66. package/dist/integrations/pi/engine/adapter.js +3 -0
  67. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +1 -0
  68. package/dist/integrations/pi/engine/prompt-suggestions.js +40 -0
  69. package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
  70. package/dist/native/darwin-arm64/manifest.json +1 -1
  71. package/dist/native/darwin-arm64/terminal-host +0 -0
  72. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +10500 -0
  73. package/dist/native/darwin-arm64/terminal-host.manifest.json +44 -0
  74. package/dist/native/darwin-x64/a1_session_lock.node +0 -0
  75. package/dist/native/darwin-x64/manifest.json +20 -0
  76. package/dist/native/darwin-x64/process-guardian +0 -0
  77. package/dist/native/darwin-x64/terminal-host +0 -0
  78. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10737 -0
  79. package/dist/native/darwin-x64/terminal-host.manifest.json +44 -0
  80. package/dist/native/linux-x64/a1_session_lock.node +0 -0
  81. package/dist/native/linux-x64/manifest.json +1 -1
  82. package/dist/native/linux-x64/terminal-host +0 -0
  83. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +11226 -0
  84. package/dist/native/linux-x64/terminal-host.manifest.json +44 -0
  85. package/dist/native/win32-x64/a1_session_lock.node +0 -0
  86. package/dist/native/win32-x64/manifest.json +2 -2
  87. package/dist/native/win32-x64/process-guardian.exe +0 -0
  88. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +10757 -0
  89. package/dist/native/win32-x64/terminal-host.exe +0 -0
  90. package/dist/native/win32-x64/terminal-host.manifest.json +44 -0
  91. package/dist/ui/settings/declarations.d.ts +19 -43
  92. package/dist/ui/settings/declarations.js +19 -35
  93. package/dist/ui/settings/index.d.ts +1 -1
  94. package/dist/ui/settings/index.js +1 -1
  95. package/dist/ui/settings/manager.d.ts +1 -0
  96. package/dist/ui/settings/manager.js +9 -1
  97. package/dist/ui/settings/migrations.js +12 -0
  98. package/dist/ui/settings/resolution.d.ts +0 -2
  99. package/dist/ui/settings/resolution.js +1 -13
  100. package/dist/ui/settings/sections.js +0 -4
  101. package/docs/architecture/evidence/resident-tabs/linux-x64.md +37 -0
  102. package/docs/architecture/evidence/resident-tabs/macos-arm64.md +36 -0
  103. package/docs/architecture/evidence/resident-tabs/macos-x64.md +35 -0
  104. package/docs/architecture/evidence/resident-tabs/windows-x64.md +36 -0
  105. package/docs/architecture/internal-naming.md +2 -0
  106. package/docs/architecture/process-guardian-provenance.md +2 -0
  107. package/docs/architecture/resident-tabs-requirements.md +1 -563
  108. package/docs/architecture/resident-tabs.md +166 -94
  109. package/docs/architecture/terminal-host-provenance.md +13 -3
  110. package/docs/architecture/toolchain.md +1 -1
  111. package/docs/ci-release-runbook.md +2 -2
  112. package/docs/features/launch-profiles.md +11 -8
  113. package/docs/manual-resident-tabs-acceptance.md +51 -0
  114. package/docs/validation.md +9 -1
  115. package/package.json +5 -1
@@ -0,0 +1,140 @@
1
+ import { resolveProductPaths } from "../foundation/lifecycle/index.js";
2
+ import { CohortStateStore } from "../foundation/release/index.js";
3
+ import { readResidentTabLaunchSettings, runTerminalHostMaintenance, writeDoctorBundle, } from "../foundation/resident-tabs/index.js";
4
+ import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../product-identity.js";
5
+ /** `a1 tabs`: inspect or stop resident tabs without attaching, and never start a host to report state. */
6
+ export async function runTabsCommand(request, options = {}) {
7
+ const environment = options.environment ?? process.env;
8
+ const stdout = options.stdout ?? (message => process.stdout.write(message));
9
+ const stderr = options.stderr ?? (message => process.stderr.write(message));
10
+ const paths = resolveProductPaths(environment);
11
+ const profileRoot = environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir;
12
+ const releaseRoots = await installedReleaseRoots(paths.dataDir, options.packageRoot);
13
+ const maintenance = options.maintenance ?? runTerminalHostMaintenance;
14
+ const report = await maintenance({
15
+ action: request.action === "doctor" ? "host-status" : request.action,
16
+ ...(request.tabId === undefined ? {} : { tabId: request.tabId }),
17
+ profileRoot,
18
+ dataDir: paths.dataDir,
19
+ runtimeDir: paths.runtimeDir,
20
+ releaseRoots,
21
+ environment,
22
+ });
23
+ if (request.action === "doctor") {
24
+ if (!report.stateDir) {
25
+ stderr(`${PRODUCT_TEXT.diagnostic(`tabs doctor could not locate resident state: ${report.message ?? report.host}`)}\n`);
26
+ return 1;
27
+ }
28
+ const settings = await readResidentTabLaunchSettings(environment.PI_CODING_AGENT_DIR ?? paths.configDir, PRODUCT_IDENTITY.commandName, paths.configDir, "a1");
29
+ const bundle = await (options.doctor ?? writeDoctorBundle)({
30
+ stateDir: report.stateDir,
31
+ hostStatus: report,
32
+ releaseRoots,
33
+ settings: { tabsMax: settings.tabsMax, tabsAutoName: settings.tabsAutoName },
34
+ });
35
+ if (request.json)
36
+ stdout(`${JSON.stringify({ schema: "a1-tabs-doctor-v1", path: bundle.path, bytes: bundle.bytes, dropped: bundle.dropped })}\n`);
37
+ else
38
+ stdout(`Wrote ${bundle.path}\n`);
39
+ return 0;
40
+ }
41
+ if (request.json) {
42
+ const { exitCode: _exitCode, ...printed } = report;
43
+ stdout(`${JSON.stringify(printed)}\n`);
44
+ }
45
+ else {
46
+ const text = describe(request, report);
47
+ if (report.exitCode === 0)
48
+ stdout(text);
49
+ else
50
+ stderr(text);
51
+ }
52
+ return report.exitCode === 0 ? 0 : 1;
53
+ }
54
+ async function installedReleaseRoots(dataDir, packageRoot) {
55
+ const roots = [];
56
+ try {
57
+ const state = await new CohortStateStore(dataDir).read();
58
+ for (const id of [state.references.active, state.references.rollback]) {
59
+ if (id && state.releases[id])
60
+ roots.push(state.releases[id].releaseRoot);
61
+ }
62
+ roots.push(...Object.values(state.releases).map(record => record.releaseRoot));
63
+ }
64
+ catch {
65
+ // Rationale: unreadable release state still leaves the running package's own host.
66
+ }
67
+ if (packageRoot)
68
+ roots.push(packageRoot);
69
+ return [...new Set(roots)];
70
+ }
71
+ const NO_TABS = "No resident tabs are running.\n";
72
+ function describe(request, report) {
73
+ const command = PRODUCT_TEXT.commandName;
74
+ if (report.host === "none")
75
+ return NO_TABS;
76
+ if (report.host === "unavailable")
77
+ return `${PRODUCT_TEXT.diagnostic(`tabs could not run maintenance: ${report.message ?? "terminal host unavailable"}`)}\n`;
78
+ if (report.host === "unresponsive") {
79
+ return `The resident host (pid ${report.pid ?? "unknown"}) is not responding. Run \`${command} tabs host stop\` to stop it.\n`;
80
+ }
81
+ if (report.host === "incompatible") {
82
+ return `${PRODUCT_TEXT.diagnostic(report.message ?? "running tabs use another version")} Run \`${command} tabs host stop\` from that version.\n`;
83
+ }
84
+ if (report.host === "unverifiable" || report.host === "refused") {
85
+ return `${PRODUCT_TEXT.diagnostic(report.message ?? "the resident host could not be verified; nothing was changed")}\n`;
86
+ }
87
+ const result = report.result ?? {};
88
+ if (request.action === "list")
89
+ return listing(result);
90
+ if (request.action === "host-status")
91
+ return status(result);
92
+ if (request.action === "host-stop") {
93
+ if (report.host === "stopped")
94
+ return "Stopped every tab and the resident host.\n";
95
+ return `Could not stop every tab: ${remaining(result)}. The host is still running.\n`;
96
+ }
97
+ if (result.accepted === false && typeof result.message === "string") {
98
+ return `${PRODUCT_TEXT.diagnostic(`tabs stop: ${result.message === "unknown tab" ? `no tab ${request.tabId ?? ""}` : result.message}`)}\n`;
99
+ }
100
+ const stopped = Array.isArray(result.stopped) ? result.stopped.length : 0;
101
+ if (result.accepted === false)
102
+ return `Could not stop: ${remaining(result)}.\n`;
103
+ if (request.action === "stop")
104
+ return `Stopped tab ${request.tabId ?? ""}; its session can be resumed.\n`;
105
+ return stopped === 0 ? NO_TABS : `Stopped ${stopped} tab${stopped === 1 ? "" : "s"}; their sessions can be resumed.\n`;
106
+ }
107
+ function remaining(result) {
108
+ return Array.isArray(result.remaining) && result.remaining.length > 0 ? result.remaining.join(", ") : "unknown tabs";
109
+ }
110
+ function listing(result) {
111
+ const tabs = Array.isArray(result.tabs) ? result.tabs : [];
112
+ if (tabs.length === 0)
113
+ return NO_TABS;
114
+ const rows = [
115
+ ["ID", "NAME", "STATUS", "CWD", "RELEASE", "HOLDER"],
116
+ ...tabs.map(tab => [tab.tabId, tab.name, tab.status, tab.cwd, tab.release, tab.holderPid ?? "-"].map(value => String(value ?? "-"))),
117
+ ];
118
+ const widths = rows[0].map((_, column) => Math.max(...rows.map(row => row[column].length)));
119
+ return `${rows.map(row => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ")).join("\n")}\n`;
120
+ }
121
+ function status(result) {
122
+ const limits = (result.limits ?? {});
123
+ const counters = (result.counters ?? {});
124
+ const byStatus = (result.tabsByStatus ?? {});
125
+ const lines = [
126
+ `pid: ${String(result.pid ?? "-")}`,
127
+ `build: ${String(result.build ?? "-")}`,
128
+ `release: ${String(result.release ?? "-")}`,
129
+ `generation: ${String(result.generation ?? "-")}`,
130
+ `epoch: ${String(result.epoch ?? "-")}`,
131
+ `uptime: ${String(result.uptimeSeconds ?? "-")}s`,
132
+ `detachment: ${String(result.detachmentMode ?? "unknown")}${result.detachmentVerified === false ? " (not verified)" : ""}`,
133
+ `tabs: ${String(result.tabs ?? 0)}${Object.keys(byStatus).length > 0 ? ` (${Object.entries(byStatus).map(([key, value]) => `${key} ${String(value)}`).join(", ")})` : ""}`,
134
+ `clients: ${String(result.clients ?? 0)}`,
135
+ `limits: ${Object.entries(limits).map(([key, value]) => `${key} ${String(value)}`).join(", ")}`,
136
+ `counters: ${Object.entries(counters).map(([key, value]) => `${key} ${String(value)}`).join(", ")}`,
137
+ ...(Array.isArray(result.logs) ? result.logs.map(path => `log: ${String(path)}`) : []),
138
+ ];
139
+ return `${lines.join("\n")}\n`;
140
+ }
@@ -19,6 +19,7 @@ import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
19
19
  */
20
20
  export interface ComposedSessionBackend extends PiSessionPresenterBackend, OwnedUiPromptSuggestionGeneratorPort {
21
21
  settingsPort(): AgentSettingsPort | null;
22
+ generateResidentTabName(instruction: string, maxTokens: number, signal: AbortSignal): Promise<string | null>;
22
23
  }
23
24
  /** The process-level engine host as composition drives it: sessions, the live accent, and disposal. */
24
25
  export interface ComposedEngineHost {
@@ -40,6 +41,8 @@ export interface OwnedUiCompositionOptions {
40
41
  * session settings-free, which is what the pinned comparison paths use.
41
42
  */
42
43
  readonly profileId?: string;
44
+ /** Hide resident-only tab controls for the explicit direct single-agent launch. */
45
+ readonly hideTabSettings?: boolean;
43
46
  /**
44
47
  * Whether A1's product-specific surfaces are reachable. Comparison profiles
45
48
  * use the same composition with those surfaces withheld.
@@ -63,7 +66,7 @@ export interface OwnedUiCompositionOptions {
63
66
  readonly dataDir: string;
64
67
  readonly incarnation: number;
65
68
  };
66
- /** One prompt-adjacent notice explaining why an opted-in resident launch fell back. */
69
+ /** One prompt-adjacent notice explaining why a resident launch fell back. */
67
70
  readonly residentFallbackNotice?: string;
68
71
  }
69
72
  export interface OwnedUiComposition {
@@ -82,4 +85,7 @@ export declare function residentStartupPresentation(resident: boolean, fallbackN
82
85
  };
83
86
  export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
84
87
  export declare function composeOwnedUi(options?: OwnedUiCompositionOptions): Promise<OwnedUiComposition>;
85
- export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session">, recovery?: Pick<ResidentTabRecovery, "settled"> | null): Promise<() => void>;
88
+ export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session" | "workflows" | "generateResidentTabName">, recovery?: Pick<ResidentTabRecovery, "settled"> | null, options?: {
89
+ readonly autoName?: boolean;
90
+ readonly onVisibility?: (visible: boolean) => void;
91
+ }): Promise<() => void>;
@@ -19,6 +19,7 @@ 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
  import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
22
+ import { deriveResidentTabStatus } from "../features/resident-tab/status-deriver.js";
22
23
  const SESSION_RUNTIME_REFRESH_MS = 30_000;
23
24
  export function residentStartupPresentation(resident, fallbackNotice) {
24
25
  if (resident)
@@ -99,9 +100,22 @@ export async function composeOwnedUi(options = {}) {
99
100
  namespace: PRODUCT_IDENTITY.commandName,
100
101
  legacyConfigDir: productPaths.configDir,
101
102
  agentProvider: () => backend.settingsPort(),
103
+ ...(options.hideTabSettings ? { hiddenOwnedSettingIds: ["tabsMax", "tabsAutoName"] } : {}),
102
104
  hiddenAgentSettingIds: ["fullscreenWheelScrollLines"],
103
105
  agentSettingLabelOverrides: { fullscreenCopyOnSelect: "Copy on select" },
104
106
  });
107
+ const tabBridge = options.tab?.bridge ?? null;
108
+ if (tabBridge !== null)
109
+ await tabBridge.start().catch(() => false);
110
+ const projectTrustPrompt = options.projectTrustPrompt === undefined ? undefined : async (request) => {
111
+ tabBridge?.status("needs-input", false, 1, 0);
112
+ try {
113
+ return await options.projectTrustPrompt(request);
114
+ }
115
+ finally {
116
+ tabBridge?.status("starting");
117
+ }
118
+ };
105
119
  // Invariant: process-wide engine state has one owner. The host installs the HTTP dispatcher, applies
106
120
  // the theme singletons, and announces startup notices once; every session is created through it.
107
121
  const host = await (options.createEngineHost ?? (async (hostOptions) => createPiEngineHost(hostOptions)))({
@@ -114,22 +128,30 @@ export async function composeOwnedUi(options = {}) {
114
128
  packageBorderProjection: ownedSurfaces,
115
129
  apply: { base: applyConfiguredPiTheme, accentColor: setPiAccentColor, packageBorderProjection: setPiPackageBorderProjectionEnabled },
116
130
  },
117
- ...(options.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: options.projectTrustPrompt }),
131
+ ...(projectTrustPrompt === undefined ? {} : { projectTrustPrompt }),
118
132
  });
133
+ const sessionForkPrompt = options.sessionForkPrompt === undefined ? undefined : async (request) => {
134
+ tabBridge?.status("needs-input", false, 1, 0);
135
+ try {
136
+ return await options.sessionForkPrompt(request);
137
+ }
138
+ finally {
139
+ tabBridge?.status("starting");
140
+ }
141
+ };
119
142
  try {
120
143
  backend = await host.create({
121
144
  cwd,
122
145
  ...(ownedSurfaces ? { repositoryContextReader: repositoryContextReaderFor(undefined) } : {}),
123
146
  ...(options.sessionPath === undefined ? {} : { sessionPath: options.sessionPath }),
124
147
  ...(options.sessionSelection === undefined ? {} : { sessionSelection: options.sessionSelection }),
125
- ...(options.sessionForkPrompt === undefined ? {} : { sessionForkPrompt: options.sessionForkPrompt }),
148
+ ...(sessionForkPrompt === undefined ? {} : { sessionForkPrompt }),
126
149
  });
127
150
  }
128
151
  catch (error) {
129
152
  await host.dispose();
130
153
  throw error;
131
154
  }
132
- const tabBridge = options.tab?.bridge ?? null;
133
155
  let recoveryDegraded = false;
134
156
  let presentRecoveryDegraded = (_degraded) => { };
135
157
  const tabRecovery = options.tab === undefined ? null : new ResidentTabRecovery(options.tab.dataDir, {
@@ -140,7 +162,15 @@ export async function composeOwnedUi(options = {}) {
140
162
  },
141
163
  });
142
164
  const recoveredTabInput = await tabRecovery?.initialize(currentBackendSessionFile(backend), backend.identity.sessionId);
143
- const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery);
165
+ if (tabBridge !== null && settings !== null && ownedSurfaces)
166
+ tabBridge.accent(settings.value("accentColor"));
167
+ let residentTabVisible = true;
168
+ let applyResidentTabVisibility = () => { };
169
+ let removeControllerBoundaryInput = () => { };
170
+ const unsubscribeTabBridge = tabBridge === null ? () => { } : await bindResidentTabBridge(tabBridge, backend, tabRecovery, {
171
+ autoName: settings?.value("tabsAutoName") ?? true,
172
+ onVisibility: visible => { residentTabVisible = visible; applyResidentTabVisibility(); },
173
+ });
144
174
  let releaseNotes = null;
145
175
  let releaseNotesFailure;
146
176
  if (ownedSurfaces) {
@@ -163,7 +193,11 @@ export async function composeOwnedUi(options = {}) {
163
193
  }));
164
194
  const unsubscribeAccent = settings === null || !ownedSurfaces
165
195
  ? () => { }
166
- : settings.onChange(current => host.setAccentColor(current.value("accentColor")));
196
+ : settings.onChange(current => {
197
+ const accent = current.value("accentColor");
198
+ host.setAccentColor(accent);
199
+ tabBridge?.accent(accent);
200
+ });
167
201
  // Rationale: Routes open only after shell construction.
168
202
  const references = {
169
203
  changelog: async (input) => {
@@ -251,9 +285,8 @@ export async function composeOwnedUi(options = {}) {
251
285
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
252
286
  ...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
253
287
  requestDetach: () => tabBridge.requestDetach(),
254
- requestRestart: () => tabBridge.requestRestart(),
255
288
  reportStatus: status => tabBridge.status(status),
256
- stallNoticeSeconds: settings?.value("tabsStallNoticeSeconds") ?? 300,
289
+ stallNoticeSeconds: 300,
257
290
  admit: input => tabRecovery.admit({ ...input, incarnation: options.tab.incarnation }),
258
291
  dispatched: () => tabRecovery.dispatched(),
259
292
  correlated: (submissionId, entryId) => tabRecovery.correlated(submissionId, entryId),
@@ -288,6 +321,12 @@ export async function composeOwnedUi(options = {}) {
288
321
  // Invariant: one terminal host owns the process terminal; the session presenter reaches it only through
289
322
  // the host and paints once attached.
290
323
  terminalHost = createdTerminalHost = new OwnedUiTerminalHost(sessionTerminalHostOptions(presenterOptions));
324
+ if (tabBridge !== null) {
325
+ applyResidentTabVisibility = () => terminalHost.setPresentationVisible(residentTabVisible);
326
+ removeControllerBoundaryInput = terminalHost.runtime.addPreInputListener(data => ({
327
+ data: tabBridge.consumeControllerBoundaryInput(data),
328
+ }));
329
+ }
291
330
  presenter = new OwnedUiSessionPresenter(terminalHost, presenterOptions);
292
331
  presentRecoveryDegraded = degraded => {
293
332
  if (!degraded)
@@ -336,6 +375,7 @@ export async function composeOwnedUi(options = {}) {
336
375
  exitNotice = import("../app/session-shell/exit-notice.js")
337
376
  .then(module => module.armExitNotice(backend, exitNoticePath), () => null);
338
377
  presenter.start();
378
+ applyResidentTabVisibility();
339
379
  void announceNewerRelease().catch(() => undefined);
340
380
  },
341
381
  flush: () => backend.session.flushEvents(),
@@ -350,6 +390,7 @@ export async function composeOwnedUi(options = {}) {
350
390
  finally {
351
391
  // Invariant: the shared spares outlive every shell and stop only after the last one is disposed.
352
392
  clipboardServices?.dispose();
393
+ removeControllerBoundaryInput();
353
394
  unsubscribeAccent();
354
395
  sessionRuntimeDisposed = true;
355
396
  if (sessionRuntimeRefreshTimer !== null)
@@ -374,27 +415,83 @@ export async function composeOwnedUi(options = {}) {
374
415
  };
375
416
  return { application, settings };
376
417
  }
377
- export async function bindResidentTabBridge(tabBridge, backend, recovery = null) {
418
+ export async function bindResidentTabBridge(tabBridge, backend, recovery = null, options = {}) {
378
419
  await tabBridge.start().catch(() => false);
379
420
  tabBridge.status("starting");
380
421
  const currentSessionFile = () => currentBackendSessionFile(backend);
422
+ let pendingAutoName = null;
381
423
  const announceSession = () => {
382
- tabBridge.session(currentSessionFile(), backend.session.view().status.footer?.sessionName ?? null);
424
+ const name = backend.session.view().status.footer?.sessionName ?? null;
425
+ const nameSource = name === pendingAutoName ? "auto" : name === null || /^agent(?: \d+)?$/i.test(name) ? "default" : "user";
426
+ tabBridge.session(currentSessionFile(), name, nameSource);
383
427
  };
384
428
  let working = false;
429
+ let lastTurnFailed = false;
430
+ let autoNameAttempted = false;
431
+ const reportStatus = (lifecycle) => {
432
+ const view = backend.session.view();
433
+ const pendingRequests = view.dialog == null ? 0 : 1;
434
+ const queuedInput = view.editor?.queuedSubmissions.length ?? 0;
435
+ tabBridge.status(deriveResidentTabStatus({
436
+ lifecycle: lifecycle ?? view.lifecycle,
437
+ pendingRequests,
438
+ queuedInput,
439
+ ...(lastTurnFailed ? { stopReason: "error" } : {}),
440
+ }), false, pendingRequests, queuedInput);
441
+ };
385
442
  announceSession();
386
- return backend.session.onEvent(event => {
443
+ const unsubscribeCommands = typeof tabBridge.onCommand === "function" ? tabBridge.onCommand(command => {
444
+ if (command.type === "visibility") {
445
+ options.onVisibility?.(command.visible);
446
+ }
447
+ else if (command.type === "rename") {
448
+ void backend.workflows.executeWorkflow({ command: "name", argument: command.name }).catch(() => { });
449
+ }
450
+ else if (command.type === "stop") {
451
+ void backend.session.dispose().catch(() => { });
452
+ }
453
+ }) : () => { };
454
+ const unsubscribeEvents = backend.session.onEvent(event => {
387
455
  if (event.type === "session-lifecycle") {
388
456
  const wasWorking = working;
389
457
  working = event.lifecycle === "busy";
390
- tabBridge.status(working ? "working" : event.lifecycle === "starting" ? "starting" : "idle");
458
+ if (working)
459
+ lastTurnFailed = false;
460
+ reportStatus(event.lifecycle);
391
461
  if (wasWorking && !working && recovery !== null) {
392
462
  void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
393
463
  }
464
+ if (wasWorking && !working && options.autoName === true && !autoNameAttempted) {
465
+ autoNameAttempted = true;
466
+ const view = backend.session.view();
467
+ const currentName = view.status.footer?.sessionName;
468
+ const prompt = view.transcript.find(block => block.kind === "user")?.text.trim();
469
+ if (prompt && (currentName == null || /^agent(?: \d+)?$/i.test(currentName))) {
470
+ void import("../features/resident-tab/index.js").then(({ generateAutoName }) => generateAutoName(prompt, request => backend.generateResidentTabName(request.prompt, request.maxTokens, request.signal))).then(async (name) => {
471
+ pendingAutoName = name;
472
+ const result = await backend.workflows.executeWorkflow({ command: "name", argument: name });
473
+ if (result.outcome === "completed")
474
+ tabBridge.session(currentSessionFile(), name, "auto");
475
+ else if (pendingAutoName === name)
476
+ pendingAutoName = null;
477
+ }).catch(() => { pendingAutoName = null; });
478
+ }
479
+ }
480
+ }
481
+ if (event.type === "agent-run-settled") {
482
+ lastTurnFailed = !event.successful && !event.aborted;
483
+ tabBridge.prompt(event.aborted);
484
+ reportStatus();
394
485
  }
395
- if (event.type === "session-view")
486
+ if (event.type === "session-view") {
396
487
  announceSession();
488
+ reportStatus();
489
+ }
397
490
  });
491
+ return () => {
492
+ unsubscribeCommands();
493
+ unsubscribeEvents();
494
+ };
398
495
  }
399
496
  function currentBackendSessionFile(backend) {
400
497
  const direct = backend.identity.currentSessionFile?.();
@@ -3,7 +3,7 @@ export type { DevelopmentLaunchEnvironment } from "./development-launch.js";
3
3
  export { PI_PROFILE_RESOURCE_DIRECTORIES, initializeProductProfile } from "./initialize-profile.js";
4
4
  export type { InitializedProfile } from "./initialize-profile.js";
5
5
  export { interactiveLaunchIntent, parseSessionSelection } from "./intent.js";
6
- export type { InteractiveLaunchIntent, LaunchIntent, LaunchProfileId, SessionSelection } from "./intent.js";
6
+ export type { InteractiveLaunchIntent, InteractiveLaunchMode, LaunchIntent, LaunchProfileId, SessionSelection } from "./intent.js";
7
7
  export { configurationRootForProfile, resolveLaunchProfilePaths, resolvePromptHistoryDataDir } from "./profile-paths.js";
8
8
  export type { LaunchProfilePathOptions, LaunchProfilePaths } from "./profile-paths.js";
9
9
  export { prepareInteractiveLaunch } from "./prepare-launch.js";
@@ -3,10 +3,12 @@ import type { LaunchProfileId } from "../../foundation/lifecycle/index.js";
3
3
  export { parseSessionSelection } from "../../foundation/lifecycle/index.js";
4
4
  export type { SessionSelection } from "../../foundation/lifecycle/index.js";
5
5
  export type { LaunchProfileId } from "../../foundation/lifecycle/index.js";
6
+ export type InteractiveLaunchMode = "tabs" | "solo";
6
7
  export interface InteractiveLaunchIntent {
7
8
  readonly kind: "interactive";
8
9
  readonly profileId: LaunchProfileId;
10
+ readonly launchMode: InteractiveLaunchMode;
9
11
  readonly sessionSelection?: SessionSelection;
10
12
  }
11
13
  export type LaunchIntent = InteractiveLaunchIntent;
12
- export declare function interactiveLaunchIntent(profileId: LaunchProfileId, sessionSelection?: SessionSelection): InteractiveLaunchIntent;
14
+ export declare function interactiveLaunchIntent(profileId: LaunchProfileId, sessionSelection?: SessionSelection, launchMode?: InteractiveLaunchMode): InteractiveLaunchIntent;
@@ -1,8 +1,11 @@
1
1
  import { sessionSelectionArguments } from "../../foundation/lifecycle/index.js";
2
2
  export { parseSessionSelection } from "../../foundation/lifecycle/index.js";
3
- export function interactiveLaunchIntent(profileId, sessionSelection) {
4
- if (sessionSelection !== undefined && profileId !== "a1")
5
- throw new Error("session selection requires the normal A1 profile");
3
+ export function interactiveLaunchIntent(profileId, sessionSelection, launchMode = profileId === "a1" ? "tabs" : "solo") {
4
+ if (sessionSelection !== undefined && (profileId !== "a1" || launchMode === "solo")) {
5
+ throw new Error("session selection requires the normal tabbed A1 launch");
6
+ }
7
+ if (profileId === "pi" && launchMode !== "solo")
8
+ throw new Error("the Pi comparison launch is always solo");
6
9
  sessionSelectionArguments(sessionSelection);
7
- return Object.freeze({ kind: "interactive", profileId, ...(sessionSelection === undefined ? {} : { sessionSelection }) });
10
+ return Object.freeze({ kind: "interactive", profileId, launchMode, ...(sessionSelection === undefined ? {} : { sessionSelection }) });
8
11
  }
@@ -0,0 +1,11 @@
1
+ export declare const AUTO_NAME_MAX_LENGTH = 16;
2
+ export declare const AUTO_NAME_TIMEOUT_MS = 10000;
3
+ export declare const AUTO_NAME_MAX_TOKENS = 64;
4
+ export declare function normalizeAutoName(value: string): string | null;
5
+ export declare function fallbackAutoName(prompt: string): string;
6
+ export interface AutoNameCompletion {
7
+ readonly prompt: string;
8
+ readonly maxTokens: number;
9
+ readonly signal: AbortSignal;
10
+ }
11
+ export declare function generateAutoName(firstPrompt: string, complete: (request: AutoNameCompletion) => Promise<string | null>, timeoutMs?: number): Promise<string>;
@@ -0,0 +1,42 @@
1
+ export const AUTO_NAME_MAX_LENGTH = 16;
2
+ export const AUTO_NAME_TIMEOUT_MS = 10_000;
3
+ export const AUTO_NAME_MAX_TOKENS = 64;
4
+ export function normalizeAutoName(value) {
5
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
6
+ if (!normalized)
7
+ return null;
8
+ if (normalized.length <= AUTO_NAME_MAX_LENGTH)
9
+ return normalized;
10
+ const clipped = normalized.slice(0, AUTO_NAME_MAX_LENGTH + 1);
11
+ const boundary = clipped.lastIndexOf("-");
12
+ const result = (boundary >= 4 ? clipped.slice(0, boundary) : normalized.slice(0, AUTO_NAME_MAX_LENGTH)).replace(/-+$/g, "");
13
+ return result || null;
14
+ }
15
+ export function fallbackAutoName(prompt) {
16
+ const words = prompt.toLowerCase().match(/[a-z0-9]+/g) ?? [];
17
+ return normalizeAutoName(words.slice(0, 3).join("-")) ?? "new-task";
18
+ }
19
+ export async function generateAutoName(firstPrompt, complete, timeoutMs = AUTO_NAME_TIMEOUT_MS) {
20
+ for (let attempt = 0; attempt < 2; attempt++) {
21
+ const controller = new AbortController();
22
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
23
+ timer.unref?.();
24
+ try {
25
+ const response = await complete({
26
+ prompt: `Name this coding task with a lowercase hyphenated title of at most ${AUTO_NAME_MAX_LENGTH} characters. Reply only with the title.\n\n${firstPrompt.slice(0, 500)}`,
27
+ maxTokens: AUTO_NAME_MAX_TOKENS,
28
+ signal: controller.signal,
29
+ });
30
+ const name = response === null ? null : normalizeAutoName(response);
31
+ if (name !== null)
32
+ return name;
33
+ }
34
+ catch {
35
+ // Rationale: naming is best effort; one retry precedes the deterministic fallback.
36
+ }
37
+ finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
41
+ return fallbackAutoName(firstPrompt);
42
+ }
@@ -0,0 +1,3 @@
1
+ export { AUTO_NAME_MAX_LENGTH, AUTO_NAME_MAX_TOKENS, AUTO_NAME_TIMEOUT_MS, fallbackAutoName, generateAutoName, normalizeAutoName, type AutoNameCompletion, } from "./auto-name.js";
2
+ export { RESIDENT_TAB_SHORTCUTS, residentTabShortcutRegistry, type ResidentTabAction, type ResidentTabShortcut, } from "./shortcuts.js";
3
+ export { deriveResidentTabStatus, type ResidentTabDerivedStatus, type ResidentTabFacts, } from "./status-deriver.js";
@@ -0,0 +1,3 @@
1
+ export { AUTO_NAME_MAX_LENGTH, AUTO_NAME_MAX_TOKENS, AUTO_NAME_TIMEOUT_MS, fallbackAutoName, generateAutoName, normalizeAutoName, } from "./auto-name.js";
2
+ export { RESIDENT_TAB_SHORTCUTS, residentTabShortcutRegistry, } from "./shortcuts.js";
3
+ export { deriveResidentTabStatus, } from "./status-deriver.js";
@@ -0,0 +1,10 @@
1
+ import { ShortcutRegistry } from "../../ui/components/index.js";
2
+ export type ResidentTabAction = "tabs.new" | "tabs.close" | "tabs.rename" | "tabs.retry" | "tabs.fresh" | "tabs.next" | "tabs.previous" | "tabs.moveRight" | "tabs.moveLeft" | `tabs.jump${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}`;
3
+ export interface ResidentTabShortcut {
4
+ readonly id: ResidentTabAction;
5
+ readonly defaultKey: string | null;
6
+ readonly scope: "global" | "failed-banner";
7
+ readonly description: string;
8
+ }
9
+ export declare const RESIDENT_TAB_SHORTCUTS: readonly ResidentTabShortcut[];
10
+ export declare function residentTabShortcutRegistry(assignments?: Readonly<Partial<Record<ResidentTabAction, string | null>>>): ShortcutRegistry<ResidentTabAction>;
@@ -0,0 +1,35 @@
1
+ import { GLOBAL_SCOPE, ShortcutRegistry } from "../../ui/components/index.js";
2
+ const jumps = Array.from({ length: 10 }, (_, index) => ({
3
+ id: `tabs.jump${index + 1}`,
4
+ defaultKey: `alt+${(index + 1) % 10}`,
5
+ scope: "global",
6
+ description: `View A1 tab ${index + 1}`,
7
+ }));
8
+ export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
9
+ { id: "tabs.new", defaultKey: "alt+a", scope: "global", description: "New A1 tab" },
10
+ { id: "tabs.close", defaultKey: "alt+w", scope: "global", description: "Close A1 tab" },
11
+ { id: "tabs.rename", defaultKey: "f2", scope: "global", description: "Rename A1 tab" },
12
+ ...jumps,
13
+ { id: "tabs.next", defaultKey: null, scope: "global", description: "View next A1 tab" },
14
+ { id: "tabs.previous", defaultKey: null, scope: "global", description: "View previous A1 tab" },
15
+ { id: "tabs.moveRight", defaultKey: null, scope: "global", description: "Move A1 tab right" },
16
+ { id: "tabs.moveLeft", defaultKey: null, scope: "global", description: "Move A1 tab left" },
17
+ { id: "tabs.retry", defaultKey: "r", scope: "failed-banner", description: "Retry failed A1 tab" },
18
+ { id: "tabs.fresh", defaultKey: "f", scope: "failed-banner", description: "Start failed A1 tab fresh" },
19
+ ]);
20
+ export function residentTabShortcutRegistry(assignments = {}) {
21
+ const registry = new ShortcutRegistry();
22
+ for (const shortcut of RESIDENT_TAB_SHORTCUTS) {
23
+ const key = assignments[shortcut.id] === undefined ? shortcut.defaultKey : assignments[shortcut.id];
24
+ if (!key)
25
+ continue;
26
+ const declaration = {
27
+ key,
28
+ scope: shortcut.scope === "global" ? GLOBAL_SCOPE : shortcut.scope,
29
+ section: "Tabs",
30
+ description: shortcut.description,
31
+ };
32
+ registry.declare(declaration, shortcut.id);
33
+ }
34
+ return registry;
35
+ }
@@ -0,0 +1,9 @@
1
+ export type ResidentTabDerivedStatus = "starting" | "working" | "needs-input" | "done-unseen" | "idle" | "failed";
2
+ export interface ResidentTabFacts {
3
+ readonly lifecycle: string;
4
+ readonly pendingRequests: number;
5
+ readonly queuedInput: number;
6
+ readonly stopReason?: string;
7
+ readonly settledUnseen?: boolean;
8
+ }
9
+ export declare function deriveResidentTabStatus(facts: ResidentTabFacts): ResidentTabDerivedStatus;
@@ -0,0 +1,15 @@
1
+ export function deriveResidentTabStatus(facts) {
2
+ if (facts.lifecycle === "failed")
3
+ return "failed";
4
+ if (facts.pendingRequests > 0)
5
+ return "needs-input";
6
+ if (facts.lifecycle === "busy" || facts.queuedInput > 0)
7
+ return "working";
8
+ if (facts.lifecycle === "starting")
9
+ return "starting";
10
+ if (facts.stopReason === "error")
11
+ return "failed";
12
+ if (facts.settledUnseen === true)
13
+ return "done-unseen";
14
+ return "idle";
15
+ }
@@ -6,6 +6,7 @@ export declare const PRIVATE_ENVIRONMENT: Readonly<{
6
6
  releaseDigest: "LAUNCH_CONTEXT_RELEASE_DIGEST";
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS";
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE";
9
+ launchMode: "LAUNCH_CONTEXT_MODE";
9
10
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP";
10
11
  residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE";
11
12
  residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS";
@@ -33,11 +34,14 @@ export interface LaunchContext {
33
34
  readonly releaseDigest?: string;
34
35
  readonly releaseLayers?: string;
35
36
  readonly launchProfile?: "a1" | "pi";
37
+ readonly launchMode?: "tabs" | "solo";
36
38
  readonly immutableWarmup?: "1";
37
39
  readonly residentRootExecutable?: string;
38
40
  readonly residentRootArguments?: string;
39
41
  readonly residentFallback?: string;
40
42
  }
43
+ /** The Tabs settings configure resident tabs; a direct-route launch, by `a1 solo` or by a fallback, hides them. */
44
+ export declare function hidesTabSettings(context: Pick<LaunchContext, "launchMode" | "residentFallback">): boolean;
41
45
  export type LaunchContextRequirement = "optional" | "profile" | "release" | "warmup";
42
46
  /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
43
47
  export declare function readLaunchContext(environment: NodeJS.ProcessEnv, requirement?: LaunchContextRequirement, platform?: NodeJS.Platform): LaunchContext;
@@ -6,6 +6,7 @@ export const PRIVATE_ENVIRONMENT = Object.freeze({
6
6
  releaseDigest: "LAUNCH_CONTEXT_RELEASE_DIGEST",
7
7
  releaseLayers: "LAUNCH_CONTEXT_RELEASE_LAYERS",
8
8
  launchProfile: "LAUNCH_CONTEXT_PROFILE",
9
+ launchMode: "LAUNCH_CONTEXT_MODE",
9
10
  immutableWarmup: "LAUNCH_CONTEXT_WARMUP",
10
11
  residentRootExecutable: "LAUNCH_CONTEXT_RESIDENT_ROOT_EXECUTABLE",
11
12
  residentRootArguments: "LAUNCH_CONTEXT_RESIDENT_ROOT_ARGUMENTS",
@@ -30,6 +31,10 @@ const SUPERSEDED_ENTRIES = Object.entries(SUPERSEDED_ENVIRONMENT);
30
31
  const PRIVATE_KEYS = new Set([...Object.values(PRIVATE_ENVIRONMENT), ...Object.values(SUPERSEDED_ENVIRONMENT)]);
31
32
  const LOGICAL_KEYS = new Map(PRIVATE_ENTRIES.map(([logical, key]) => [key, logical]));
32
33
  const SUPERSEDED_LOGICAL_KEYS = new Map(SUPERSEDED_ENTRIES.map(([logical, key]) => [key, logical]));
34
+ /** The Tabs settings configure resident tabs; a direct-route launch, by `a1 solo` or by a fallback, hides them. */
35
+ export function hidesTabSettings(context) {
36
+ return context.launchMode === "solo" || context.residentFallback !== undefined;
37
+ }
33
38
  /** Read current keys, fall back to superseded keys per field, with bounded diagnostics. */
34
39
  export function readLaunchContext(environment, requirement = "optional", platform = process.platform) {
35
40
  const values = {};
@@ -64,6 +69,11 @@ export function readLaunchContext(environment, requirement = "optional", platfor
64
69
  if (values.launchProfile !== undefined && values.launchProfile !== "a1" && values.launchProfile !== "pi") {
65
70
  throw invalidContext("launchProfile", "unsupported profile");
66
71
  }
72
+ if (values.launchMode !== undefined && values.launchMode !== "tabs" && values.launchMode !== "solo") {
73
+ throw invalidContext("launchMode", "unsupported launch mode");
74
+ }
75
+ if (values.launchProfile === "pi" && values.launchMode === "tabs")
76
+ throw invalidContext("launchMode", "Pi comparison cannot use tabs");
67
77
  if (values.immutableWarmup !== undefined && values.immutableWarmup !== "1")
68
78
  throw invalidContext("immutableWarmup", "invalid permission flag");
69
79
  if ((values.residentRootExecutable === undefined) !== (values.residentRootArguments === undefined))