@timurproko/a1 0.2.7-dev.820 → 0.2.7-dev.828

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 (157) hide show
  1. package/README.md +7 -1
  2. package/bin/automatic-update.js +7 -0
  3. package/bin/tab-launch-context.d.ts +1 -0
  4. package/bin/tab-launch-context.js +3 -3
  5. package/bin/ui.js +96 -66
  6. package/dist/app/session-shell/session-shell-root.d.ts +13 -6
  7. package/dist/app/session-shell/session-shell-root.js +9 -5
  8. package/dist/app/session-shell/session-shell.js +68 -18
  9. package/dist/app/session-shell/shell-host.js +6 -3
  10. package/dist/app/session-shell/transcript-document.js +1 -1
  11. package/dist/cli/tabs.d.ts +1 -1
  12. package/dist/cli/tabs.js +10 -14
  13. package/dist/composition/owned-settings.d.ts +12 -0
  14. package/dist/composition/owned-settings.js +36 -0
  15. package/dist/composition/owned-ui.d.ts +5 -14
  16. package/dist/composition/owned-ui.js +93 -63
  17. package/dist/composition/resident-tab-bridge.d.ts +10 -1
  18. package/dist/composition/resident-tab-bridge.js +21 -0
  19. package/dist/composition/settings-route-host.d.ts +12 -2
  20. package/dist/composition/settings-route-host.js +27 -9
  21. package/dist/composition/settings-search-input.d.ts +9 -0
  22. package/dist/composition/settings-search-input.js +58 -0
  23. package/dist/composition/utility-tab.d.ts +28 -0
  24. package/dist/composition/utility-tab.js +242 -0
  25. package/dist/contracts/owned-ui/session-backend.d.ts +2 -0
  26. package/dist/contracts/owned-ui/session-workflows.d.ts +7 -3
  27. package/dist/contracts/presentation/index.d.ts +11 -0
  28. package/dist/contracts/presentation/index.js +12 -0
  29. package/dist/features/launch/development-launch.js +6 -0
  30. package/dist/features/launch/profile-paths.js +4 -0
  31. package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
  32. package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
  33. package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
  34. package/dist/features/owned-ui/project-trust-dialog.js +6 -8
  35. package/dist/features/owned-ui/project-trust-prompt.d.ts +1 -0
  36. package/dist/features/owned-ui/project-trust-prompt.js +7 -3
  37. package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
  38. package/dist/features/owned-ui/reference-screen-app.js +12 -2
  39. package/dist/features/owned-ui/settings-app.d.ts +10 -3
  40. package/dist/features/owned-ui/settings-app.js +98 -45
  41. package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
  42. package/dist/features/owned-ui/settings-search-input.js +1 -0
  43. package/dist/features/prompt-history/index.d.ts +1 -1
  44. package/dist/features/prompt-history/paths.d.ts +2 -1
  45. package/dist/features/prompt-history/paths.js +10 -2
  46. package/dist/features/prompt-history/service.d.ts +5 -0
  47. package/dist/features/prompt-history/service.js +1 -1
  48. package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
  49. package/dist/features/resident-tab/shortcut-declarations.js +2 -0
  50. package/dist/foundation/launch-context/index.d.ts +2 -0
  51. package/dist/foundation/launch-context/index.js +1 -0
  52. package/dist/foundation/lifecycle/session-repository-context.js +16 -2
  53. package/dist/foundation/release/automatic-update.d.ts +37 -0
  54. package/dist/foundation/release/automatic-update.js +265 -0
  55. package/dist/foundation/release/bootstrap.d.ts +1 -9
  56. package/dist/foundation/release/bootstrap.js +7 -3
  57. package/dist/foundation/release/early-accent.d.ts +12 -0
  58. package/dist/foundation/release/early-accent.js +59 -0
  59. package/dist/foundation/release/index.d.ts +1 -1
  60. package/dist/foundation/release/index.js +1 -1
  61. package/dist/foundation/release/latest-release.d.ts +3 -2
  62. package/dist/foundation/release/latest-release.js +1 -1
  63. package/dist/foundation/release/progress-palette.generated.d.ts +137 -3
  64. package/dist/foundation/release/progress-palette.generated.js +137 -3
  65. package/dist/foundation/release/release-gc.js +19 -4
  66. package/dist/foundation/release/update-execution-lease.d.ts +36 -0
  67. package/dist/foundation/release/update-execution-lease.js +145 -0
  68. package/dist/foundation/release/update.d.ts +9 -7
  69. package/dist/foundation/release/update.js +244 -175
  70. package/dist/foundation/resident-tabs/bridge-client.d.ts +9 -2
  71. package/dist/foundation/resident-tabs/bridge-client.js +19 -7
  72. package/dist/foundation/resident-tabs/maintenance.d.ts +2 -6
  73. package/dist/foundation/resident-tabs/maintenance.js +5 -23
  74. package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
  75. package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
  76. package/dist/foundation/resident-tabs/progress-observation.d.ts +3 -6
  77. package/dist/foundation/resident-tabs/progress-observation.js +43 -12
  78. package/dist/foundation/resident-tabs/release-holds.js +3 -14
  79. package/dist/foundation/resident-tabs/routes.d.ts +5 -0
  80. package/dist/foundation/resident-tabs/routes.js +5 -0
  81. package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
  82. package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
  83. package/dist/integrations/pi/components/modal-frame.js +48 -9
  84. package/dist/integrations/pi/components/models-dialog.js +4 -52
  85. package/dist/integrations/pi/components/owned-editor-ux.d.ts +3 -0
  86. package/dist/integrations/pi/components/owned-editor-ux.js +75 -4
  87. package/dist/integrations/pi/components/shell-editor-autocomplete.js +57 -37
  88. package/dist/integrations/pi/components/shell-footer-status.js +19 -3
  89. package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
  90. package/dist/integrations/pi/components/shell-selectors-dialogs.js +4 -1
  91. package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
  92. package/dist/integrations/pi/components/skills-dialog.js +2 -2
  93. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -5
  94. package/dist/integrations/pi/components/upstream/components/owned-editor.js +15 -8
  95. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -3
  96. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
  97. package/dist/integrations/pi/components/upstream/components/session-footer.js +26 -23
  98. package/dist/integrations/pi/components/upstream/components/session-selector.js +7 -6
  99. package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
  100. package/dist/integrations/pi/components/upstream/components/thinking-selector.js +9 -8
  101. package/dist/integrations/pi/components/upstream/components/tool-execution.js +6 -4
  102. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +8 -8
  103. package/dist/integrations/pi/components/upstream/components/tree-selector.js +10 -10
  104. package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -0
  105. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +2 -0
  106. package/dist/integrations/pi/components/upstream/theme/theme.js +5 -1
  107. package/dist/integrations/pi/engine/adapter.js +7 -2
  108. package/dist/integrations/pi/engine/offline-settings-port.d.ts +23 -0
  109. package/dist/integrations/pi/engine/offline-settings-port.js +53 -0
  110. package/dist/integrations/pi/engine/ports.d.ts +1 -1
  111. package/dist/integrations/pi/engine/ports.js +4 -0
  112. package/dist/integrations/pi/engine/settings-bridge.d.ts +5 -0
  113. package/dist/integrations/pi/engine/settings-bridge.js +13 -0
  114. package/dist/integrations/pi/engine/settings-port.js +1 -14
  115. package/dist/integrations/pi/engine/workflow-runner.js +9 -2
  116. package/dist/native/darwin-arm64/manifest.json +1 -1
  117. package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
  118. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  119. package/dist/native/darwin-x64/manifest.json +1 -1
  120. package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
  121. package/dist/native/darwin-x64/terminal-host.node +0 -0
  122. package/dist/native/linux-x64/manifest.json +1 -1
  123. package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
  124. package/dist/native/linux-x64/terminal-host.node +0 -0
  125. package/dist/native/win32-x64/manifest.json +2 -2
  126. package/dist/native/win32-x64/process-guardian.node +0 -0
  127. package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
  128. package/dist/native/win32-x64/terminal-host.node +0 -0
  129. package/dist/product-identity.d.ts +1 -1
  130. package/dist/product-identity.js +2 -2
  131. package/dist/product-identity.json +2 -0
  132. package/dist/ui/apps/host.d.ts +2 -0
  133. package/dist/ui/apps/host.js +7 -1
  134. package/dist/ui/components/dialog-panel.js +35 -30
  135. package/dist/ui/components/line-input.d.ts +3 -0
  136. package/dist/ui/components/line-input.js +3 -1
  137. package/dist/ui/components/prompt-input.d.ts +3 -2
  138. package/dist/ui/components/prompt-input.js +6 -4
  139. package/dist/ui/components/spans.js +2 -2
  140. package/dist/ui/components/surface.js +4 -2
  141. package/dist/ui/components/text.d.ts +3 -6
  142. package/dist/ui/components/text.js +6 -8
  143. package/dist/ui/settings/declarations.d.ts +56 -10
  144. package/dist/ui/settings/declarations.js +38 -9
  145. package/dist/ui/settings/early-accent.d.ts +81 -0
  146. package/dist/ui/settings/early-accent.js +50 -0
  147. package/dist/ui/settings/manager.js +7 -3
  148. package/dist/ui/settings/migrations.js +15 -0
  149. package/dist/ui/settings/progress-palette.generated.d.ts +139 -0
  150. package/dist/ui/settings/progress-palette.generated.js +139 -0
  151. package/dist/ui/settings/sections.d.ts +4 -0
  152. package/dist/ui/settings/sections.js +9 -3
  153. package/docs/architecture/resident-tabs.md +48 -17
  154. package/docs/architecture/tool-image-presentation.md +2 -1
  155. package/docs/features/prompt-history.md +41 -20
  156. package/docs/validation.md +25 -5
  157. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import crossSpawn from "cross-spawn";
6
6
  import { valid as validSemver } from "semver";
7
7
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
8
+ import { earlyAccentPalette, earlyTerminalAppearance, earlyTerminalColorMode, resolveEarlyAccentColor } from "./early-accent.js";
8
9
  import { SHARED_PROGRESS_ACCENT_ANSI } from "./progress-palette.generated.js";
9
10
  import { isNewerRelease, releaseChannelOf } from "./latest-release.js";
10
11
  import { certifyMaterializedRelease, ensureSupervisor, waitForProcessExit, } from "./bootstrap.js";
@@ -17,6 +18,7 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
17
18
  import { scheduleReleaseCleanup } from "./release-gc.js";
18
19
  import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
19
20
  import { UpdateTransactionStore } from "./update-transaction.js";
21
+ import { acquireUpdateExecutionLease } from "./update-execution-lease.js";
20
22
  import { npmPrefixForGlobalRoot, removeUpdateRecoveryCapsule, resolveNpmCli, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
21
23
  export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
22
24
  const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
@@ -34,6 +36,7 @@ const defaultFileSystem = {
34
36
  * The update therefore keeps checking for this long before it calls the package locked.
35
37
  */
36
38
  export const PACKAGE_UNLOCK_PATIENCE_MS = 15_000;
39
+ export const UPDATE_ALREADY_RUNNING_EXIT_CODE = 75;
37
40
  const PACKAGE_UNLOCK_INITIAL_DELAY_MS = 100;
38
41
  const PACKAGE_UNLOCK_MAX_DELAY_MS = 1_000;
39
42
  // Platform: Windows reports a held directory as EPERM or EBUSY; EACCES and ENOTEMPTY are the other
@@ -85,6 +88,21 @@ export function createNpmProcessRunner(platform = process.platform) {
85
88
  * its own endpoint. A cohort running from the mutable installation is the one exception,
86
89
  * because that installation is exactly what is about to be replaced under it.
87
90
  */
91
+ async function verifyAutomaticUpdateSafety(paths, stateStore, environment) {
92
+ const state = await stateStore.read();
93
+ const owner = await readActiveEndpoint(paths, state.references.active, environment);
94
+ if (!owner)
95
+ return;
96
+ const ownership = await probeOwnership(owner.metadata);
97
+ if (ownership === "dead")
98
+ return;
99
+ if (ownership !== "live-verified") {
100
+ throw new UpdateAutomaticDeferred(PRODUCT_TEXT.diagnostic(`automatic update deferred because supervisor ownership is ${ownership}`));
101
+ }
102
+ if (!await canonicalImmutableRoot(paths.dataDir, owner.metadata.releaseRoot)) {
103
+ throw new UpdateAutomaticDeferred(PRODUCT_TEXT.diagnostic("automatic update deferred because the running session is not using a retained immutable release; run the update command manually"));
104
+ }
105
+ }
88
106
  export function planUpdateOwnership(ownership, runsFromRetainedRelease) {
89
107
  if (ownership === "dead")
90
108
  return "clean-dead-record";
@@ -111,13 +129,13 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
111
129
  const stateStore = new CohortStateStore(paths.dataDir);
112
130
  return {
113
131
  async targetIsActive(targetVersion) {
114
- const state = await stateStore.read();
115
- const activeId = state.references.active;
132
+ const state = await stateStore.read(), activeId = state.references.active;
116
133
  if (!activeId || state.releases[activeId]?.packageVersion !== targetVersion)
117
134
  return false;
118
135
  const endpoint = await readActiveEndpoint(paths, activeId, environment);
119
136
  return endpoint?.metadata.releaseId === activeId && await probeOwnership(endpoint.metadata) === "live-verified";
120
137
  },
138
+ async verifyAutomaticSafety() { await verifyAutomaticUpdateSafety(paths, stateStore, environment); },
121
139
  async shutdownVerifiedOwners(targetVersion) {
122
140
  const state = await stateStore.read();
123
141
  const activeId = state.references.active;
@@ -233,16 +251,19 @@ const ACTIVATION_PROGRESS = {
233
251
  "active-reference-committed": { at: 96, creepTo: 99 },
234
252
  };
235
253
  /** Renders the terminal-only update meter; the visual preview shares this exact frame. */
236
- export function renderUpdateProgressBar(percent) {
254
+ export function renderUpdateProgressBar(percent, accent = SHARED_PROGRESS_ACCENT_ANSI) {
237
255
  const bounded = Math.min(100, Math.max(0, Math.round(percent)));
238
256
  const filled = Math.round((bounded / 100) * PROGRESS_BAR_WIDTH);
239
- // Rationale: the completed run follows the pinned Pi controls' semantic accent,
240
- // while the track and percentage retain the update meter's neutral treatment.
257
+ // Rationale: the completed run follows the A1 profile accent; the track and
258
+ // percentage retain the update meter's neutral treatment.
241
259
  const gray = "\u001b[38;2;128;128;128m";
242
260
  const track = "\u001b[38;2;102;102;102m";
243
- return `${SHARED_PROGRESS_ACCENT_ANSI}${"━".repeat(filled)}${track}${"─".repeat(PROGRESS_BAR_WIDTH - filled)}${gray} ${bounded}%\u001b[39m`;
261
+ return `${accent}${"━".repeat(filled)}${track}${"─".repeat(PROGRESS_BAR_WIDTH - filled)}${gray} ${bounded}%\u001b[39m`;
262
+ }
263
+ function updateProgressAccent(environment) {
264
+ return earlyAccentPalette(resolveEarlyAccentColor(environment), earlyTerminalAppearance(environment), earlyTerminalColorMode(environment)).accent;
244
265
  }
245
- function createUpdateProgress(output, enabled) {
266
+ function createUpdateProgress(output, enabled, accent = SHARED_PROGRESS_ACCENT_ANSI) {
246
267
  let visible = false;
247
268
  let current = 0;
248
269
  let shown = -1;
@@ -253,7 +274,7 @@ function createUpdateProgress(output, enabled) {
253
274
  return;
254
275
  shown = rounded;
255
276
  visible = true;
256
- output.stdout(`\r${renderUpdateProgressBar(rounded)}`);
277
+ output.stdout(`\r${renderUpdateProgressBar(rounded, accent)}`);
257
278
  };
258
279
  const stopCreep = () => {
259
280
  if (timer !== null) {
@@ -455,9 +476,58 @@ async function launchersOwnPackage(globalRoot, fileSystem) {
455
476
  return false;
456
477
  }
457
478
  }
479
+ async function resolveSelfUpdateTarget(input) {
480
+ if (input.exactTarget !== undefined && input.exactTarget.length > 0) {
481
+ return resolveExactAutomaticTarget(input.exactTarget, input.channel, input.output);
482
+ }
483
+ return input.requested === undefined || input.requested.length === 0
484
+ ? await resolveChannelHead(input.runner, input.distTag, input.output)
485
+ : await resolveRequestedPreview(input.runner, input.requested, input.output);
486
+ }
487
+ function resolveExactAutomaticTarget(target, channel, output) {
488
+ const version = validSemver(target);
489
+ const expected = channel === "stable" ? "stable" : "development";
490
+ if (version === null || releaseChannelOf(version) !== expected) {
491
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`refused invalid automatic ${expected} update target ${JSON.stringify(target)}`)}\n`);
492
+ return { version: null, exitCode: 1 };
493
+ }
494
+ return { version, exitCode: 0 };
495
+ }
458
496
  function reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot) {
459
497
  output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} could not be verified as an npm-managed ${PRODUCT_PACKAGE} installation; npm's active global package root is ${activeGlobalRoot}. Run npm install --global ${PRODUCT_PACKAGE} to install it manually.`)}\n`);
460
498
  }
499
+ async function withUpdateExecutionLease(input, options, output, operation) {
500
+ const lease = await acquireUpdateExecutionLease(input);
501
+ if (lease.status === "busy") {
502
+ options.onLeaseBusy?.(lease.owner);
503
+ if ((options.mode ?? "manual") === "manual") {
504
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`update already in progress for ${lease.owner.targetVersion}`)}\n`);
505
+ }
506
+ return UPDATE_ALREADY_RUNNING_EXIT_CODE;
507
+ }
508
+ try {
509
+ options.onLeaseAcquired?.(lease.owner);
510
+ return await operation();
511
+ }
512
+ finally {
513
+ await lease.release();
514
+ }
515
+ }
516
+ async function readRunningPackageVersion(packageRoot, fileSystem, output) {
517
+ try {
518
+ const packageJson = JSON.parse(await fileSystem.readFile(resolve(packageRoot, "package.json")));
519
+ const version = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
520
+ if (packageJson.name !== PRODUCT_PACKAGE)
521
+ throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
522
+ if (version === null)
523
+ throw new Error("package.json does not contain a valid semantic version");
524
+ return version;
525
+ }
526
+ catch (error) {
527
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
528
+ return null;
529
+ }
530
+ }
461
531
  export async function runSelfUpdate(options) {
462
532
  const fileSystem = options.fileSystem ?? defaultFileSystem;
463
533
  const output = options.output ?? defaultOutput;
@@ -474,24 +544,14 @@ export async function runSelfUpdate(options) {
474
544
  options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - startedAt) });
475
545
  }
476
546
  };
477
- let runningVersion;
478
- try {
479
- const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
480
- const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
481
- if (packageJson.name !== PRODUCT_PACKAGE)
482
- throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
483
- if (parsedVersion === null)
484
- throw new Error("package.json does not contain a valid semantic version");
485
- runningVersion = parsedVersion;
486
- }
487
- catch (error) {
488
- output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
547
+ const runningVersion = await measure("package-version", async () => await readRunningPackageVersion(options.packageRoot, fileSystem, output));
548
+ if (runningVersion === null)
489
549
  return 1;
490
- }
491
550
  const requested = options.target?.trim();
492
- const resolved = await measure("target-resolution", async () => requested === undefined || requested.length === 0
493
- ? await resolveChannelHead(runner, distTag, output)
494
- : await resolveRequestedPreview(runner, requested, output));
551
+ const exactTarget = options.resolvedTarget?.trim();
552
+ const resolved = await measure("target-resolution", async () => await resolveSelfUpdateTarget({
553
+ runner, output, channel, distTag, requested, exactTarget,
554
+ }));
495
555
  if (resolved.version === null)
496
556
  return resolved.exitCode;
497
557
  const targetVersion = resolved.version;
@@ -506,178 +566,185 @@ export async function runSelfUpdate(options) {
506
566
  // Rationale: no full stop after a version: it already ends in a dot-separated identifier,
507
567
  // and a trailing one reads as part of the version rather than as punctuation.
508
568
  output.stdout(`${PRODUCT_TEXT.commandName} update: ${runningVersion} → ${targetVersion}\n`);
509
- const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
569
+ const environment = options.environment ?? process.env;
570
+ const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true), updateProgressAccent(environment));
510
571
  const managed = await measure("global-root", async () => await resolveManagedNpmInstallation(options.packageRoot, fileSystem, runner, output));
511
572
  if (managed.installation === null)
512
573
  return managed.exitCode;
513
574
  const { packageRoot, globalRoot, npmCliRoot } = managed.installation;
514
- const environment = options.environment ?? process.env;
515
575
  const paths = resolveProductPaths(environment);
516
576
  const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem, output);
517
577
  const transactionStore = options.transactionStore ?? new UpdateTransactionStore(paths.dataDir);
578
+ const mode = options.mode ?? "manual";
518
579
  const maintenance = options.maintenance ?? (options.lifecycle
519
580
  ? async () => { }
520
581
  : async () => await scheduleReleaseCleanup(paths.dataDir, paths));
521
- let transaction = await transactionStore.read();
522
- let npmCli;
523
- try {
524
- if ((!transaction || transaction.status === "completed") && await lifecycle.targetIsActive(targetVersion)) {
525
- await maintenance();
526
- await transactionStore.clearCompleted();
527
- output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
528
- return 0;
529
- }
530
- const preflightRequired = options.npmCliResolver !== undefined
531
- || options.packageReplacement === undefined && options.runner === undefined;
532
- if (preflightRequired) {
533
- try {
534
- npmCli = await (options.npmCliResolver ?? resolveNpmCli)(npmCliRoot, environment);
582
+ return await withUpdateExecutionLease({ dataDir: paths.dataDir, packageRoot, channel, targetVersion, mode }, options, output, async () => {
583
+ let transaction = await transactionStore.read();
584
+ let npmCli;
585
+ try {
586
+ if (mode === "automatic")
587
+ await lifecycle.verifyAutomaticSafety?.();
588
+ if ((!transaction || transaction.status === "completed") && await lifecycle.targetIsActive(targetVersion)) {
589
+ await maintenance();
590
+ await transactionStore.clearCompleted();
591
+ output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
592
+ return 0;
535
593
  }
536
- catch (error) {
537
- throw new UpdatePreflightFailure(errorMessage(error));
594
+ const preflightRequired = options.npmCliResolver !== undefined
595
+ || options.packageReplacement === undefined && options.runner === undefined;
596
+ if (preflightRequired) {
597
+ try {
598
+ npmCli = await (options.npmCliResolver ?? resolveNpmCli)(npmCliRoot, environment);
599
+ }
600
+ catch (error) {
601
+ throw new UpdatePreflightFailure(errorMessage(error));
602
+ }
538
603
  }
539
- }
540
- // Rationale: the bar first appears only after every non-mutating replacement prerequisite passes.
541
- progress.set(3, 15);
542
- const cohortStore = new CohortStateStore(paths.dataDir);
543
- let cohortState = await cohortStore.read();
544
- let priorActiveReleaseId = cohortState.references.active;
545
- if (options.runner === undefined && (priorActiveReleaseId === null || cohortState.releases[priorActiveReleaseId]?.approval !== "approved")) {
546
- // Invariant: protected replacement needs an immutable launch authority that npm cannot rename.
547
- const recoveryRelease = await materializeRelease(packageRoot, paths.dataDir);
548
- await cohortStore.recordCandidate(recoveryRelease);
549
- const diagnostics = await certifyMaterializedRelease(recoveryRelease, paths.dataDir);
550
- await cohortStore.approve(recoveryRelease.releaseId, diagnostics);
551
- await cohortStore.activate(recoveryRelease.releaseId);
552
- cohortState = await cohortStore.read();
553
- priorActiveReleaseId = recoveryRelease.releaseId;
554
- }
555
- transaction = await transactionStore.begin({
556
- channel,
557
- targetVersion,
558
- packageRoot,
559
- priorActiveReleaseId,
560
- });
561
- if (phaseBefore(transaction.phase, "ownership-released")) {
562
- await measure("ownership-release", async () => {
563
- await lifecycle.shutdownVerifiedOwners(targetVersion);
564
- await lifecycle.verifyPackageUnlocked(packageRoot);
604
+ // Rationale: the bar first appears only after every non-mutating replacement prerequisite passes.
605
+ progress.set(3, 15);
606
+ const cohortStore = new CohortStateStore(paths.dataDir);
607
+ let cohortState = await cohortStore.read();
608
+ let priorActiveReleaseId = cohortState.references.active;
609
+ if (options.runner === undefined && (priorActiveReleaseId === null || cohortState.releases[priorActiveReleaseId]?.approval !== "approved")) {
610
+ // Invariant: protected replacement needs an immutable launch authority that npm cannot rename.
611
+ const recoveryRelease = await materializeRelease(packageRoot, paths.dataDir);
612
+ await cohortStore.recordCandidate(recoveryRelease);
613
+ const diagnostics = await certifyMaterializedRelease(recoveryRelease, paths.dataDir);
614
+ await cohortStore.approve(recoveryRelease.releaseId, diagnostics);
615
+ await cohortStore.activate(recoveryRelease.releaseId);
616
+ cohortState = await cohortStore.read();
617
+ priorActiveReleaseId = recoveryRelease.releaseId;
618
+ }
619
+ transaction = await transactionStore.begin({
620
+ channel,
621
+ targetVersion,
622
+ packageRoot,
623
+ priorActiveReleaseId,
565
624
  });
566
- transaction = await transactionStore.advance("ownership-released");
567
- }
568
- progress.set(15, 70);
569
- if (phaseBefore(transaction.phase, "package-installed")) {
570
- if (options.packageReplacement || options.runner === undefined) {
571
- const priorReleaseId = transaction.priorActiveReleaseId ?? priorActiveReleaseId;
572
- const recordedPrior = priorReleaseId === null ? undefined : cohortState.releases[priorReleaseId];
573
- if ((!recordedPrior || recordedPrior.approval !== "approved") && !options.packageReplacement) {
574
- throw new Error(PRODUCT_TEXT.diagnostic("cannot protect package replacement because no approved prior release is available"));
625
+ if (phaseBefore(transaction.phase, "ownership-released")) {
626
+ await measure("ownership-release", async () => {
627
+ await lifecycle.shutdownVerifiedOwners(targetVersion);
628
+ await lifecycle.verifyPackageUnlocked(packageRoot);
629
+ });
630
+ transaction = await transactionStore.advance("ownership-released");
631
+ }
632
+ progress.set(15, 70);
633
+ if (phaseBefore(transaction.phase, "package-installed")) {
634
+ if (options.packageReplacement || options.runner === undefined) {
635
+ const priorReleaseId = transaction.priorActiveReleaseId ?? priorActiveReleaseId;
636
+ const recordedPrior = priorReleaseId === null ? undefined : cohortState.releases[priorReleaseId];
637
+ if ((!recordedPrior || recordedPrior.approval !== "approved") && !options.packageReplacement) {
638
+ throw new Error(PRODUCT_TEXT.diagnostic("cannot protect package replacement because no approved prior release is available"));
639
+ }
640
+ const priorRelease = recordedPrior ?? {
641
+ releaseId: priorReleaseId ?? "injected-prior-release",
642
+ releaseRoot: packageRoot,
643
+ contentDigest: "0".repeat(64),
644
+ };
645
+ const replacementTransaction = transaction;
646
+ const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
647
+ dataDir: paths.dataDir,
648
+ globalRoot,
649
+ npmCliRoot,
650
+ ...(npmCli === undefined ? {} : { npmCli }),
651
+ packageRoot,
652
+ transaction: replacementTransaction,
653
+ priorRelease,
654
+ output,
655
+ environment,
656
+ onRecoveryState: async (recovery) => {
657
+ if (transactionStore.setRecovery)
658
+ transaction = await transactionStore.setRecovery(recovery) ?? transaction;
659
+ },
660
+ }));
661
+ // Rationale: npm's own text explains a failed replacement and says nothing about a
662
+ // successful one, so it reaches the terminal only when the installation did not happen.
663
+ if (replacement.outcome !== "installed")
664
+ reportChildDiagnostics(output, replacement);
665
+ if (replacement.outcome === "installed")
666
+ transaction = await transactionStore.advance("package-installed");
667
+ if (replacement.cancelled) {
668
+ progress.clear();
669
+ output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
670
+ return 130;
671
+ }
672
+ if (replacement.outcome !== "installed") {
673
+ throw new UpdateFailure(unsuccessfulCode(replacement.npmExitCode), `npm exited with status ${formatExitCode(replacement.npmExitCode)}`);
674
+ }
575
675
  }
576
- const priorRelease = recordedPrior ?? {
577
- releaseId: priorReleaseId ?? "injected-prior-release",
578
- releaseRoot: packageRoot,
579
- contentDigest: "0".repeat(64),
580
- };
581
- const replacementTransaction = transaction;
582
- const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
583
- dataDir: paths.dataDir,
584
- globalRoot,
585
- npmCliRoot,
586
- ...(npmCli === undefined ? {} : { npmCli }),
587
- packageRoot,
588
- transaction: replacementTransaction,
589
- priorRelease,
590
- output,
591
- environment,
592
- onRecoveryState: async (recovery) => {
593
- if (transactionStore.setRecovery)
594
- transaction = await transactionStore.setRecovery(recovery) ?? transaction;
595
- },
596
- }));
597
- // Rationale: npm's own text explains a failed replacement and says nothing about a
598
- // successful one, so it reaches the terminal only when the installation did not happen.
599
- if (replacement.outcome !== "installed")
600
- reportChildDiagnostics(output, replacement);
601
- if (replacement.outcome === "installed")
676
+ else {
677
+ const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
678
+ if (installation.result === null)
679
+ throw new UpdateFailure(installation.exitCode, "npm process failed");
680
+ if (installation.result.code !== 0) {
681
+ reportChildDiagnostics(output, installation.result);
682
+ throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
683
+ }
602
684
  transaction = await transactionStore.advance("package-installed");
603
- if (replacement.cancelled) {
604
- progress.clear();
605
- output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
606
- return 130;
607
- }
608
- if (replacement.outcome !== "installed") {
609
- throw new UpdateFailure(unsuccessfulCode(replacement.npmExitCode), `npm exited with status ${formatExitCode(replacement.npmExitCode)}`);
610
685
  }
611
686
  }
612
- else {
613
- const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
614
- if (installation.result === null)
615
- throw new UpdateFailure(installation.exitCode, "npm process failed");
616
- if (installation.result.code !== 0) {
617
- reportChildDiagnostics(output, installation.result);
618
- throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
687
+ progress.set(70, 75);
688
+ // Concurrency: ownership can be reacquired after an interrupted installation (for
689
+ // example, if bare A1 is launched before the update is resumed). Recheck
690
+ // immediately before activation so recovery cannot start a second cohort.
691
+ await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
692
+ progress.set(75, MATERIALIZE_PROGRESS.from);
693
+ let activationPhaseStartedAt = now();
694
+ await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
695
+ options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
696
+ transaction = await transactionStore.advance(phase);
697
+ progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
698
+ activationPhaseStartedAt = now();
699
+ }, ({ completed, total }) => {
700
+ const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
701
+ const done = total > 0 ? Math.min(1, completed / total) : 0;
702
+ progress.set(MATERIALIZE_PROGRESS.from + span * done);
703
+ }, state => {
704
+ if (state === "started") {
705
+ activationPhaseStartedAt = now();
706
+ progress.set(98, 99);
619
707
  }
620
- transaction = await transactionStore.advance("package-installed");
621
- }
708
+ else {
709
+ options.onPhaseTiming?.({ phase: "warmup", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
710
+ activationPhaseStartedAt = now();
711
+ progress.set(99);
712
+ }
713
+ });
714
+ options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
715
+ const transactionStartedAt = now();
716
+ await transactionStore.advance("supervisor-verified");
717
+ if (transaction.recovery?.capsulePath)
718
+ await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId);
719
+ // Invariant: successful output follows the durable cleanup disposition. Slow recursive
720
+ // removal belongs to the detached worker started by this maintenance coordinator.
721
+ await maintenance();
722
+ // Concurrency: this is the launch cutover. No failure-prone update work follows success.
723
+ await transactionStore.finish("completed");
724
+ await transactionStore.clearCompleted().catch(() => undefined);
725
+ options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
726
+ progress.finish();
727
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${targetVersion}\n`);
728
+ return 0;
622
729
  }
623
- progress.set(70, 75);
624
- // Concurrency: ownership can be reacquired after an interrupted installation (for
625
- // example, if bare A1 is launched before the update is resumed). Recheck
626
- // immediately before activation so recovery cannot start a second cohort.
627
- await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
628
- progress.set(75, MATERIALIZE_PROGRESS.from);
629
- let activationPhaseStartedAt = now();
630
- await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
631
- options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
632
- transaction = await transactionStore.advance(phase);
633
- progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
634
- activationPhaseStartedAt = now();
635
- }, ({ completed, total }) => {
636
- const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
637
- const done = total > 0 ? Math.min(1, completed / total) : 0;
638
- progress.set(MATERIALIZE_PROGRESS.from + span * done);
639
- }, state => {
640
- if (state === "started") {
641
- activationPhaseStartedAt = now();
642
- progress.set(98, 99);
643
- }
644
- else {
645
- options.onPhaseTiming?.({ phase: "warmup", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
646
- activationPhaseStartedAt = now();
647
- progress.set(99);
730
+ catch (error) {
731
+ progress.clear();
732
+ const message = errorMessage(error);
733
+ if (error instanceof UpdateAutomaticDeferred)
734
+ return 1;
735
+ if (error instanceof UpdatePreflightFailure) {
736
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
737
+ return 1;
648
738
  }
649
- });
650
- options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
651
- const transactionStartedAt = now();
652
- await transactionStore.advance("supervisor-verified");
653
- if (transaction.recovery?.capsulePath)
654
- await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId);
655
- // Invariant: successful output follows the durable cleanup disposition. Slow recursive
656
- // removal belongs to the detached worker started by this maintenance coordinator.
657
- await maintenance();
658
- // Concurrency: this is the launch cutover. No failure-prone update work follows success.
659
- await transactionStore.finish("completed");
660
- await transactionStore.clearCompleted().catch(() => undefined);
661
- options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
662
- progress.finish();
663
- output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${targetVersion}\n`);
664
- return 0;
665
- }
666
- catch (error) {
667
- progress.clear();
668
- const message = errorMessage(error);
669
- if (error instanceof UpdatePreflightFailure) {
670
- output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
671
- return 1;
739
+ const rollback = options.lifecycle
740
+ ? "previous test lifecycle retained"
741
+ : await rollbackPriorCohort(paths.dataDir, environment, transaction?.priorActiveReleaseId ?? null).catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
742
+ if (transaction)
743
+ await transactionStore.finish(rollback === "rolled back" ? "rolled-back" : "failed", `${message}; ${rollback}`);
744
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`update failed: ${message}. ${rollback}. Diagnostics: ${transactionStore.path}`)}\n`);
745
+ return error instanceof UpdateFailure ? error.exitCode : 1;
672
746
  }
673
- const rollback = options.lifecycle
674
- ? "previous test lifecycle retained"
675
- : await rollbackPriorCohort(paths.dataDir, environment, transaction?.priorActiveReleaseId ?? null).catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
676
- if (transaction)
677
- await transactionStore.finish(rollback === "rolled back" ? "rolled-back" : "failed", `${message}; ${rollback}`);
678
- output.stderr(`${PRODUCT_TEXT.diagnostic(`update failed: ${message}. ${rollback}. Diagnostics: ${transactionStore.path}`)}\n`);
679
- return error instanceof UpdateFailure ? error.exitCode : 1;
680
- }
747
+ });
681
748
  }
682
749
  export function assertUpdatePerformanceBudget(evidence, maximumPostNpmDurationMs = 30_000) {
683
750
  const failures = [];
@@ -777,6 +844,8 @@ class UpdateFailure extends Error {
777
844
  }
778
845
  class UpdatePreflightFailure extends Error {
779
846
  }
847
+ class UpdateAutomaticDeferred extends Error {
848
+ }
780
849
  async function canonicalImmutableRoot(dataDir, releaseRoot) {
781
850
  try {
782
851
  const [store, selected] = await Promise.all([realpath(resolve(dataDir, "releases")), realpath(releaseRoot)]);
@@ -1,8 +1,11 @@
1
+ import { type ResidentTabRoute } from "./routes.js";
1
2
  export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "crashed" | "restarting" | "failed";
3
+ export { residentUtilityRoute, type ResidentTabRoute } from "./routes.js";
2
4
  export interface TabBridgeCredentials {
3
5
  readonly tabId: string;
4
6
  readonly token: string;
5
7
  readonly endpoint: string;
8
+ readonly releaseId?: string;
6
9
  }
7
10
  export interface TabAttachment {
8
11
  readonly clientId: string;
@@ -17,7 +20,7 @@ export type TabBridgeCommand = {
17
20
  readonly nameSource: "default" | "user" | "auto";
18
21
  } | {
19
22
  readonly type: "route";
20
- readonly route: "settings";
23
+ readonly route: ResidentTabRoute;
21
24
  } | {
22
25
  readonly type: "stop";
23
26
  readonly deadlineMs: number;
@@ -59,5 +62,9 @@ export declare class TabBridgeClient {
59
62
  requestClose(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
60
63
  consumeControllerBoundaryInput(data: string): string;
61
64
  close(): void;
65
+ /**
66
+ * Asks the server to act for the client that controls this tab, as `origin`: leave it, restart or close the tab,
67
+ * or open the tab bar's Plus Menu dialog (`configure-plus`).
68
+ */
69
+ requestAction(action: "detach" | "restart" | "close" | "configure-plus", origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
62
70
  }
63
- export {};
@@ -1,5 +1,8 @@
1
1
  import { connect } from "node:net";
2
2
  import { encodeResidentFrame, ResidentFrameDecoder } from "./protocol.js";
3
+ import { RESIDENT_TAB_ROUTES } from "./routes.js";
4
+ export { residentUtilityRoute } from "./routes.js";
5
+ const RESIDENT_TAB_ROUTE_SET = new Set(RESIDENT_TAB_ROUTES);
3
6
  /** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
4
7
  export class TabBridgeClient {
5
8
  #options;
@@ -67,7 +70,7 @@ export class TabBridgeClient {
67
70
  this.#startWaiter = finish;
68
71
  transport.once("connect", () => {
69
72
  this.#send("hello", {
70
- role: "bridge", generation: 1, build: this.#options.build ?? "a1",
73
+ role: "bridge", generation: 1, build: this.#options.build ?? "a1", releaseId: this.#options.credentials.releaseId,
71
74
  features: ["bridge-status", "bridge-detach", "bridge-heartbeat", "bridge-restart", "bridge-visibility", "bridge-rename", "bridge-route", "bridge-stop", "bridge-shortcuts", "controller-boundary", "recovery-journal"],
72
75
  token: [...Buffer.from(this.#options.credentials.token, "base64")], tabId: this.#options.credentials.tabId,
73
76
  pid: this.#options.pid ?? process.pid, startIdentity: this.#options.startIdentity ?? `${process.pid}:node`,
@@ -118,13 +121,13 @@ export class TabBridgeClient {
118
121
  this.#send("bridge.shortcuts", { assignments: this.#latestShortcuts.map(([action, key]) => [action, key]) });
119
122
  }
120
123
  requestDetach(origin = this.captureAttachment(), timeoutMs = 1_000) {
121
- return this.#requestAction("detach", origin, timeoutMs);
124
+ return this.requestAction("detach", origin, timeoutMs);
122
125
  }
123
126
  requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
124
- return this.#requestAction("restart", origin, timeoutMs);
127
+ return this.requestAction("restart", origin, timeoutMs);
125
128
  }
126
129
  requestClose(origin = this.captureAttachment(), timeoutMs = 1_000) {
127
- return this.#requestAction("close", origin, timeoutMs);
130
+ return this.requestAction("close", origin, timeoutMs);
128
131
  }
129
132
  consumeControllerBoundaryInput(data) {
130
133
  const prefix = "\x1bP+a1;controller=";
@@ -205,6 +208,11 @@ export class TabBridgeClient {
205
208
  }
206
209
  #receive(message) {
207
210
  if (message.type === "welcome" && message.body["generation"] === 1) {
211
+ const id = this.#options.credentials.releaseId;
212
+ if (id !== undefined && message.body["releaseId"] !== id) {
213
+ this.#fail(false);
214
+ return;
215
+ }
208
216
  const advertised = message.body["features"];
209
217
  this.#features = new Set(Array.isArray(advertised) ? advertised.filter((feature) => typeof feature === "string") : []);
210
218
  this.#connected = true;
@@ -232,8 +240,8 @@ export class TabBridgeClient {
232
240
  this.#emitCommand({ type: "rename", name: message.body["name"], nameSource: source === "auto" || source === "default" ? source : "user" });
233
241
  return;
234
242
  }
235
- if (message.type === "bridge.route" && message.body["route"] === "settings") {
236
- this.#emitCommand({ type: "route", route: "settings" });
243
+ if (message.type === "bridge.route" && RESIDENT_TAB_ROUTE_SET.has(message.body["route"])) {
244
+ this.#emitCommand({ type: "route", route: message.body["route"] });
237
245
  return;
238
246
  }
239
247
  if (message.type === "bridge.stop" && typeof message.body["deadlineMs"] === "number") {
@@ -255,7 +263,11 @@ export class TabBridgeClient {
255
263
  }
256
264
  }
257
265
  }
258
- async #requestAction(action, origin, timeoutMs) {
266
+ /**
267
+ * Asks the server to act for the client that controls this tab, as `origin`: leave it, restart or close the tab,
268
+ * or open the tab bar's Plus Menu dialog (`configure-plus`).
269
+ */
270
+ async requestAction(action, origin = this.captureAttachment(), timeoutMs = 1_000) {
259
271
  if (!this.available || origin === null || (action === "restart" && !this.#supports("bridge-restart")))
260
272
  return false;
261
273
  const requestId = ++this.#request;