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

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 (149) hide show
  1. package/bin/guardian.js +5 -0
  2. package/bin/tab-launch-context.d.ts +23 -0
  3. package/bin/tab-launch-context.js +45 -0
  4. package/bin/ui.js +20 -5
  5. package/dist/app/session-shell/helper-pool.d.ts +3 -5
  6. package/dist/app/session-shell/helper-pool.js +3 -5
  7. package/dist/app/session-shell/session-shell-root.d.ts +29 -1
  8. package/dist/app/session-shell/session-shell-root.js +3 -2
  9. package/dist/app/session-shell/session-shell.js +153 -30
  10. package/dist/app/session-shell/session-viewport-controller.d.ts +1 -1
  11. package/dist/app/session-shell/session-viewport-controller.js +3 -3
  12. package/dist/composition/owned-ui.d.ts +20 -0
  13. package/dist/composition/owned-ui.js +76 -3
  14. package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
  15. package/dist/features/owned-ui/settings-app.js +8 -35
  16. package/dist/foundation/launch-context/index.d.ts +6 -0
  17. package/dist/foundation/launch-context/index.js +15 -0
  18. package/dist/foundation/launch-guardian/main.d.ts +5 -0
  19. package/dist/foundation/launch-guardian/main.js +2 -1
  20. package/dist/foundation/release/bootstrap.d.ts +4 -0
  21. package/dist/foundation/release/bootstrap.js +44 -6
  22. package/dist/foundation/release/dependency-certification.js +54 -19
  23. package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
  24. package/dist/foundation/release/progress-palette.generated.js +2 -2
  25. package/dist/foundation/resident-tabs/bridge-client.d.ts +41 -0
  26. package/dist/foundation/resident-tabs/bridge-client.js +199 -0
  27. package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
  28. package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
  29. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +30 -0
  30. package/dist/foundation/resident-tabs/ensure-terminal-host.js +121 -0
  31. package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
  32. package/dist/foundation/resident-tabs/fault-injection.js +26 -0
  33. package/dist/foundation/resident-tabs/index.d.ts +6 -0
  34. package/dist/foundation/resident-tabs/index.js +3 -0
  35. package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
  36. package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
  37. package/dist/foundation/resident-tabs/protocol.d.ts +12 -0
  38. package/dist/foundation/resident-tabs/protocol.js +195 -0
  39. package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
  40. package/dist/foundation/resident-tabs/recovery.js +205 -0
  41. package/dist/foundation/resident-tabs/routing.d.ts +10 -0
  42. package/dist/foundation/resident-tabs/routing.js +8 -0
  43. package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
  44. package/dist/foundation/resident-tabs/session-sync.js +37 -0
  45. package/dist/foundation/resident-tabs/settings.d.ts +9 -0
  46. package/dist/foundation/resident-tabs/settings.js +65 -0
  47. package/dist/foundation/session-writer-lock/index.d.ts +27 -0
  48. package/dist/foundation/session-writer-lock/index.js +197 -0
  49. package/dist/foundation/startup/startup-descriptor.js +2 -2
  50. package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
  51. package/dist/foundation/terminal-cleanup/index.js +1 -1
  52. package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +1 -0
  53. package/dist/foundation/terminal-cleanup/terminal-reset.js +1 -0
  54. package/dist/integrations/pi/components/owned-editor-ux.d.ts +1 -1
  55. package/dist/integrations/pi/components/owned-editor-ux.js +2 -2
  56. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
  57. package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
  58. package/dist/integrations/pi/components/shell-shared-facade.d.ts +1 -1
  59. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
  60. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
  61. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  62. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
  63. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
  64. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
  65. package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
  66. package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
  67. package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
  68. package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
  69. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
  70. package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
  71. package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
  72. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
  73. package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
  74. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
  75. package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
  76. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
  77. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
  78. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
  79. package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
  80. package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
  81. package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
  82. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
  83. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
  84. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
  85. package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
  86. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
  87. package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
  88. package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
  89. package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
  90. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
  91. package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
  92. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
  93. package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
  94. package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
  95. package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
  96. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
  97. package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
  98. package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
  99. package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
  100. package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
  101. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
  102. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
  103. package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
  104. package/dist/integrations/pi/engine/adapter.js +1 -0
  105. package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
  106. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  107. package/dist/integrations/pi/engine/runtime-integration.js +102 -29
  108. package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
  109. package/dist/integrations/pi/engine/session-runtime.js +8 -3
  110. package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
  111. package/dist/integrations/pi/engine/session-selection.js +45 -9
  112. package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
  113. package/dist/integrations/pi/startup-public.js +152 -222
  114. package/dist/integrations/pi/startup-public.manifest.json +266 -177
  115. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  116. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
  117. package/dist/native/darwin-arm64/manifest.json +1 -1
  118. package/dist/native/linux-x64/manifest.json +1 -1
  119. package/dist/native/win32-x64/manifest.json +2 -2
  120. package/dist/native/win32-x64/process-guardian.exe +0 -0
  121. package/dist/product-identity.d.ts +1 -1
  122. package/dist/product-identity.js +1 -1
  123. package/dist/product-identity.json +5 -0
  124. package/dist/runtime-payload-inventory.json +5349 -6035
  125. package/dist/ui/components/line-input.d.ts +1 -1
  126. package/dist/ui/components/prompt-input.d.ts +7 -5
  127. package/dist/ui/components/prompt-input.js +7 -7
  128. package/dist/ui/components/transcript-viewport.d.ts +6 -3
  129. package/dist/ui/components/transcript-viewport.js +13 -15
  130. package/dist/ui/settings/declarations.d.ts +49 -1
  131. package/dist/ui/settings/declarations.js +37 -1
  132. package/dist/ui/settings/index.d.ts +5 -3
  133. package/dist/ui/settings/index.js +3 -2
  134. package/dist/ui/settings/manager.d.ts +9 -3
  135. package/dist/ui/settings/manager.js +81 -29
  136. package/dist/ui/settings/migrations.d.ts +6 -0
  137. package/dist/ui/settings/migrations.js +32 -0
  138. package/dist/ui/settings/profile-document.d.ts +26 -0
  139. package/dist/ui/settings/profile-document.js +164 -0
  140. package/dist/ui/settings/resolution.d.ts +2 -0
  141. package/dist/ui/settings/resolution.js +13 -1
  142. package/dist/ui/settings/sections.js +4 -0
  143. package/docs/architecture/internal-naming.md +1 -1
  144. package/docs/architecture/resident-tabs-requirements.md +580 -0
  145. package/docs/architecture/resident-tabs.md +522 -0
  146. package/docs/architecture/terminal-host-proof-gate.md +2 -0
  147. package/docs/openspec-archive-automation.md +17 -16
  148. package/docs/validation.md +1 -1
  149. package/package.json +6 -4
package/bin/guardian.js CHANGED
@@ -16,8 +16,13 @@ const profileId = readLaunchContext(process.env, "profile").launchProfile;
16
16
  if (releaseRoot === undefined || profileId === undefined) throw new Error("A1 launch context is incomplete");
17
17
 
18
18
  const sessionSelection = parseSessionSelection(process.argv.slice(2));
19
+ const rootCommand = launchContext.residentRootExecutable === undefined ? undefined : {
20
+ executable: launchContext.residentRootExecutable,
21
+ arguments: JSON.parse(launchContext.residentRootArguments ?? "[]"),
22
+ };
19
23
  Promise.resolve().then(() => runLaunchGuardian({
20
24
  ...(sessionSelection === undefined ? {} : { sessionSelection }),
25
+ ...(rootCommand === undefined ? {} : { rootCommand }),
21
26
  profileId,
22
27
  releaseRoot,
23
28
  uiEntry: resolve(releaseRoot, "bin", "ui.js"),
@@ -0,0 +1,23 @@
1
+ export interface TabLaunchCredentials {
2
+ readonly tabId: string;
3
+ readonly token: string;
4
+ readonly endpoint: string;
5
+ }
6
+
7
+ export function formatResidentFallbackNotice(reason: string): string;
8
+
9
+ export function assignFreshSessionRuntimeId(
10
+ environment: Record<string, string | undefined>,
11
+ name: string,
12
+ createId: () => string,
13
+ ): string;
14
+
15
+ export function consumeTabLaunchContext(
16
+ arguments_: readonly string[],
17
+ environment: Record<string, string | undefined>,
18
+ ): {
19
+ readonly uiArguments: string[];
20
+ readonly tabCredentials: TabLaunchCredentials | undefined;
21
+ readonly tabDataDir?: string;
22
+ readonly tabIncarnation?: number;
23
+ };
@@ -0,0 +1,45 @@
1
+ const TAB_ENVIRONMENT_NAMES = ["A1_TAB_ID", "A1_TAB_TOKEN", "A1_TAB_ENDPOINT"];
2
+
3
+ /** @param {string} reason */
4
+ export function formatResidentFallbackNotice(reason) {
5
+ return `Resident tabs unavailable (${reason}); running a single agent.`;
6
+ }
7
+
8
+ /**
9
+ * @param {Record<string, string | undefined>} environment
10
+ * @param {string} name
11
+ * @param {() => string} createId
12
+ */
13
+ export function assignFreshSessionRuntimeId(environment, name, createId) {
14
+ const id = createId();
15
+ if (typeof id !== "string" || id.length === 0 || id.includes("\0")) throw new Error("Session runtime identity generation failed");
16
+ environment[name] = id;
17
+ return id;
18
+ }
19
+
20
+ /**
21
+ * Security: consume bridge credentials into caller-private memory and erase their inheritable
22
+ * environment entries before any extension-capable module loads.
23
+ * @param {readonly string[]} arguments_
24
+ * @param {Record<string, string | undefined>} environment
25
+ */
26
+ export function consumeTabLaunchContext(arguments_, environment) {
27
+ const uiArguments = [...arguments_];
28
+ const tabArgument = uiArguments.indexOf("--tab");
29
+ const tabMode = tabArgument !== -1;
30
+ if (tabMode) uiArguments.splice(tabArgument, 1);
31
+ const dataArgument = uiArguments.indexOf("--tab-data-dir");
32
+ const tabDataDir = dataArgument === -1 ? undefined : uiArguments[dataArgument + 1];
33
+ if (dataArgument !== -1) uiArguments.splice(dataArgument, 2);
34
+ const incarnationArgument = uiArguments.indexOf("--tab-incarnation");
35
+ const tabIncarnation = incarnationArgument === -1 ? undefined : Number(uiArguments[incarnationArgument + 1]);
36
+ if (incarnationArgument !== -1) uiArguments.splice(incarnationArgument, 2);
37
+ const [tabId, token, endpoint] = TAB_ENVIRONMENT_NAMES.map(name => environment[name]);
38
+ for (const name of TAB_ENVIRONMENT_NAMES) delete environment[name];
39
+ if (!tabMode) return { uiArguments, tabCredentials: undefined };
40
+ if (!tabId || !token || !endpoint || !tabDataDir || tabDataDir.includes("\0")
41
+ || typeof tabIncarnation !== "number" || !Number.isSafeInteger(tabIncarnation) || tabIncarnation < 1) {
42
+ throw new Error("A1 tab mode requires bridge credentials, recovery directory, and incarnation");
43
+ }
44
+ return { uiArguments, tabCredentials: { tabId, token, endpoint }, tabDataDir, tabIncarnation };
45
+ }
package/bin/ui.js CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Security: this helper has no A1, Pi, tool, or extension dependency and erases credentials before those modules load.
4
+ const { assignFreshSessionRuntimeId, consumeTabLaunchContext, formatResidentFallbackNotice } = await import("./tab-launch-context.js");
5
+ const { uiArguments, tabCredentials, tabDataDir, tabIncarnation } = consumeTabLaunchContext(process.argv.slice(2), process.env);
6
+
3
7
  // Invariant: one terminal module identity per process, decided before any A1 or Pi module loads.
4
8
  const { installPinnedPiTuiResolver } = await import("./module-resolver.js");
5
9
  installPinnedPiTuiResolver(new URL("..", import.meta.url).pathname.replace(/^\/(.:)/, "$1").replace(/\/$/, ""));
@@ -14,15 +18,17 @@ if (profile === "a1") {
14
18
  import("node:crypto"),
15
19
  import("../dist/product-identity.js"),
16
20
  ]);
17
- process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId] = randomUUID();
21
+ assignFreshSessionRuntimeId(process.env, PRODUCT_IDENTITY.environment.sessionRuntimeId, randomUUID);
18
22
  }
19
23
  const { installFatalExit } = await import("../dist/foundation/terminal-cleanup/fatal-exit.js");
20
24
  const { resolveProductPaths } = await import("../dist/foundation/lifecycle/paths.js");
21
- const { join } = await import("node:path");
25
+ const { dirname, join } = await import("node:path");
22
26
  /** @type {{ dispose(): void | Promise<unknown> } | undefined} */
23
27
  let runningApplication;
24
28
  const fatal = profile === "a1" ? installFatalExit({
25
- directory: join(resolveProductPaths().runtimeDir, "crashes"),
29
+ directory: tabDataDir === undefined
30
+ ? join(resolveProductPaths().runtimeDir, "crashes")
31
+ : join(dirname(tabDataDir), "diagnostics"),
26
32
  ...(launchContext.releaseId === undefined ? {} : { releaseId: launchContext.releaseId }),
27
33
  dispose: () => runningApplication?.dispose(),
28
34
  }) : undefined;
@@ -51,8 +57,13 @@ const [
51
57
  identity.assertSinglePiTuiModuleAtLaunch(packageRootPath, (/** @type {string} */ message) => process.stderr.write(message));
52
58
  await startup.markStartupPhase(process.env, "ui-modules-loaded");
53
59
 
54
- const sessionSelection = parseSessionSelection(process.argv.slice(2));
55
- Promise.resolve().then(() => {
60
+ const sessionSelection = parseSessionSelection(uiArguments);
61
+ Promise.resolve().then(async () => {
62
+ const tab = tabCredentials === undefined ? undefined : {
63
+ bridge: new (await import("../dist/foundation/resident-tabs/bridge-client.js")).TabBridgeClient({ credentials: tabCredentials }),
64
+ dataDir: /** @type {string} */ (tabDataDir),
65
+ incarnation: /** @type {number} */ (tabIncarnation),
66
+ };
56
67
  if (sessionSelection && profile !== "a1") throw new Error("session selection requires the normal A1 profile");
57
68
  return runSelectedInteractiveRuntime(profile, {
58
69
  ownedUi: async (profileId, ownedSurfaces) => {
@@ -69,6 +80,10 @@ Promise.resolve().then(() => {
69
80
  projectTrustPrompt: createConsoleProjectTrustPrompt({ presentation: profile === "a1" ? "bare" : "comparison" }),
70
81
  sessionForkPrompt: createConsoleSessionForkPrompt(),
71
82
  ...(sessionSelection === undefined ? {} : { sessionSelection }),
83
+ ...(tab === undefined ? {} : { tab }),
84
+ ...(launchContext.residentFallback === undefined ? {} : {
85
+ residentFallbackNotice: formatResidentFallbackNotice(launchContext.residentFallback),
86
+ }),
72
87
  });
73
88
  runningApplication = application;
74
89
  return await runOwnedUi({ application, ...(settings === null ? {} : { settings }) });
@@ -12,16 +12,14 @@ export interface HelperPoolOptions {
12
12
  readonly stop: (child: ChildProcess) => void;
13
13
  readonly idleMs?: number;
14
14
  }
15
- /**
16
- * Keeps one forked IPC helper ready so a request takes a live child instead of paying fork and module
17
- * load on its own critical path. The spare stays idle on its channel until it is taken; a spare that
18
- * exits or idles too long is dropped, and the next request forks cold and warms a replacement afterwards.
19
- */
15
+ /** Holds one idle IPC helper; requests take it, and exits, expiry, or use trigger replacement. */
20
16
  export declare class HelperPool {
21
17
  #private;
22
18
  constructor(options: HelperPoolOptions);
23
19
  /** Whether a spare is currently held (ready or still starting). */
24
20
  get warmed(): boolean;
21
+ /** Whether the held spare has announced that module loading is complete. */
22
+ get ready(): boolean;
25
23
  /** Opts the pool in and forks the first spare; from here on `replenish` keeps one ready after each request. */
26
24
  warm(): void;
27
25
  /** Forks the next spare after a request released its child; a no-op until the owner warmed the pool. A fork failure leaves the pool empty. */
@@ -1,10 +1,6 @@
1
1
  /** How long an unused spare helper may idle before it is stopped so a background session holds no extra process. */
2
2
  export const HELPER_SPARE_IDLE_MS = 5 * 60_000;
3
- /**
4
- * Keeps one forked IPC helper ready so a request takes a live child instead of paying fork and module
5
- * load on its own critical path. The spare stays idle on its channel until it is taken; a spare that
6
- * exits or idles too long is dropped, and the next request forks cold and warms a replacement afterwards.
7
- */
3
+ /** Holds one idle IPC helper; requests take it, and exits, expiry, or use trigger replacement. */
8
4
  export class HelperPool {
9
5
  #fork;
10
6
  #stop;
@@ -19,6 +15,8 @@ export class HelperPool {
19
15
  }
20
16
  /** Whether a spare is currently held (ready or still starting). */
21
17
  get warmed() { return this.#spare !== undefined; }
18
+ /** Whether the held spare has announced that module loading is complete. */
19
+ get ready() { return this.#spare?.ready === true; }
22
20
  /** Opts the pool in and forks the first spare; from here on `replenish` keeps one ready after each request. */
23
21
  warm() {
24
22
  if (this.#disposed)
@@ -43,6 +43,34 @@ export interface OwnedUiShellEngineOptions {
43
43
  };
44
44
  /** Bare A1 selects the owned bounded viewport; comparison profiles stay pinned. */
45
45
  readonly sessionLayout?: "pinned" | "custom-viewport";
46
+ /** Resident tab quit routes detach the outer client without stopping this engine session. */
47
+ readonly residentTab?: {
48
+ requestDetach(): Promise<boolean>;
49
+ requestRestart?(): Promise<boolean>;
50
+ reportStatus?(status: "working" | "idle" | "stalled"): void;
51
+ readonly stallNoticeSeconds?: number;
52
+ admit?(input: {
53
+ readonly sessionId: string;
54
+ readonly sessionFile: string | null;
55
+ readonly kind: "prompt" | "steer" | "follow-up";
56
+ readonly text: string;
57
+ readonly images?: readonly unknown[];
58
+ }): Promise<{
59
+ readonly id: string;
60
+ }>;
61
+ dispatched?(): void;
62
+ correlated?(submissionId: string, entryId: string): Promise<void>;
63
+ checkpoint?(text: string): void;
64
+ gracefulStop?(sessionFile: string | null): Promise<void>;
65
+ readonly recovered?: {
66
+ readonly prompts: readonly {
67
+ readonly id: string;
68
+ readonly text: string;
69
+ }[];
70
+ readonly draft: string | null;
71
+ };
72
+ readonly unavailableNotice?: string;
73
+ };
46
74
  }
47
75
  /** Terminal, startup, viewport, and presentation-cadence seams; production leaves most at their defaults. */
48
76
  export interface OwnedUiShellPresentationOptions {
@@ -182,7 +210,7 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
182
210
  readonly onPromptSuggestionAccepted?: (text: string) => void;
183
211
  readonly onInputSurfaceChanged?: () => void;
184
212
  readonly onPromptSuggestionEligibilityChanged?: () => void;
185
- readonly onCopyText?: (text: string) => void;
213
+ readonly onCopyText?: (text: string, cut: boolean) => void;
186
214
  readonly readClipboardContent?: (signal?: AbortSignal) => Promise<PiShellClipboardContent | null>;
187
215
  readonly captureClipboardPaste?: () => PasteSource;
188
216
  readonly pasteDiagnostics?: (event: PasteEvent) => void;
@@ -458,7 +458,7 @@ export class OwnedUiSessionShellRoot {
458
458
  && snapshot.inputSurface === inputSurface
459
459
  && snapshot.viewportRevision === viewportRevision
460
460
  && snapshot.selectionRevision === selectionRevision
461
- ? this.#viewportController.composeDockOnly(dockRows, width, height)
461
+ ? this.#viewportController.composeDockOnly(dockRows, width, height, dock.footerRows)
462
462
  : null;
463
463
  if (frame === null) {
464
464
  frame = this.#viewportController.compose({
@@ -474,6 +474,7 @@ export class OwnedUiSessionShellRoot {
474
474
  ...(document.liveTailStartRow === undefined ? {} : { liveTailStartRow: document.liveTailStartRow }),
475
475
  bottomAlignedTailRowCount: steeringRows.length + statusRows.length,
476
476
  dockRows,
477
+ explicitCopyDockRows: dock.footerRows,
477
478
  promptAnchors: document.promptAnchors,
478
479
  width,
479
480
  height,
@@ -603,7 +604,6 @@ export class OwnedUiSessionShellRoot {
603
604
  const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
604
605
  const aboveWidgets = this.#renderWidgets("aboveEditor", width);
605
606
  const input = this.#inputSurface.render(width);
606
- // Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
607
607
  const body = this.usesDefaultInputSurface() ? this.editor.bodyGeometry?.() : undefined;
608
608
  const belowWidgets = this.#renderWidgets("belowEditor", width);
609
609
  const footer = this.#renderFooter(width);
@@ -614,6 +614,7 @@ export class OwnedUiSessionShellRoot {
614
614
  inputOffset,
615
615
  editorOffset: inputOffset + (body?.rowOffset ?? 0),
616
616
  inputRows: body?.rowCount ?? input.length,
617
+ footerRows: footer.length,
617
618
  };
618
619
  }
619
620
  #renderCopyAcknowledgement(width) {
@@ -3,7 +3,7 @@ import { createResponseCopyExecutor, hasAsyncClipboardOutput, responseCopyDestin
3
3
  import { MAX_COPY_CONTROL_BYTES } from "./response-copy-protocol.js";
4
4
  import { PromptHistoryController } from "./prompt-history-controller.js";
5
5
  import { PRODUCT_TEXT } from "../../product-identity.js";
6
- import { boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
6
+ import { CLEAR_EXIT_INTERVAL_MS, boundedCleanup } from "../../foundation/terminal-cleanup/terminal-reset.js";
7
7
  import { assertOwnedUiCommand, OWNED_UI_HIDDEN_COMMAND_NAMES } from "../../contracts/owned-ui/index.js";
8
8
  import { assertPromptImages, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
9
9
  import { ContextualPromptSuggestionController } from "./prompt-suggestion-controller.js";
@@ -34,6 +34,7 @@ export class OwnedUiSessionPresenter {
34
34
  #host;
35
35
  #terminal;
36
36
  #cwd;
37
+ #residentTabOptions;
37
38
  #listeners = new Set();
38
39
  #unsubscribe;
39
40
  #unsubscribePromptSuggestions;
@@ -53,6 +54,9 @@ export class OwnedUiSessionPresenter {
53
54
  #routeHost;
54
55
  #startupRoute;
55
56
  #startupRouteTimer;
57
+ #residentProgressTimer;
58
+ #residentWorking = false;
59
+ #residentStalled = false;
56
60
  #dialogHandle;
57
61
  #sequence = 0;
58
62
  #editorRevision = 0;
@@ -104,6 +108,7 @@ export class OwnedUiSessionPresenter {
104
108
  this.#sessionBindingGeneration = this.backend.identity.sessionBindingGeneration;
105
109
  this.#suggestionModelKey = modelKey(this.backend.session.view());
106
110
  this.#cwd = cwd;
111
+ this.#residentTabOptions = options.engine.residentTab;
107
112
  this.#routeHost = routeHost ?? null;
108
113
  this.#startupRoute = options.engine.startupRoute;
109
114
  this.#customViewport = sessionLayout === "custom-viewport";
@@ -202,6 +207,7 @@ export class OwnedUiSessionPresenter {
202
207
  onDequeue: () => this.restoreQueuedInput(),
203
208
  onEditorChange: text => {
204
209
  this.#editorRevision++;
210
+ this.#residentTabOptions?.checkpoint?.(text);
205
211
  promptSuggestionController?.abortPending();
206
212
  if (text.length === 0) {
207
213
  // Concurrency: the editor synchronizes deletion-owned autocomplete after onChange.
@@ -218,9 +224,9 @@ export class OwnedUiSessionPresenter {
218
224
  promptSuggestionController?.invalidate();
219
225
  this.#promptHistory?.synchronize();
220
226
  },
221
- onCopyText: text => {
227
+ onCopyText: (text, cut) => {
222
228
  if (this.#responseCopy !== null) {
223
- void this.#responseCopy.submitText(text);
229
+ this.#trackCopy(this.#responseCopy.submitText(text), cut ? undefined : text.length);
224
230
  return;
225
231
  }
226
232
  const write = () => {
@@ -263,6 +269,7 @@ export class OwnedUiSessionPresenter {
263
269
  // Invariant: the presenter owns its root (theme subscription and helper clients) and disposes it once the
264
270
  // terminal no longer renders it.
265
271
  constructed.push(() => this.root.dispose());
272
+ this.#restoreResidentInput();
266
273
  const initialPiSettings = this.backend.settings.snapshot();
267
274
  this.#programStatus = new ProgramStatusReporter(status => this.#terminal.setProgramStatus(status), () => this.view().status.footer?.sessionName ?? undefined);
268
275
  this.#reloadPresentation = reloadPresentation;
@@ -474,6 +481,7 @@ export class OwnedUiSessionPresenter {
474
481
  this.#settleStoppedLifecycle();
475
482
  return;
476
483
  }
484
+ this.#observeResidentProgress(event);
477
485
  // Performance: a streamed chunk names one block, and touching only that block is what keeps the
478
486
  // cost of a chunk the same in a long session as in a new one. Everything else
479
487
  // resynchronizes the view, which is cheap next to re-reading the transcript.
@@ -626,21 +634,21 @@ export class OwnedUiSessionPresenter {
626
634
  const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.#terminal.hasFocusedOverlay());
627
635
  if (routed.copySelection !== undefined) {
628
636
  const snapshot = routed.copySelection;
629
- const acknowledgement = copyAcknowledgement(snapshot);
630
- const intent = ++this.#copyIntentSequence;
631
- void this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite).then(result => {
632
- if (this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
633
- || result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
634
- return;
635
- if (acknowledgement.hasNonWhitespace) {
636
- this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
637
- }
638
- });
637
+ this.#trackCopy(this.#responseCopy?.submit(snapshot, this.#pendingClipboardWrite), copyCharacters(snapshot));
639
638
  }
640
639
  if (!routed.consumed)
641
640
  return routed.data === data ? undefined : { data: routed.data };
642
641
  return routed.data.length === 0 ? { consume: true } : { data: routed.data };
643
642
  }
643
+ #trackCopy(delivery, count) {
644
+ const intent = ++this.#copyIntentSequence;
645
+ void delivery?.then(({ outcome }) => {
646
+ if (count === undefined || this.#disposed || !this.#terminal.running || intent !== this.#copyIntentSequence
647
+ || outcome !== "delivered" && outcome !== "submitted-unverified")
648
+ return;
649
+ this.root.showCopyAcknowledgement(`copied ${count} chars to clipboard`);
650
+ });
651
+ }
644
652
  #promptSuggestionPresentationBlockReason(identity) {
645
653
  const view = this.view();
646
654
  if (this.#disposed)
@@ -779,15 +787,38 @@ export class OwnedUiSessionPresenter {
779
787
  const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
780
788
  this.#rememberInput(this.root.omitUnsentPromptImages(displayInput).trim(), type);
781
789
  this.root.resumeViewportFollowing();
782
- const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
790
+ const command = {
791
+ type,
792
+ correlationId: this.#correlation(type),
793
+ sessionId: this.backend.identity.sessionId,
794
+ text: input,
795
+ ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
796
+ };
797
+ let submission;
783
798
  try {
784
- return await this.#execute({
785
- type,
786
- correlationId: this.#correlation(type),
787
- sessionId: this.backend.identity.sessionId,
799
+ submission = await this.#residentTabOptions?.admit?.({
800
+ sessionId: command.sessionId,
801
+ sessionFile: this.#currentSessionFile(),
802
+ kind: type,
788
803
  text: input,
789
804
  ...(prepared.images.length === 0 ? {} : { images: prepared.images }),
790
- }, displayInput);
805
+ });
806
+ }
807
+ catch {
808
+ if (this.root.editor.getText().length === 0)
809
+ this.root.editor.setText(displayInput);
810
+ const message = "Prompt was not sent because its recovery journal could not be synchronized.";
811
+ this.#reportSubmissionError(undefined, message);
812
+ return rejected(message);
813
+ }
814
+ const finishSending = prepared.images.length === 0 ? () => { } : this.root.beginImageSubmissionStatus();
815
+ try {
816
+ const result = await this.#execute(command, displayInput);
817
+ if (submission !== undefined && result.outcome !== "rejected") {
818
+ this.#residentTabOptions?.dispatched?.();
819
+ await this.#residentTabOptions?.correlated?.(submission.id, command.correlationId).catch(() => undefined);
820
+ }
821
+ return result;
791
822
  }
792
823
  finally {
793
824
  finishSending();
@@ -795,7 +826,7 @@ export class OwnedUiSessionPresenter {
795
826
  }
796
827
  async clearOrExit(now = Date.now()) {
797
828
  this.#promptSuggestions?.abortPending();
798
- if (now - this.#lastClearTime < 500)
829
+ if (now - this.#lastClearTime < CLEAR_EXIT_INTERVAL_MS)
799
830
  return this.shutdown();
800
831
  this.root.editor.setText("");
801
832
  this.#lastClearTime = now;
@@ -1149,10 +1180,23 @@ export class OwnedUiSessionPresenter {
1149
1180
  this.#presenters.openSettingsSelector(this.#dialogHost);
1150
1181
  }
1151
1182
  shutdown() {
1152
- this.#shutdownPromise ??= this.#shutdown();
1183
+ this.#shutdownPromise ??= this.#shutdown().finally(() => {
1184
+ // Invariant: a rejected resident detach is retryable; ordinary shutdown has disposed the shell already.
1185
+ if (!this.#disposed)
1186
+ this.#shutdownPromise = undefined;
1187
+ });
1153
1188
  return this.#shutdownPromise;
1154
1189
  }
1155
1190
  async #shutdown() {
1191
+ const residentTab = this.#residentTab();
1192
+ if (residentTab !== undefined) {
1193
+ const detached = await residentTab.requestDetach().catch(() => false);
1194
+ if (detached)
1195
+ return { outcome: "completed", diagnostic: null };
1196
+ this.root.appendWorkflowStatus(residentTab.unavailableNotice ?? "Press Ctrl+C twice to leave; this agent keeps running.");
1197
+ this.#terminal.requestRender();
1198
+ return rejected("resident tab detach is unavailable");
1199
+ }
1156
1200
  try {
1157
1201
  const quit = this.backend.workflows.executeWorkflow({ command: "quit", argument: "" });
1158
1202
  // Invariant: a slow or hung engine quit never holds the terminal past the cleanup deadline. The
@@ -1168,6 +1212,75 @@ export class OwnedUiSessionPresenter {
1168
1212
  this.#resolveStoppedLifecycle();
1169
1213
  }
1170
1214
  }
1215
+ #restoreResidentInput() {
1216
+ const recovered = this.#residentTabOptions?.recovered;
1217
+ if (recovered === undefined)
1218
+ return;
1219
+ const prompts = recovered.prompts.map(prompt => prompt.text).filter(Boolean);
1220
+ if (prompts.length > 0) {
1221
+ this.root.editor.setText(prompts[0]);
1222
+ if (recovered.draft)
1223
+ this.root.editor.addToHistory(recovered.draft);
1224
+ for (const additional of prompts.slice(1))
1225
+ this.root.editor.addToHistory(additional);
1226
+ this.root.appendWorkflowStatus(recovered.draft
1227
+ ? `Recovered an unfinished prompt; the prior draft and ${Math.max(0, prompts.length - 1)} additional prompt${prompts.length === 2 ? "" : "s"} are in editor history.`
1228
+ : `Recovered an unfinished prompt${prompts.length > 1 ? `; ${prompts.length - 1} additional prompts are in editor history` : ""}.`);
1229
+ }
1230
+ else if (recovered.draft) {
1231
+ this.root.editor.setText(recovered.draft);
1232
+ this.root.appendWorkflowStatus("Recovered the editor draft from the previous tab process.");
1233
+ }
1234
+ }
1235
+ #currentSessionFile() {
1236
+ const direct = this.backend.identity.currentSessionFile?.();
1237
+ if (direct !== undefined && direct !== null)
1238
+ return direct;
1239
+ const resume = this.backend.identity.currentSessionResumeMetadata();
1240
+ return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
1241
+ }
1242
+ #residentTab() {
1243
+ return this.#residentTabOptions;
1244
+ }
1245
+ #observeResidentProgress(event) {
1246
+ const resident = this.#residentTabOptions;
1247
+ const seconds = resident?.stallNoticeSeconds;
1248
+ if (resident === undefined || seconds === undefined)
1249
+ return;
1250
+ if (event.type === "session-lifecycle") {
1251
+ this.#residentWorking = event.lifecycle === "busy";
1252
+ if (!this.#residentWorking) {
1253
+ if (this.#residentProgressTimer !== undefined)
1254
+ clearTimeout(this.#residentProgressTimer);
1255
+ this.#residentProgressTimer = undefined;
1256
+ this.#residentStalled = false;
1257
+ resident.reportStatus?.("idle");
1258
+ return;
1259
+ }
1260
+ }
1261
+ else if (!this.#residentWorking || ![
1262
+ "transcript-block", "session-view", "assistant-message-completed", "agent-run-started",
1263
+ ].includes(event.type)) {
1264
+ return;
1265
+ }
1266
+ if (this.#residentStalled) {
1267
+ this.#residentStalled = false;
1268
+ resident.reportStatus?.("working");
1269
+ }
1270
+ if (this.#residentProgressTimer !== undefined)
1271
+ clearTimeout(this.#residentProgressTimer);
1272
+ this.#residentProgressTimer = setTimeout(() => {
1273
+ this.#residentProgressTimer = undefined;
1274
+ if (this.#disposed || !this.#residentWorking)
1275
+ return;
1276
+ this.#residentStalled = true;
1277
+ resident.reportStatus?.("stalled");
1278
+ 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.#terminal.requestRender();
1281
+ }, seconds * 1_000);
1282
+ this.#residentProgressTimer.unref?.();
1283
+ }
1171
1284
  async runWorkflow(request) {
1172
1285
  if (request.command === "quit")
1173
1286
  return this.shutdown();
@@ -1425,6 +1538,9 @@ export class OwnedUiSessionPresenter {
1425
1538
  if (this.#startupRouteTimer !== undefined)
1426
1539
  clearTimeout(this.#startupRouteTimer);
1427
1540
  this.#startupRouteTimer = undefined;
1541
+ if (this.#residentProgressTimer !== undefined)
1542
+ clearTimeout(this.#residentProgressTimer);
1543
+ this.#residentProgressTimer = undefined;
1428
1544
  });
1429
1545
  attempt(() => this.#promptSuggestions?.dispose());
1430
1546
  attempt(() => this.#unsubscribePromptSuggestions());
@@ -1461,6 +1577,8 @@ export class OwnedUiSessionPresenter {
1461
1577
  }
1462
1578
  await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
1463
1579
  await boundedCleanup(() => pasteCleanup).catch(error => settled.push(error));
1580
+ await boundedCleanup(() => this.#residentTabOptions?.gracefulStop?.(this.#currentSessionFile()) ?? Promise.resolve())
1581
+ .catch(error => settled.push(error));
1464
1582
  await boundedCleanup(() => this.backend.extensions.unbindExtensionUi()).catch(error => settled.push(error));
1465
1583
  return settled;
1466
1584
  };
@@ -1650,6 +1768,12 @@ export class OwnedUiSessionPresenter {
1650
1768
  const argument = separator < 0 ? "" : body.slice(separator + 1).trimStart();
1651
1769
  if (this.#routeHost?.claims(name))
1652
1770
  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
+ }
1653
1777
  if (this.#isWorkflowRoute(name))
1654
1778
  return this.runWorkflow({ command: name, argument });
1655
1779
  if (this.#skillsCollapsed()) {
@@ -1920,16 +2044,15 @@ export class OwnedUiSessionShell extends OwnedUiSessionPresenter {
1920
2044
  function isCoalescedStreamBlock(kind) {
1921
2045
  return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
1922
2046
  }
1923
- function copyAcknowledgement(snapshot) {
2047
+ function copyCharacters(snapshot) {
1924
2048
  if (snapshot.literal !== true)
1925
- return { characters: snapshot.sourceUnits, hasNonWhitespace: true };
1926
- let characters = Math.max(0, snapshot.rows.length - 1);
1927
- let hasNonWhitespace = false;
1928
- for (const row of snapshot.rows) {
1929
- characters += row.text.length;
1930
- hasNonWhitespace ||= /\S/u.test(row.text);
1931
- }
1932
- return { characters, hasNonWhitespace };
2049
+ return snapshot.sourceUnits;
2050
+ let count = Math.max(0, snapshot.rows.length - 1), hasContent = false;
2051
+ for (const { text } of snapshot.rows) {
2052
+ count += text.length;
2053
+ hasContent ||= /\S/u.test(text);
2054
+ }
2055
+ return hasContent ? count : undefined;
1933
2056
  }
1934
2057
  export function formatSessionResumeCommand(metadata) {
1935
2058
  const args = [PRODUCT_TEXT.commandName];
@@ -64,7 +64,7 @@ export declare class SessionViewportController {
64
64
  setOverlaySurfaces(surfaces: readonly PiTuiPointerSurface[] | null): void;
65
65
  get inputGeometryReady(): boolean;
66
66
  compose(input: Omit<TranscriptViewportFrameInput, "pointerPosition">): TranscriptViewportFrame;
67
- composeDockOnly(dockRows: readonly string[], width: number, height: number): TranscriptViewportFrame | null;
67
+ composeDockOnly(dockRows: readonly string[], width: number, height: number, explicitCopyDockRows?: number): TranscriptViewportFrame | null;
68
68
  /** Returns true when document wrapping may have changed. */
69
69
  setConfig(config: OwnedUiViewportSettings): boolean;
70
70
  resumeFollowing(): void;
@@ -165,8 +165,8 @@ export class SessionViewportController {
165
165
  }
166
166
  return frame;
167
167
  }
168
- composeDockOnly(dockRows, width, height) {
169
- return this.#viewport.composeDockOnly(dockRows, width, height);
168
+ composeDockOnly(dockRows, width, height, explicitCopyDockRows = 0) {
169
+ return this.#viewport.composeDockOnly(dockRows, width, height, explicitCopyDockRows);
170
170
  }
171
171
  /** Returns true when document wrapping may have changed. */
172
172
  setConfig(config) {
@@ -307,7 +307,7 @@ export class SessionViewportController {
307
307
  this.#requestRender();
308
308
  }
309
309
  else if (this.#viewport.hasSelection) {
310
- const copySelection = this.#viewport.captureSelectedText();
310
+ const copySelection = this.#viewport.captureSelectedText("explicit");
311
311
  this.#viewport.clearSelection();
312
312
  this.#stopSelectionAutoScroll();
313
313
  this.#requestRender();
@@ -10,6 +10,8 @@ import type { OwnedUiApplicationPort, PresentationTerminalPort } from "../contra
10
10
  import type { AgentSettingsPort } from "../contracts/agent-engine/index.js";
11
11
  import type { OwnedUiPromptSuggestionGeneratorPort, UiAccentColor } from "../contracts/owned-ui/index.js";
12
12
  import type { ReleaseNoteCatalog } from "../features/owned-ui/release-notes.js";
13
+ import type { TabBridgeClient } from "../foundation/resident-tabs/bridge-client.js";
14
+ import { ResidentTabRecovery } from "../foundation/resident-tabs/recovery.js";
13
15
  /**
14
16
  * The session backend composition wires: the owned-UI port, the prompt-suggestion generator, and
15
17
  * the agent settings port. `settingsPort` stays outside the owned-UI contract because its type
@@ -55,11 +57,29 @@ export interface OwnedUiCompositionOptions {
55
57
  readonly checkForNewerRelease?: (options: StartupReleaseCheckOptions) => Promise<AvailableRelease | null>;
56
58
  /** Exact native process inspection injected by the shipped entry; absent SDK seams do not publish claims. */
57
59
  readonly inspectProcess?: (pid: number) => Promise<NativeProcessIdentity | null>;
60
+ /** Private credentials were consumed by the entry before composition or extensions loaded. */
61
+ readonly tab?: {
62
+ readonly bridge: TabBridgeClient;
63
+ readonly dataDir: string;
64
+ readonly incarnation: number;
65
+ };
66
+ /** One prompt-adjacent notice explaining why an opted-in resident launch fell back. */
67
+ readonly residentFallbackNotice?: string;
58
68
  }
59
69
  export interface OwnedUiComposition {
60
70
  readonly application: OwnedUiApplicationPort;
61
71
  /** Present when a profile was supplied, so the caller can resolve settings before start. */
62
72
  readonly settings: OwnedSettingsManager | null;
63
73
  }
74
+ export declare function residentStartupPresentation(resident: boolean, fallbackNotice?: string): {
75
+ readonly startup?: {
76
+ readonly quiet?: boolean;
77
+ readonly notices?: readonly [{
78
+ readonly kind: "warning";
79
+ readonly message: string;
80
+ }];
81
+ };
82
+ };
64
83
  export declare function composeOwnedUiApplication(options?: OwnedUiCompositionOptions): Promise<OwnedUiApplicationPort>;
65
84
  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>;