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

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 (105) hide show
  1. package/dist/app/session-shell/dialog-coordinator.d.ts +42 -0
  2. package/dist/app/session-shell/dialog-coordinator.js +133 -0
  3. package/dist/app/session-shell/dock-chrome.d.ts +34 -0
  4. package/dist/app/session-shell/dock-chrome.js +109 -0
  5. package/dist/app/session-shell/exit-notice.d.ts +1 -1
  6. package/dist/app/session-shell/exit-notice.js +1 -1
  7. package/dist/app/session-shell/extension-surface.d.ts +30 -0
  8. package/dist/app/session-shell/extension-surface.js +105 -0
  9. package/dist/app/session-shell/input-surface-switch.d.ts +36 -0
  10. package/dist/app/session-shell/input-surface-switch.js +85 -0
  11. package/dist/app/session-shell/login-interaction.d.ts +27 -0
  12. package/dist/app/session-shell/login-interaction.js +91 -0
  13. package/dist/app/session-shell/prompt-submission.d.ts +48 -0
  14. package/dist/app/session-shell/prompt-submission.js +276 -0
  15. package/dist/app/session-shell/resident-tab-coordinator.d.ts +41 -0
  16. package/dist/app/session-shell/resident-tab-coordinator.js +120 -0
  17. package/dist/app/session-shell/session-concern.d.ts +22 -0
  18. package/dist/app/session-shell/session-concern.js +29 -0
  19. package/dist/app/session-shell/session-lifecycle.d.ts +29 -0
  20. package/dist/app/session-shell/session-lifecycle.js +49 -0
  21. package/dist/app/session-shell/session-shell-root.d.ts +1 -1
  22. package/dist/app/session-shell/session-shell-root.js +182 -931
  23. package/dist/app/session-shell/session-shell.d.ts +2 -8
  24. package/dist/app/session-shell/session-shell.js +209 -897
  25. package/dist/app/session-shell/settings-bindings.d.ts +17 -0
  26. package/dist/app/session-shell/settings-bindings.js +91 -0
  27. package/dist/app/session-shell/shell-editor-factory.d.ts +27 -0
  28. package/dist/app/session-shell/shell-editor-factory.js +102 -0
  29. package/dist/app/session-shell/shell-host.d.ts +6 -0
  30. package/dist/app/session-shell/shell-host.js +26 -4
  31. package/dist/app/session-shell/shell-layout.d.ts +64 -0
  32. package/dist/app/session-shell/shell-layout.js +18 -0
  33. package/dist/app/session-shell/suggestion-surface.d.ts +25 -0
  34. package/dist/app/session-shell/suggestion-surface.js +63 -0
  35. package/dist/app/session-shell/transcript-document.d.ts +78 -0
  36. package/dist/app/session-shell/transcript-document.js +365 -0
  37. package/dist/app/session-shell/viewport-layout.d.ts +31 -0
  38. package/dist/app/session-shell/viewport-layout.js +160 -0
  39. package/dist/app/session-shell/workflow-output.d.ts +33 -0
  40. package/dist/app/session-shell/workflow-output.js +174 -0
  41. package/dist/app/session-shell/workflow-router.d.ts +25 -0
  42. package/dist/app/session-shell/workflow-router.js +39 -0
  43. package/dist/composition/owned-ui.d.ts +6 -25
  44. package/dist/composition/owned-ui.js +48 -242
  45. package/dist/composition/resident-tab-bridge.d.ts +16 -0
  46. package/dist/composition/resident-tab-bridge.js +159 -0
  47. package/dist/composition/session-backend.d.ts +19 -0
  48. package/dist/composition/session-backend.js +1 -0
  49. package/dist/composition/session-runtime-registry.d.ts +20 -0
  50. package/dist/composition/session-runtime-registry.js +85 -0
  51. package/dist/composition/settings-route-host.d.ts +6 -1
  52. package/dist/composition/settings-route-host.js +7 -6
  53. package/dist/composition/startup-route.d.ts +32 -0
  54. package/dist/composition/startup-route.js +72 -0
  55. package/dist/features/owned-ui/settings-app.d.ts +4 -1
  56. package/dist/features/owned-ui/settings-app.js +9 -3
  57. package/dist/features/resident-tab/shortcut-declarations.js +1 -1
  58. package/dist/features/resident-tab/status-deriver.d.ts +4 -3
  59. package/dist/features/resident-tab/status-deriver.js +2 -5
  60. package/dist/foundation/release/bootstrap.js +2 -2
  61. package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
  62. package/dist/foundation/resident-tabs/bridge-client.js +2 -1
  63. package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
  64. package/dist/foundation/resident-tabs/fixture-control.js +35 -0
  65. package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
  66. package/dist/foundation/resident-tabs/progress-observation.js +119 -0
  67. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  68. package/dist/foundation/terminal-cleanup/index.js +1 -1
  69. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
  70. package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
  71. package/dist/integrations/pi/engine/adapter.d.ts +4 -77
  72. package/dist/integrations/pi/engine/adapter.js +85 -393
  73. package/dist/integrations/pi/engine/command-dispatch.d.ts +13 -0
  74. package/dist/integrations/pi/engine/command-dispatch.js +57 -0
  75. package/dist/integrations/pi/engine/host.js +2 -2
  76. package/dist/integrations/pi/engine/ports.d.ts +56 -0
  77. package/dist/integrations/pi/engine/ports.js +89 -0
  78. package/dist/integrations/pi/engine/session-runtime.js +5 -2
  79. package/dist/integrations/pi/engine/view-state.d.ts +42 -0
  80. package/dist/integrations/pi/engine/view-state.js +126 -0
  81. package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
  82. package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
  83. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
  84. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
  85. package/dist/native/darwin-arm64/manifest.json +1 -1
  86. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  87. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  88. package/dist/native/darwin-x64/manifest.json +1 -1
  89. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  90. package/dist/native/darwin-x64/terminal-host.node +0 -0
  91. package/dist/native/linux-x64/manifest.json +1 -1
  92. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  93. package/dist/native/linux-x64/terminal-host.node +0 -0
  94. package/dist/native/win32-x64/manifest.json +2 -2
  95. package/dist/native/win32-x64/process-guardian.node +0 -0
  96. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  97. package/dist/native/win32-x64/terminal-host.node +0 -0
  98. package/dist/product-identity.d.ts +2 -2
  99. package/dist/product-identity.js +2 -2
  100. package/dist/product-identity.json +6 -0
  101. package/dist/ui/settings/manager.d.ts +6 -0
  102. package/dist/ui/settings/manager.js +25 -0
  103. package/docs/architecture/boundaries.md +14 -0
  104. package/docs/architecture/resident-tabs.md +23 -18
  105. package/package.json +2 -1
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { unwatchFile, watchFile } from "node:fs";
2
2
  import { SuggestionDiagnosticCapture } from "../features/prompt-suggestions/diagnostics.js";
3
3
  import { PRODUCT_IDENTITY } from "../product-identity.js";
4
4
  import { PromptHistoryService } from "../features/prompt-history/service.js";
@@ -12,8 +12,6 @@ import { createPiSessionPresenters } from "../integrations/pi/session-presenters
12
12
  import { ClipboardDiagnosticCapture } from "../app/session-shell/clipboard-diagnostics.js";
13
13
  import { createOwnedUiClipboardServices } from "../app/session-shell/clipboard-services.js";
14
14
  import { createPiKeybindingsHost } from "../integrations/pi/components/shell-shared-facade.js";
15
- import { projectResidentTabShortcuts } from "../features/resident-tab/shortcut-projection.js";
16
- import { readShortcutBindingsFile } from "../features/resident-tab/shortcut-bindings-file.js";
17
15
  import { OwnedUiSessionPresenter, OwnedUiTerminalHost, sessionTerminalHostOptions, } from "../app/session-shell/session-shell.js";
18
16
  import { OwnedSettingsManager } from "../ui/settings/manager.js";
19
17
  import { createPiTerminalBridge } from "../integrations/pi/tui-runtime/presentation-adapter.js";
@@ -21,8 +19,10 @@ import { createOwnedRouteHost } from "./settings-route-host.js";
21
19
  import { renderPiShellChangelogLines } from "../integrations/pi/components/shell-presenters-info.js";
22
20
  import { nativeHyperlinkStyle } from "../ui/components/spans.js";
23
21
  import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
24
- import { deriveResidentTabStatus } from "../features/resident-tab/status-deriver.js";
25
- const SESSION_RUNTIME_REFRESH_MS = 30_000;
22
+ import { bindResidentTabBridge, correlateResidentTabSubmission, currentBackendSessionFile, residentTabKeybindings } from "./resident-tab-bridge.js";
23
+ import { SessionRuntimeRegistry } from "./session-runtime-registry.js";
24
+ import { readPackageVersion, StartupReleaseNote } from "./startup-route.js";
25
+ export { bindResidentTabBridge, correlateResidentTabSubmission } from "./resident-tab-bridge.js";
26
26
  export function residentStartupPresentation(resident, fallbackNotice) {
27
27
  if (resident)
28
28
  return { startup: { quiet: true } };
@@ -34,62 +34,10 @@ export async function composeOwnedUiApplication(options = {}) {
34
34
  export async function composeOwnedUi(options = {}) {
35
35
  const cwd = options.cwd ?? process.cwd();
36
36
  const ownedSurfaces = options.ownedSurfaces !== "off";
37
- const sessionRuntimeId = ownedSurfaces ? process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId] : undefined;
38
- const inspectSessionProcess = options.inspectProcess;
39
- let sessionRuntimeIdentity = null;
40
- // Invariant: one entry per agent of this runtime; each agent holds its own claim, and the runtime's exit releases all.
41
- const activeSessionIdentities = new Map();
42
- let sessionRuntimeRefreshTimer = null;
43
- let sessionRuntimeRefresh = Promise.resolve();
44
- let sessionRuntimeDisposed = false;
45
- const runtimeIdentity = async () => {
46
- if (sessionRuntimeId === undefined || inspectSessionProcess === undefined)
47
- return null;
48
- sessionRuntimeIdentity ??= inspectSessionProcess(process.pid).catch(() => null);
49
- return await sessionRuntimeIdentity;
50
- };
51
- const queueSessionRuntimeRefresh = () => {
52
- if (sessionRuntimeDisposed || activeSessionIdentities.size === 0 || sessionRuntimeId === undefined || inspectSessionProcess === undefined)
53
- return;
54
- sessionRuntimeRefresh = sessionRuntimeRefresh.then(async () => {
55
- if (sessionRuntimeDisposed)
56
- return;
57
- const owner = await runtimeIdentity();
58
- if (owner === null)
59
- return;
60
- const { registerSessionRepositoryRuntime } = await import("../foundation/lifecycle/session-repository-context.js");
61
- for (const [agentId, identity] of activeSessionIdentities) {
62
- if (sessionRuntimeDisposed)
63
- return;
64
- await registerSessionRepositoryRuntime(identity, sessionRuntimeId, owner, {
65
- inspectProcess: inspectSessionProcess,
66
- ...(agentId === undefined ? {} : { agentId }),
67
- }).catch(() => undefined);
68
- }
69
- }).catch(() => undefined);
70
- };
71
- /** The repository context reader for one agent; undefined names the primary agent, the only one composed today. */
72
- const repositoryContextReaderFor = (agentId) => async (sessionId, sessionFile, signal) => {
73
- const owner = await runtimeIdentity();
74
- if (owner === null || sessionRuntimeId === undefined || inspectSessionProcess === undefined)
75
- return null;
76
- const identity = { sessionId, sessionFile };
77
- activeSessionIdentities.set(agentId, identity);
78
- const { activateSessionRepositoryContext } = await import("../foundation/lifecycle/session-repository-context.js");
79
- const context = await activateSessionRepositoryContext(identity, sessionRuntimeId, owner, {
80
- signal,
81
- inspectProcess: inspectSessionProcess,
82
- ...(agentId === undefined ? {} : { agentId }),
83
- });
84
- armSessionRuntimeRefresh();
85
- return context;
86
- };
87
- const armSessionRuntimeRefresh = () => {
88
- if (sessionRuntimeRefreshTimer !== null)
89
- return;
90
- sessionRuntimeRefreshTimer = setInterval(queueSessionRuntimeRefresh, SESSION_RUNTIME_REFRESH_MS);
91
- sessionRuntimeRefreshTimer.unref();
92
- };
37
+ const sessionRuntimes = new SessionRuntimeRegistry({
38
+ runtimeId: ownedSurfaces ? process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId] : undefined,
39
+ inspectProcess: options.inspectProcess,
40
+ });
93
41
  const productPaths = resolveProductPaths();
94
42
  const profilePaths = resolveLaunchProfilePaths();
95
43
  // Rationale: the manager reads the engine's settings port lazily, after the session exists.
@@ -144,7 +92,7 @@ export async function composeOwnedUi(options = {}) {
144
92
  try {
145
93
  backend = await host.create({
146
94
  cwd,
147
- ...(ownedSurfaces ? { repositoryContextReader: repositoryContextReaderFor(undefined) } : {}),
95
+ ...(ownedSurfaces ? { repositoryContextReader: sessionRuntimes.readerFor(undefined) } : {}),
148
96
  ...(options.sessionPath === undefined ? {} : { sessionPath: options.sessionPath }),
149
97
  ...(options.sessionSelection === undefined ? {} : { sessionSelection: options.sessionSelection }),
150
98
  ...(sessionForkPrompt === undefined ? {} : { sessionForkPrompt }),
@@ -168,39 +116,32 @@ export async function composeOwnedUi(options = {}) {
168
116
  tabBridge.accent(settings.value("accentColor"));
169
117
  let removeControllerBoundaryInput = () => { };
170
118
  let unsubscribeTabBridge = () => { };
171
- let releaseNotes = null;
172
- let releaseNotesFailure;
173
- if (ownedSurfaces) {
174
- try {
175
- releaseNotes = options.releaseNotes
176
- ?? await import("../features/owned-ui/release-notes.js").then(module => module.readPackagedReleaseNotes());
177
- }
178
- catch (error) {
179
- releaseNotesFailure = error;
180
- }
181
- }
182
- const packageVersion = ownedSurfaces
183
- ? options.packageVersion ?? await readPackageVersion()
184
- : null;
185
- const currentReleaseNote = packageVersion === null ? null : releaseNotes?.current(packageVersion) ?? null;
186
- const releaseNoteClaim = currentReleaseNote === null || options.profileId === undefined
187
- ? null
188
- : await import("../features/owned-ui/release-note-state.js").then(module => module.claimReleaseNote({
189
- configDir: productPaths.configDir, profileId: options.profileId, version: currentReleaseNote.version,
190
- }));
191
- const unsubscribeAccent = settings === null || !ownedSurfaces
119
+ const releaseNote = await StartupReleaseNote.load({
120
+ ownedSurfaces,
121
+ configDir: productPaths.configDir,
122
+ ...(options.releaseNotes === undefined ? {} : { releaseNotes: options.releaseNotes }),
123
+ ...(options.packageVersion === undefined ? {} : { packageVersion: options.packageVersion }),
124
+ ...(options.profileId === undefined ? {} : { profileId: options.profileId }),
125
+ });
126
+ const unsubscribeAccentChange = settings === null || !ownedSurfaces
192
127
  ? () => { }
193
128
  : settings.onChange(current => {
194
129
  const accent = current.value("accentColor");
195
130
  host.setAccentColor(accent);
196
131
  tabBridge?.accent(accent);
197
132
  });
133
+ // Rationale: every resident tab is its own process over one shared settings document, so a
134
+ // change saved in any tab reaches the others only by rereading that document.
135
+ const unwatchSharedSettings = settings === null || !ownedSurfaces || tabBridge === null
136
+ ? () => { }
137
+ : watchSharedSettings(settings);
138
+ const unsubscribeAccent = () => { unwatchSharedSettings(); unsubscribeAccentChange(); };
198
139
  // Rationale: Routes open only after shell construction.
199
140
  const references = {
200
141
  changelog: async (input) => {
201
- if (input?.document === undefined && releaseNotesFailure !== undefined)
202
- throw releaseNotesFailure;
203
- const markdown = input?.document ?? releaseNotes?.completeMarkdown ?? "No A1 release notes found.";
142
+ if (input?.document === undefined && releaseNote.failure !== undefined)
143
+ throw releaseNote.failure;
144
+ const markdown = input?.document ?? releaseNote.catalog?.completeMarkdown ?? "No A1 release notes found.";
204
145
  return { rows: width => renderPiShellChangelogLines(markdown, width).map(row => nativeHyperlinkStyle(row)) };
205
146
  },
206
147
  hotkeys: async () => {
@@ -210,7 +151,9 @@ export async function composeOwnedUi(options = {}) {
210
151
  },
211
152
  session: () => import("./session-info-reference.js").then(m => m.loadSessionInfoReference(backend)),
212
153
  };
213
- const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references);
154
+ const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references, {
155
+ settingsPinned: tabBridge?.tabId.startsWith("settings-") === true,
156
+ });
214
157
  const viewportSettings = settings === null || !ownedSurfaces ? null : {
215
158
  snapshot: () => viewportSettingsSnapshot(settings),
216
159
  onChange: listener => settings.onChange(() => listener(viewportSettingsSnapshot(settings))),
@@ -257,22 +200,11 @@ export async function composeOwnedUi(options = {}) {
257
200
  // Invariant: process-wide services are created once here and borrowed by every session shell. The
258
201
  // keybinding registry is applied before any presenter is built, so none of them reads Pi's defaults.
259
202
  const keybindingsHost = createPiKeybindingsHost({ profile: ownedSurfaces ? "a1" : "pi", agentDir: backend.identity.agentDir });
260
- // Invariant: the resident host dispatches and labels the tab shortcuts this child projects
261
- // from its effective declarations, so a customization reload republishes the projection.
262
- const publishTabShortcuts = () => tabBridge?.shortcuts(projectResidentTabShortcuts({
263
- configured: readShortcutBindingsFile(backend.identity.agentDir),
264
- reserved: keybindingsHost.manager.getEffectiveConfig(),
265
- }).assignments);
266
- const keybindings = tabBridge === null ? keybindingsHost : {
267
- ...keybindingsHost,
268
- reload: () => { keybindingsHost.reload(); publishTabShortcuts(); },
269
- };
270
- publishTabShortcuts();
203
+ const keybindings = residentTabKeybindings(tabBridge, backend.identity.agentDir, keybindingsHost);
271
204
  const clipboardServices = ownedSurfaces ? createOwnedUiClipboardServices() : null;
272
205
  let createdTerminalHost;
273
206
  let terminalHost;
274
207
  let presenter;
275
- let releaseNoteAcknowledgement = null;
276
208
  try {
277
209
  const presenterOptions = {
278
210
  presenters: createPiSessionPresenters(backend),
@@ -280,16 +212,7 @@ export async function composeOwnedUi(options = {}) {
280
212
  backend,
281
213
  cwd: backend.identity.cwd,
282
214
  ...(routeHost === null ? {} : { routeHost }),
283
- ...(currentReleaseNote === null || releaseNoteClaim === null ? {} : {
284
- startupRoute: {
285
- route: "changelog",
286
- input: { document: currentReleaseNote.markdown },
287
- onClosed: () => {
288
- releaseNoteAcknowledgement ??= releaseNoteClaim.acknowledge();
289
- return releaseNoteAcknowledgement;
290
- },
291
- },
292
- }),
215
+ ...(releaseNote.startupRoute === undefined ? {} : { startupRoute: releaseNote.startupRoute }),
293
216
  ...(ownedSurfaces ? { sessionLayout: "custom-viewport" } : {}),
294
217
  ...(tabBridge === null || tabRecovery === null ? {} : { residentTab: {
295
218
  requestDetach: () => tabBridge.requestDetach(),
@@ -358,7 +281,7 @@ export async function composeOwnedUi(options = {}) {
358
281
  catch (error) {
359
282
  await createdTerminalHost?.dispose().catch(() => undefined);
360
283
  unsubscribeAccent();
361
- await releaseNoteClaim?.release();
284
+ await releaseNote.releaseClaim();
362
285
  clipboardDiagnostics?.dispose();
363
286
  suggestionDiagnostics?.dispose();
364
287
  clipboardServices?.dispose();
@@ -372,7 +295,7 @@ export async function composeOwnedUi(options = {}) {
372
295
  const check = options.checkForNewerRelease
373
296
  ?? (await import("../foundation/release/latest-release.js")).checkForNewerRelease;
374
297
  const release = await check({
375
- runningVersion: packageVersion ?? await readPackageVersion(),
298
+ runningVersion: releaseNote.packageVersion ?? await readPackageVersion(),
376
299
  configDir: productPaths.configDir,
377
300
  interactive: process.stdout.isTTY === true,
378
301
  ...(settings === null || !ownedSurfaces ? {} : { settingEnabled: settings.value("updateCheck") }),
@@ -401,25 +324,15 @@ export async function composeOwnedUi(options = {}) {
401
324
  await terminalHost.dispose();
402
325
  // Invariant: cleared only once the terminal is restored; a failed dispose leaves it armed.
403
326
  (await exitNotice)?.clear();
404
- await releaseNoteAcknowledgement?.catch(() => undefined);
327
+ await releaseNote.settleAcknowledgement();
405
328
  }
406
329
  finally {
407
330
  // Invariant: the shared spares outlive every shell and stop only after the last one is disposed.
408
331
  clipboardServices?.dispose();
409
332
  removeControllerBoundaryInput();
410
333
  unsubscribeAccent();
411
- sessionRuntimeDisposed = true;
412
- if (sessionRuntimeRefreshTimer !== null)
413
- clearInterval(sessionRuntimeRefreshTimer);
414
- sessionRuntimeRefreshTimer = null;
415
- await sessionRuntimeRefresh;
416
- const owner = await runtimeIdentity();
417
- if (sessionRuntimeId !== undefined && owner !== null) {
418
- await import("../foundation/lifecycle/session-repository-context.js")
419
- .then(module => module.releaseSessionRepositoryRuntime(sessionRuntimeId, owner))
420
- .catch(() => undefined);
421
- }
422
- await releaseNoteClaim?.release();
334
+ await sessionRuntimes.dispose();
335
+ await releaseNote.releaseClaim();
423
336
  suggestionDiagnostics?.dispose();
424
337
  clipboardDiagnostics?.dispose();
425
338
  unsubscribeTabBridge();
@@ -431,127 +344,20 @@ export async function composeOwnedUi(options = {}) {
431
344
  };
432
345
  return { application, settings };
433
346
  }
434
- export async function bindResidentTabBridge(tabBridge, backend, recovery = null, options = {}) {
435
- await tabBridge.start().catch(() => false);
436
- tabBridge.status("starting");
437
- const currentSessionFile = () => currentBackendSessionFile(backend);
438
- let pendingAutoName = null;
439
- const isDefaultName = (name) => name == null || /^agent(?: \d+)?$/i.test(name);
440
- const currentSessionName = () => backend.session.view().status.footer?.sessionName ?? null;
441
- const announceSession = () => {
442
- const name = currentSessionName();
443
- const nameSource = name === pendingAutoName ? "auto" : isDefaultName(name) ? "default" : "user";
444
- tabBridge.session(currentSessionFile(), name, nameSource);
445
- };
446
- let working = false;
447
- let lastTurnFailed = false;
448
- let autoNameAttempted = false;
449
- let autoNameArmed = false;
450
- // Invariant: naming launches once per tab during an agent run, as soon as its prompt is projected,
451
- // and runs beside the turn instead of waiting for it to settle.
452
- const launchAutoName = () => {
453
- if (!autoNameArmed || autoNameAttempted || options.autoName !== true || !isDefaultName(currentSessionName()))
454
- return;
455
- const prompt = backend.session.view().transcript.find(block => block.kind === "user")?.text.trim();
456
- if (!prompt)
457
- return;
458
- autoNameAttempted = true;
459
- void import("../features/resident-tab/index.js").then(({ generateAutoName }) => generateAutoName(prompt, request => backend.generateResidentTabName(request.prompt, request.maxTokens, request.signal))).then(async (name) => {
460
- // Invariant: a manual rename made while generation was pending owns both the session and the tab.
461
- if (!isDefaultName(currentSessionName()))
462
- return;
463
- pendingAutoName = name;
464
- const result = await backend.workflows.executeWorkflow({ command: "name", argument: name });
465
- if (result.outcome === "completed")
466
- tabBridge.session(currentSessionFile(), name, "auto");
467
- else if (pendingAutoName === name)
468
- pendingAutoName = null;
469
- }).catch(() => { pendingAutoName = null; });
470
- };
471
- const reportStatus = (lifecycle) => {
472
- const view = backend.session.view();
473
- const pendingRequests = view.dialog == null ? 0 : 1;
474
- const queuedInput = view.editor?.queuedSubmissions.length ?? 0;
475
- tabBridge.status(deriveResidentTabStatus({
476
- lifecycle: lifecycle ?? view.lifecycle,
477
- pendingRequests,
478
- queuedInput,
479
- ...(lastTurnFailed ? { stopReason: "error" } : {}),
480
- }), false, pendingRequests, queuedInput);
481
- };
482
- announceSession();
483
- const unsubscribeCommands = typeof tabBridge.onCommand === "function" ? tabBridge.onCommand(command => {
484
- // Invariant: visibility stays decoded for generation-1 peers but never pauses rendering; a hidden tab
485
- // keeps its resident terminal current while the holder withholds only its client surfaces.
486
- if (command.type === "rename") {
487
- void backend.workflows.executeWorkflow({ command: "name", argument: command.name }).catch(() => { });
488
- }
489
- else if (command.type === "route") {
490
- if (options.openRoute !== undefined)
491
- options.openRoute(command.route);
492
- else
493
- void backend.workflows.executeWorkflow({ command: command.route, argument: "" }).catch(() => { });
494
- }
495
- else if (command.type === "stop") {
496
- void backend.session.dispose().catch(() => { });
497
- }
498
- }) : () => { };
499
- const unsubscribeEvents = backend.session.onEvent(event => {
500
- if (event.type === "session-lifecycle") {
501
- const wasWorking = working;
502
- working = event.lifecycle === "busy";
503
- if (working)
504
- lastTurnFailed = false;
505
- reportStatus(event.lifecycle);
506
- if (wasWorking && !working && recovery !== null) {
507
- void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
508
- }
509
- }
510
- if (event.type === "agent-run-started") {
511
- autoNameArmed = true;
512
- launchAutoName();
513
- }
514
- // Rationale: Pi starts the run before it projects the user message, so the prompt may arrive later.
515
- if (event.type === "transcript-block" || event.type === "session-view")
516
- launchAutoName();
517
- if (event.type === "agent-run-settled") {
518
- launchAutoName();
519
- autoNameArmed = false;
520
- lastTurnFailed = !event.successful && !event.aborted;
521
- tabBridge.prompt(event.aborted);
522
- reportStatus();
347
+ const SHARED_SETTINGS_POLL_MS = 250;
348
+ /** Rereads the shared settings document whenever its stat changes; returns the stop. */
349
+ export function watchSharedSettings(settings) {
350
+ const file = settings.file;
351
+ const listener = () => {
352
+ try {
353
+ settings.refresh();
523
354
  }
524
- if (event.type === "session-view") {
525
- announceSession();
526
- reportStatus();
355
+ catch {
356
+ // Compatibility: an unreadable peer write resolves on the next stat change.
527
357
  }
528
- });
529
- return () => {
530
- unsubscribeCommands();
531
- unsubscribeEvents();
532
358
  };
533
- }
534
- export async function correlateResidentTabSubmission(recovery, backend, submissionId, entryId) {
535
- await recovery.correlated(submissionId, entryId);
536
- // Concurrency: a normal prompt command can resolve after the ready transition already fired.
537
- // Re-read lifecycle only after correlation is active; a concurrent ready observer and this path
538
- // safely race because recovery consumes the active entry once and retirement is idempotent.
539
- if (backend.session.view().lifecycle !== "busy")
540
- await recovery.settled(currentBackendSessionFile(backend));
541
- }
542
- function currentBackendSessionFile(backend) {
543
- const direct = backend.identity.currentSessionFile?.();
544
- if (direct !== undefined && direct !== null)
545
- return direct;
546
- const resume = backend.identity.currentSessionResumeMetadata();
547
- return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
548
- }
549
- async function readPackageVersion() {
550
- const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
551
- const version = manifest?.version;
552
- if (typeof version !== "string")
553
- throw new Error("A1 package version is unavailable");
554
- return version;
359
+ watchFile(file, { interval: SHARED_SETTINGS_POLL_MS, persistent: false }, listener);
360
+ return () => unwatchFile(file, listener);
555
361
  }
556
362
  function quitOutroSettingsSnapshot(settings) {
557
363
  return {
@@ -0,0 +1,16 @@
1
+ import type { TabBridgeClient } from "../foundation/resident-tabs/bridge-client.js";
2
+ import type { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
3
+ import type { PiKeybindingsHost } from "../integrations/pi/components/shell-shared-facade.js";
4
+ import type { ComposedSessionBackend } from "./session-backend.js";
5
+ /**
6
+ * The keybindings the shells borrow. Inside a resident tab the host dispatches and labels the tab
7
+ * shortcuts this child projects from its effective declarations, so the projection is published now
8
+ * and again after every customization reload.
9
+ */
10
+ export declare function residentTabKeybindings(tabBridge: TabBridgeClient | null, agentDir: string, keybindingsHost: PiKeybindingsHost): PiKeybindingsHost;
11
+ export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backend: Pick<ComposedSessionBackend, "identity" | "session" | "workflows" | "generateResidentTabName">, recovery?: Pick<ResidentTabRecovery, "settled"> | null, options?: {
12
+ readonly autoName?: boolean;
13
+ readonly openRoute?: (route: "settings") => void;
14
+ }): Promise<() => void>;
15
+ export declare function correlateResidentTabSubmission(recovery: Pick<ResidentTabRecovery, "correlated" | "settled">, backend: Pick<ComposedSessionBackend, "identity" | "session">, submissionId: string, entryId: string): Promise<void>;
16
+ export declare function currentBackendSessionFile(backend: Pick<ComposedSessionBackend, "identity">): string | null;
@@ -0,0 +1,159 @@
1
+ import { deriveResidentTabStatus } from "../features/resident-tab/status-deriver.js";
2
+ import { projectResidentTabShortcuts } from "../features/resident-tab/shortcut-projection.js";
3
+ import { readShortcutBindingsFile } from "../features/resident-tab/shortcut-bindings-file.js";
4
+ /**
5
+ * The keybindings the shells borrow. Inside a resident tab the host dispatches and labels the tab
6
+ * shortcuts this child projects from its effective declarations, so the projection is published now
7
+ * and again after every customization reload.
8
+ */
9
+ export function residentTabKeybindings(tabBridge, agentDir, keybindingsHost) {
10
+ if (tabBridge === null)
11
+ return keybindingsHost;
12
+ const publish = () => tabBridge.shortcuts(projectResidentTabShortcuts({
13
+ configured: readShortcutBindingsFile(agentDir),
14
+ reserved: keybindingsHost.manager.getEffectiveConfig(),
15
+ }).assignments);
16
+ publish();
17
+ return { ...keybindingsHost, reload: () => { keybindingsHost.reload(); publish(); } };
18
+ }
19
+ export async function bindResidentTabBridge(tabBridge, backend, recovery = null, options = {}) {
20
+ await tabBridge.start().catch(() => false);
21
+ tabBridge.status("starting");
22
+ const currentSessionFile = () => currentBackendSessionFile(backend);
23
+ let pendingAutoName = null;
24
+ // Invariant: while a generated name is being applied, the session name it produces is automatic
25
+ // even if the session normalizes it; reporting it as `user` would block the strip's title
26
+ // animation and every later automatic rename.
27
+ let applyingAutoName = false;
28
+ const isDefaultName = (name) => name == null || /^agent(?: \d+)?$/i.test(name);
29
+ const currentSessionName = () => backend.session.view().status.footer?.sessionName ?? null;
30
+ const announceSession = () => {
31
+ const name = currentSessionName();
32
+ // Invariant: the session may announce the applied name after the workflow resolves; the
33
+ // first non-default name announced for a pending auto-name is that automatic name.
34
+ if (applyingAutoName && !isDefaultName(name)) {
35
+ pendingAutoName = name;
36
+ applyingAutoName = false;
37
+ }
38
+ const automatic = name === pendingAutoName;
39
+ const nameSource = automatic ? "auto" : isDefaultName(name) ? "default" : "user";
40
+ tabBridge.session(currentSessionFile(), name, nameSource);
41
+ };
42
+ let working = false;
43
+ // Invariant: the last turn's settlement stays reported until the next turn starts.
44
+ let lastTurn;
45
+ let autoNameAttempted = false;
46
+ let autoNameArmed = false;
47
+ // Invariant: naming launches once per tab during an agent run, as soon as its prompt is projected,
48
+ // and runs beside the turn instead of waiting for it to settle.
49
+ const launchAutoName = () => {
50
+ if (!autoNameArmed || autoNameAttempted || options.autoName !== true || !isDefaultName(currentSessionName()))
51
+ return;
52
+ const prompt = backend.session.view().transcript.find(block => block.kind === "user")?.text.trim();
53
+ if (!prompt)
54
+ return;
55
+ autoNameAttempted = true;
56
+ void import("../features/resident-tab/index.js").then(({ generateAutoName }) => generateAutoName(prompt, request => backend.generateResidentTabName(request.prompt, request.maxTokens, request.signal))).then(async (name) => {
57
+ // Invariant: a manual rename made while generation was pending owns both the session and the tab.
58
+ if (!isDefaultName(currentSessionName()))
59
+ return;
60
+ pendingAutoName = name;
61
+ applyingAutoName = true;
62
+ const result = await backend.workflows.executeWorkflow({ command: "name", argument: name });
63
+ if (result.outcome !== "completed") {
64
+ applyingAutoName = false;
65
+ if (pendingAutoName === name)
66
+ pendingAutoName = null;
67
+ return;
68
+ }
69
+ const current = currentSessionName();
70
+ if (current !== null && !isDefaultName(current)) {
71
+ // Invariant: The session already holds the applied name; report it as automatic now.
72
+ pendingAutoName = current;
73
+ applyingAutoName = false;
74
+ tabBridge.session(currentSessionFile(), current, "auto");
75
+ }
76
+ else {
77
+ // Invariant: The session announces later; show the generated name meanwhile, still automatic.
78
+ tabBridge.session(currentSessionFile(), name, "auto");
79
+ }
80
+ }).catch(() => { pendingAutoName = null; applyingAutoName = false; });
81
+ };
82
+ const reportStatus = (lifecycle) => {
83
+ const view = backend.session.view();
84
+ const pendingRequests = view.dialog == null ? 0 : 1;
85
+ const queuedInput = view.editor?.queuedSubmissions.length ?? 0;
86
+ tabBridge.status(deriveResidentTabStatus({
87
+ lifecycle: lifecycle ?? view.lifecycle,
88
+ pendingRequests,
89
+ queuedInput,
90
+ ...(lastTurn === undefined ? {} : { lastTurn }),
91
+ }), false, pendingRequests, queuedInput);
92
+ };
93
+ announceSession();
94
+ const unsubscribeCommands = typeof tabBridge.onCommand === "function" ? tabBridge.onCommand(command => {
95
+ // Invariant: visibility stays decoded for generation-1 peers but never pauses rendering; a hidden tab
96
+ // keeps its resident terminal current while the holder withholds only its client surfaces.
97
+ if (command.type === "rename") {
98
+ void backend.workflows.executeWorkflow({ command: "name", argument: command.name }).catch(() => { });
99
+ }
100
+ else if (command.type === "route") {
101
+ if (options.openRoute !== undefined)
102
+ options.openRoute(command.route);
103
+ else
104
+ void backend.workflows.executeWorkflow({ command: command.route, argument: "" }).catch(() => { });
105
+ }
106
+ else if (command.type === "stop") {
107
+ void backend.session.dispose().catch(() => { });
108
+ }
109
+ }) : () => { };
110
+ const unsubscribeEvents = backend.session.onEvent(event => {
111
+ if (event.type === "session-lifecycle") {
112
+ const wasWorking = working;
113
+ working = event.lifecycle === "busy";
114
+ if (working)
115
+ lastTurn = undefined;
116
+ reportStatus(event.lifecycle);
117
+ if (wasWorking && !working && recovery !== null) {
118
+ void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
119
+ }
120
+ }
121
+ if (event.type === "agent-run-started") {
122
+ autoNameArmed = true;
123
+ launchAutoName();
124
+ }
125
+ // Rationale: Pi starts the run before it projects the user message, so the prompt may arrive later.
126
+ if (event.type === "transcript-block" || event.type === "session-view")
127
+ launchAutoName();
128
+ if (event.type === "agent-run-settled") {
129
+ launchAutoName();
130
+ autoNameArmed = false;
131
+ lastTurn = event.aborted ? "aborted" : event.successful ? "done" : "error";
132
+ tabBridge.prompt(event.aborted);
133
+ reportStatus();
134
+ }
135
+ if (event.type === "session-view") {
136
+ announceSession();
137
+ reportStatus();
138
+ }
139
+ });
140
+ return () => {
141
+ unsubscribeCommands();
142
+ unsubscribeEvents();
143
+ };
144
+ }
145
+ export async function correlateResidentTabSubmission(recovery, backend, submissionId, entryId) {
146
+ await recovery.correlated(submissionId, entryId);
147
+ // Concurrency: a normal prompt command can resolve after the ready transition already fired.
148
+ // Re-read lifecycle only after correlation is active; a concurrent ready observer and this path
149
+ // safely race because recovery consumes the active entry once and retirement is idempotent.
150
+ if (backend.session.view().lifecycle !== "busy")
151
+ await recovery.settled(currentBackendSessionFile(backend));
152
+ }
153
+ export function currentBackendSessionFile(backend) {
154
+ const direct = backend.identity.currentSessionFile?.();
155
+ if (direct !== undefined && direct !== null)
156
+ return direct;
157
+ const resume = backend.identity.currentSessionResumeMetadata();
158
+ return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
159
+ }
@@ -0,0 +1,19 @@
1
+ import type { AgentSettingsPort } from "../contracts/agent-engine/index.js";
2
+ import type { OwnedUiPromptSuggestionGeneratorPort, UiAccentColor } from "../contracts/owned-ui/index.js";
3
+ import type { PiEngineSessionRequest } from "../integrations/pi/engine/host.js";
4
+ import type { PiSessionPresenterBackend } from "../integrations/pi/session-presenters/index.js";
5
+ /**
6
+ * The session backend composition wires: the owned-UI port, the prompt-suggestion generator, and
7
+ * the agent settings port. `settingsPort` stays outside the owned-UI contract because its type
8
+ * belongs to the agent-engine contracts, which the owned-UI contract may not import.
9
+ */
10
+ export interface ComposedSessionBackend extends PiSessionPresenterBackend, OwnedUiPromptSuggestionGeneratorPort {
11
+ settingsPort(): AgentSettingsPort | null;
12
+ generateResidentTabName(instruction: string, maxTokens: number, signal: AbortSignal): Promise<string | null>;
13
+ }
14
+ /** The process-level engine host as composition drives it: sessions, the live accent, and disposal. */
15
+ export interface ComposedEngineHost {
16
+ create(request: PiEngineSessionRequest): Promise<ComposedSessionBackend>;
17
+ setAccentColor(color: UiAccentColor): void;
18
+ dispose(): Promise<void>;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { NativeProcessIdentity } from "../foundation/lifecycle/model.js";
2
+ export interface SessionRuntimeRegistryOptions {
3
+ /** This runtime's id from the environment; absent outside a runtime that claims worktrees. */
4
+ readonly runtimeId: string | undefined;
5
+ /** Exact native process inspection; absent SDK seams publish no claims. */
6
+ readonly inspectProcess: ((pid: number) => Promise<NativeProcessIdentity | null>) | undefined;
7
+ readonly refreshMs?: number;
8
+ }
9
+ /**
10
+ * The live worktree claims this runtime's agents hold: each agent's session identity, the heartbeat
11
+ * that keeps the claims fresh, and their release when the runtime is disposed.
12
+ */
13
+ export declare class SessionRuntimeRegistry {
14
+ #private;
15
+ constructor(options: SessionRuntimeRegistryOptions);
16
+ /** The repository context reader for one agent; undefined names the primary agent. */
17
+ readerFor(agentId: string | undefined): (sessionId: string, sessionFile: string, signal: AbortSignal) => Promise<import("../foundation/lifecycle/session-repository-context.js").SessionRepositoryContext | null>;
18
+ /** Stops the heartbeat, waits for an in-flight refresh, and releases every claim this runtime holds. */
19
+ dispose(): Promise<void>;
20
+ }