nudge-ui 0.2.1 → 0.2.2

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 (79) hide show
  1. package/dist/client.mjs +16 -9
  2. package/dist/hosts/astro/clientTransport.js +3 -0
  3. package/dist/hosts/astro/integration.d.ts +2 -2
  4. package/dist/hosts/astro/integration.js +4 -3
  5. package/dist/hosts/environment.d.ts +15 -0
  6. package/dist/hosts/environment.js +16 -0
  7. package/dist/hosts/next/sidecar.js +7 -1
  8. package/dist/hosts/next/wrapper.d.ts +2 -0
  9. package/dist/hosts/next/wrapper.js +2 -1
  10. package/dist/hosts/projectBridge.d.ts +1 -0
  11. package/dist/hosts/projectBridge.js +1 -0
  12. package/dist/hosts/static/manifest.js +2 -0
  13. package/dist/hosts/static/server.js +1 -0
  14. package/dist/hosts/vite/vite.d.ts +1 -0
  15. package/dist/hosts/vite/vite.js +4 -1
  16. package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
  17. package/dist/inspector/agent/McpConnectionDialog.js +11 -4
  18. package/dist/inspector/agent/client.d.ts +6 -0
  19. package/dist/inspector/agent/client.js +36 -4
  20. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  21. package/dist/inspector/agent/connectionStatus.js +8 -1
  22. package/dist/inspector/agent/httpTransport.d.ts +6 -0
  23. package/dist/inspector/agent/httpTransport.js +41 -1
  24. package/dist/inspector/agent/protocol.d.ts +3 -0
  25. package/dist/inspector/canvas/CanvasElementOverlay.js +49 -7
  26. package/dist/inspector/canvas/CanvasToolbar.js +4 -4
  27. package/dist/inspector/canvas/CanvasWorkspace.js +30 -11
  28. package/dist/inspector/canvas/frameProtocol.d.ts +16 -3
  29. package/dist/inspector/canvas/frameProtocol.js +9 -2
  30. package/dist/inspector/canvas/keyboardShortcuts.d.ts +2 -1
  31. package/dist/inspector/canvas/keyboardShortcuts.js +3 -1
  32. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  33. package/dist/inspector/canvas/linkEligibility.js +0 -29
  34. package/dist/inspector/canvas/rendererBootstrap.js +13 -0
  35. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  36. package/dist/inspector/canvas/rendererElementSelector.js +55 -39
  37. package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
  38. package/dist/inspector/changes/workspaceChanges.js +29 -3
  39. package/dist/inspector/clientManifest.d.ts +2 -0
  40. package/dist/inspector/clientManifest.js +1 -0
  41. package/dist/inspector/index.js +6 -8
  42. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  43. package/dist/inspector/overlay/DomNavigator.js +40 -0
  44. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  45. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  46. package/dist/inspector/overlay/clickPolicy.js +34 -3
  47. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  48. package/dist/inspector/overlay/elementSelector.js +18 -7
  49. package/dist/inspector/overlay/spacingGestures.d.ts +3 -3
  50. package/dist/inspector/overlay/spacingGestures.js +15 -17
  51. package/dist/inspector/runtime/runtimeConfig.d.ts +1 -1
  52. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  53. package/dist/inspector/selection/hierarchy.js +8 -0
  54. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  55. package/dist/inspector/settings/SettingsDialog.js +2 -2
  56. package/dist/inspector/shell/ChangesLog.js +20 -20
  57. package/dist/inspector/shell/CopyPromptButton.js +9 -2
  58. package/dist/inspector/shell/InspectorShell.js +2 -5
  59. package/dist/inspector/sketch/store.js +48 -23
  60. package/dist/inspector/styleEditors/SpacingBox.js +9 -4
  61. package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
  62. package/dist/inspector/styleEditors/nudgeValue.js +9 -7
  63. package/dist/inspector/tokens/TokenField.js +21 -2
  64. package/dist/inspector/tokens/TokensPanel.js +2 -1
  65. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  66. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  67. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  68. package/dist/inspector/ui/Disclosure.js +13 -0
  69. package/dist/inspector/ui/dragCursor.d.ts +8 -0
  70. package/dist/inspector/ui/dragCursor.js +21 -0
  71. package/dist/inspector/ui/styles.js +11 -9
  72. package/dist/nudge-ui.mjs +15 -1
  73. package/dist/transport/editor.d.ts +20 -1
  74. package/dist/transport/editor.js +42 -11
  75. package/dist/transport/routes.d.ts +4 -1
  76. package/dist/transport/routes.js +4 -1
  77. package/package.json +1 -1
  78. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  79. package/dist/inspector/shell/DomNavigation.js +0 -30
@@ -3,6 +3,7 @@ import { createAstroRuntimeConfig } from "./astroRuntimeConfig.js";
3
3
  import { createAstroClientAssetHandler } from "./clientAsset.js";
4
4
  import { NUDGE_UI_CLIENT_PATH, NUDGE_UI_EDITOR_PATH, NUDGE_UI_MANIFEST_PATH, createNudgeUiEditorDocument, isNudgeUiEditorDocumentRequest, } from "../../transport/index.js";
5
5
  import { startOptionalProjectBridge } from "../projectBridge.js";
6
+ import { automationManifestFields } from "../environment.js";
6
7
  const serveClient = createAstroClientAssetHandler();
7
8
  /** Serves the prebuilt client and its host-neutral manifest in Astro dev. */
8
9
  export function createAstroClientTransportPlugin() {
@@ -77,6 +78,7 @@ async function createManifest(server, request, setBridge) {
77
78
  appRoot: server.config.root,
78
79
  projectId: runtime.projectId,
79
80
  origin,
81
+ info: (message) => server.config.logger.info(message),
80
82
  warn: (message) => server.config.logger.warn(message),
81
83
  })
82
84
  : null;
@@ -86,6 +88,7 @@ async function createManifest(server, request, setBridge) {
86
88
  revision: 0,
87
89
  runtime,
88
90
  ...(bridge ? { agentBridge: bridge.browser } : {}),
91
+ ...automationManifestFields(),
89
92
  };
90
93
  }
91
94
  function resolvedRequestOrigin(server, request) {
@@ -11,8 +11,8 @@ export interface NudgeUiAstroOptions extends NudgeUiOptions {
11
11
  /**
12
12
  * Nudge UI host Adapter for Astro dev servers (ADR-0011).
13
13
  *
14
- * Dev-only by contract (ADR-0002): unless `command === "dev"` and
15
- * `enabled !== false`, the integration registers nothing at all — no Vite
14
+ * Dev-only by contract (ADR-0002): unless `command === "dev"`,
15
+ * `enabled !== false`, and `NUDGE_UI` is not `0`, the integration registers nothing at all — no Vite
16
16
  * plugins, no injected scripts, no middleware — so `astro build` output is
17
17
  * byte-identical to a project without the integration.
18
18
  *
@@ -2,6 +2,7 @@ import { nudgeUi } from "../vite/index.js";
2
2
  import { NUDGE_UI_CLIENT_PATH, NUDGE_UI_MANIFEST_PATH } from "../../transport/index.js";
3
3
  import { createAstroClientTransportPlugin, } from "./clientTransport.js";
4
4
  import { createProjectContextPlugin } from "./projectContext.js";
5
+ import { isNudgeUiEnabled } from "../environment.js";
5
6
  /**
6
7
  * Adds the prebuilt client as an external module. The inspector dependency
7
8
  * graph therefore never enters Astro's Vite optimizer.
@@ -23,8 +24,8 @@ const MIDDLEWARE_ENTRYPOINT = new URL(import.meta.url.endsWith(".ts") ? "./middl
23
24
  /**
24
25
  * Nudge UI host Adapter for Astro dev servers (ADR-0011).
25
26
  *
26
- * Dev-only by contract (ADR-0002): unless `command === "dev"` and
27
- * `enabled !== false`, the integration registers nothing at all — no Vite
27
+ * Dev-only by contract (ADR-0002): unless `command === "dev"`,
28
+ * `enabled !== false`, and `NUDGE_UI` is not `0`, the integration registers nothing at all — no Vite
28
29
  * plugins, no injected scripts, no middleware — so `astro build` output is
29
30
  * byte-identical to a project without the integration.
30
31
  *
@@ -37,7 +38,7 @@ const MIDDLEWARE_ENTRYPOINT = new URL(import.meta.url.endsWith(".ts") ? "./middl
37
38
  * identity Module.
38
39
  */
39
40
  export function nudgeUiAstro(options = {}) {
40
- const enabled = options.enabled ?? true;
41
+ const enabled = isNudgeUiEnabled(options.enabled);
41
42
  const sharedOptions = { ...options };
42
43
  return {
43
44
  name: "nudge-ui",
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The `NUDGE_UI` environment switch shared by every development host.
3
+ *
4
+ * - `0`, `false`, or `off` disables the host Adapter for that run.
5
+ * - `1`, `true`, or `on` also opens the editor in automated browsers, which
6
+ * otherwise receive the plain application.
7
+ */
8
+ export type NudgeUiEnvironmentSwitch = "off" | "on" | null;
9
+ export declare function readNudgeUiEnvironmentSwitch(env?: Readonly<Record<string, string | undefined>>): NudgeUiEnvironmentSwitch;
10
+ /** An explicit `enabled: false` or `NUDGE_UI=0` disables the Adapter. */
11
+ export declare function isNudgeUiEnabled(enabled: boolean | undefined, env?: Readonly<Record<string, string | undefined>>): boolean;
12
+ /** Manifest fields derived from the environment at request time. */
13
+ export declare function automationManifestFields(env?: Readonly<Record<string, string | undefined>>): {
14
+ inspectAutomatedBrowsers?: true;
15
+ };
@@ -0,0 +1,16 @@
1
+ export function readNudgeUiEnvironmentSwitch(env = process.env) {
2
+ const value = env.NUDGE_UI?.trim().toLowerCase();
3
+ if (value === "0" || value === "false" || value === "off")
4
+ return "off";
5
+ if (value === "1" || value === "true" || value === "on")
6
+ return "on";
7
+ return null;
8
+ }
9
+ /** An explicit `enabled: false` or `NUDGE_UI=0` disables the Adapter. */
10
+ export function isNudgeUiEnabled(enabled, env = process.env) {
11
+ return enabled !== false && readNudgeUiEnvironmentSwitch(env) !== "off";
12
+ }
13
+ /** Manifest fields derived from the environment at request time. */
14
+ export function automationManifestFields(env = process.env) {
15
+ return readNudgeUiEnvironmentSwitch(env) === "on" ? { inspectAutomatedBrowsers: true } : {};
16
+ }
@@ -9,6 +9,7 @@ import { createProjectFileWatcher, } from "../../project/files.js";
9
9
  import { createProjectTokenSnapshot } from "../../project/tokens.js";
10
10
  import { NUDGE_UI_CLIENT_PATH, NUDGE_UI_EDITOR_PATH, NUDGE_UI_MANIFEST_PATH, NUDGE_UI_RELOAD_PATH, NUDGE_UI_ROUTE_PREFIX, createNudgeUiEditorDocument, isNudgeUiEditorDocumentRequest, } from "../../transport/index.js";
11
11
  import { startOptionalProjectBridge } from "../projectBridge.js";
12
+ import { automationManifestFields } from "../environment.js";
12
13
  /**
13
14
  * Loopback-only manifest/reload sidecar (ADR-0010).
14
15
  *
@@ -264,6 +265,7 @@ export async function ensureSidecar(root, options = {}) {
264
265
  ...(options.projectBridgeAllowedOrigins
265
266
  ? { allowedOrigins: options.projectBridgeAllowedOrigins }
266
267
  : {}),
268
+ info: (message) => console.info(message),
267
269
  warn: (message) => console.warn(message),
268
270
  }).then((bridge) => {
269
271
  projectBridge = bridge;
@@ -529,7 +531,11 @@ function respond(req, res, currentManifest, currentGeneration, streams, receiveC
529
531
  if (url === NUDGE_UI_MANIFEST_PATH) {
530
532
  // The served snapshot carries its revision so clients can reconcile
531
533
  // against SSE notifications instead of guessing.
532
- const body = `${JSON.stringify({ ...currentManifest(), revision: currentGeneration() })}\n`;
534
+ const body = `${JSON.stringify({
535
+ ...currentManifest(),
536
+ revision: currentGeneration(),
537
+ ...automationManifestFields(),
538
+ })}\n`;
533
539
  res.writeHead(200, {
534
540
  "content-type": "application/json",
535
541
  "cache-control": "no-store",
@@ -28,6 +28,8 @@ export interface NudgeUiNextConfig {
28
28
  }
29
29
  /** Host-specific policy additions passed to the shared source compiler. */
30
30
  export interface NudgeUiNextOptions {
31
+ /** `false` returns the configuration untouched. `NUDGE_UI=0` has the same effect for one run. */
32
+ readonly enabled?: boolean;
31
33
  readonly componentProtocols?: ComponentModuleProtocols;
32
34
  /** Authored workspace directories outside the Next.js application root. */
33
35
  readonly sourceRoots?: readonly string[];
@@ -4,6 +4,7 @@ import { dirname, join, relative, resolve } from "node:path";
4
4
  import { ensureSidecar } from "./sidecar.js";
5
5
  import { buildManifest } from "./manifest.js";
6
6
  import { nudgeUiRepositoryPackagePath } from "./repositoryScope.js";
7
+ import { isNudgeUiEnabled } from "../environment.js";
7
8
  import { NUDGE_UI_EDITOR_QUERY_PARAM, NUDGE_UI_EDITOR_QUERY_VALUE, NUDGE_UI_ROUTE_PREFIX, } from "../../transport/index.js";
8
9
  const NAMESPACE_PREFIX = NUDGE_UI_ROUTE_PREFIX.slice(0, -1);
9
10
  const SUPPORTED_NEXT_RANGE = ">=15.3 <17";
@@ -104,7 +105,7 @@ export function withNudgeUi(config = {}, options = {}) {
104
105
  : () => config;
105
106
  return (phase) => {
106
107
  const resolved = factory(phase);
107
- if (!isDevelopmentEvaluation(phase))
108
+ if (!isDevelopmentEvaluation(phase) || !isNudgeUiEnabled(options.enabled))
108
109
  return resolved;
109
110
  return instrumentConfig(resolved, options);
110
111
  };
@@ -8,6 +8,7 @@ export declare function startOptionalProjectBridge(input: {
8
8
  readonly projectId: string;
9
9
  readonly origin: string;
10
10
  readonly allowedOrigins?: readonly string[];
11
+ readonly info?: (message: string) => void;
11
12
  readonly warn?: (message: string) => void;
12
13
  }): Promise<{
13
14
  browser: ProjectBridgeBrowserConfig;
@@ -53,6 +53,7 @@ async function launchOptionalProjectBridge(input, key) {
53
53
  await runtime.close().catch(() => undefined);
54
54
  throw new Error("the project bridge returned a different project or origin");
55
55
  }
56
+ input.info?.(`[nudge-ui] project bridge ready for ${input.projectId}`);
56
57
  let closed = false;
57
58
  return {
58
59
  browser: { baseUrl: runtime.browser.bridgeUrl, autoConnect: true },
@@ -1,4 +1,5 @@
1
1
  import { NUDGE_UI_MANIFEST_VERSION, NUDGE_UI_RELOAD_PATH } from "../../transport/index.js";
2
+ import { automationManifestFields } from "../environment.js";
2
3
  export { NUDGE_UI_CLIENT_PATH, NUDGE_UI_EDITOR_PATH, NUDGE_UI_MANIFEST_PATH, NUDGE_UI_MOUNT_ID, NUDGE_UI_RELOAD_PATH, NUDGE_UI_ROUTE_PREFIX, } from "../../transport/index.js";
3
4
  /**
4
5
  * Creates the first valid static-HTML runtime snapshot.
@@ -39,6 +40,7 @@ export function createStandaloneRuntimeManifest(projectId, tokenSnapshot = empty
39
40
  strategy: "reload-document",
40
41
  events: ["ready", "reload"],
41
42
  },
43
+ ...automationManifestFields(),
42
44
  };
43
45
  }
44
46
  function emptyTokenSnapshot() {
@@ -157,6 +157,7 @@ export function createStandaloneServer(options) {
157
157
  appRoot: rootDirectory,
158
158
  projectId,
159
159
  origin: address.url.replace(/\/$/, ""),
160
+ info: (message) => console.info(message),
160
161
  warn: (message) => console.warn(message),
161
162
  }).then((bridge) => {
162
163
  projectBridge = bridge;
@@ -18,6 +18,7 @@ export interface VanillaExtractOptions {
18
18
  source?: string;
19
19
  }
20
20
  export interface NudgeUiOptions extends ReactOptions {
21
+ /** `false` registers nothing. `NUDGE_UI=0` has the same effect for one run. */
21
22
  enabled?: boolean;
22
23
  /** Enables experimental DOM parent/child navigation in the Inspector. */
23
24
  debug?: boolean;
@@ -8,6 +8,7 @@ import { catalogSourcePath, createViteStylesheetArtifact, isHostApplicationSourc
8
8
  import { interpretDialects } from "../../css/dialects/index.js";
9
9
  import { detectStylingSystem, } from "../../inspector/clientManifest.js";
10
10
  import { loopbackOrigin, startOptionalProjectBridge, } from "../projectBridge.js";
11
+ import { automationManifestFields, isNudgeUiEnabled } from "../environment.js";
11
12
  function isRecord(value) {
12
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
13
14
  }
@@ -101,7 +102,7 @@ function invalidateHmrModules(server, modules, timestamp) {
101
102
  * here has grown a React assumption.
102
103
  */
103
104
  export function createVitePlugins(options = {}, createFramework = null) {
104
- const enabled = options.enabled ?? true;
105
+ const enabled = isNudgeUiEnabled(options.enabled);
105
106
  let root;
106
107
  let buildOutputDirectory;
107
108
  let command = "serve";
@@ -484,6 +485,7 @@ export function createVitePlugins(options = {}, createFramework = null) {
484
485
  revision: 0,
485
486
  runtime: buildRuntimeSnapshot(),
486
487
  ...(projectBridge ? { agentBridge: projectBridge.browser } : {}),
488
+ ...automationManifestFields(),
487
489
  };
488
490
  }
489
491
  function startProjectBridge(server) {
@@ -506,6 +508,7 @@ export function createVitePlugins(options = {}, createFramework = null) {
506
508
  projectId: options.projectId ?? basename(root),
507
509
  origin,
508
510
  ...(allowedOrigins.length > 0 ? { allowedOrigins } : {}),
511
+ info: (message) => server.config.logger.info(message),
509
512
  warn: (message) => server.config.logger.warn(message),
510
513
  }).then((bridge) => {
511
514
  projectBridge = bridge;
@@ -8,6 +8,7 @@ export interface McpConnectionContentProps {
8
8
  readonly origin: string;
9
9
  readonly snapshot: AgentClientSnapshot;
10
10
  readonly onConnect: () => Promise<boolean> | boolean | void;
11
+ readonly onTakeOver: () => Promise<boolean> | boolean | void;
11
12
  readonly onDisconnect: () => void;
12
13
  readonly onCheckAgain: () => Promise<void>;
13
14
  }
@@ -16,11 +17,11 @@ export interface McpConnectionDialogProps extends McpConnectionContentProps {
16
17
  readonly onOpenChange: (open: boolean) => void;
17
18
  }
18
19
  /** Renders the MCP setup and pairing flow without owning a dialog surface. */
19
- export declare function McpConnectionContent({ projectId, origin, snapshot, onConnect, onDisconnect, onCheckAgain, }: McpConnectionContentProps): ReactElement;
20
+ export declare function McpConnectionContent({ projectId, origin, snapshot, onConnect, onTakeOver, onDisconnect, onCheckAgain, }: McpConnectionContentProps): ReactElement;
20
21
  /**
21
22
  * Explains and controls the project-scoped MCP companion connection.
22
23
  *
23
24
  * This compatibility wrapper keeps the existing standalone MCP surface while
24
25
  * the shared content is also embedded in the general settings modal.
25
26
  */
26
- export declare function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onDisconnect, onCheckAgain, }: McpConnectionDialogProps): ReactElement;
27
+ export declare function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onTakeOver, onDisconnect, onCheckAgain, }: McpConnectionDialogProps): ReactElement;
@@ -5,6 +5,7 @@ import { IconCheck, IconClipboard, IconExternalLink, IconPlugConnected, IconRefr
5
5
  import { getAgentConnectionStatus } from "./connectionStatus.js";
6
6
  import { copyToClipboard } from "../prompt/copyToClipboard.js";
7
7
  import { Button } from "../ui/Button.js";
8
+ import { Disclosure } from "../ui/Disclosure.js";
8
9
  import { SegmentedControl } from "../ui/SegmentedControl.js";
9
10
  import { StatusCallout } from "../ui/StatusCallout.js";
10
11
  export const MCP_DOCS_URL = "https://github.com/charlessmart/nudge-ui#connect-a-coding-agent";
@@ -21,7 +22,7 @@ function TimelineStep({ number, complete, title, children, }) {
21
22
  return (_jsxs("li", { className: `mcp-connection__timeline-step${complete ? " mcp-connection__timeline-step--complete" : ""}`, "data-test": `mcp-step-${number}`, "data-complete": complete ? "true" : "false", children: [_jsx("div", { className: "mcp-connection__timeline-marker", "aria-hidden": "true", children: complete ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 2 }) : number }), _jsxs("section", { className: "mcp-connection__timeline-content", children: [_jsx("h3", { className: "mcp-connection__section-title", children: title }), children] })] }));
22
23
  }
23
24
  /** Renders the MCP setup and pairing flow without owning a dialog surface. */
24
- export function McpConnectionContent({ projectId, origin, snapshot, onConnect, onDisconnect, onCheckAgain, }) {
25
+ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, onTakeOver, onDisconnect, onCheckAgain, }) {
25
26
  const [checking, setChecking] = useState(false);
26
27
  const [setupMethod, setSetupMethod] = useState("ai");
27
28
  const [copiedCommand, setCopiedCommand] = useState(false);
@@ -42,10 +43,16 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
42
43
  const status = getAgentConnectionStatus(snapshot);
43
44
  const canConnect = snapshot.companionReachable
44
45
  && !snapshot.paired
46
+ && !snapshot.pairedElsewhere
45
47
  && snapshot.state !== "pairing"
46
48
  && snapshot.state !== "working"
47
49
  && snapshot.request?.status !== "working";
48
50
  const canDisconnect = snapshot.paired || snapshot.request?.status === "working";
51
+ const canTakeOver = snapshot.companionReachable
52
+ && snapshot.pairedElsewhere
53
+ && snapshot.state !== "pairing"
54
+ && snapshot.state !== "working"
55
+ && snapshot.request?.status !== "working";
49
56
  async function handleCheckAgain() {
50
57
  if (checking)
51
58
  return;
@@ -97,7 +104,7 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
97
104
  return (_jsxs(_Fragment, { children: [_jsxs(StatusCallout, { className: "mcp-connection__status", tone: status.tone, "data-test": "mcp-connection-status", children: [_jsx("span", { className: "mcp-connection__status-label", role: "status", children: status.label }), snapshot.error ? (_jsx("span", { className: "mcp-connection__status-detail", children: snapshot.error })) : null, checkError ? _jsx("span", { role: "alert", children: checkError }) : null, snapshot.request?.summary ? (_jsx("span", { className: "mcp-connection__status-detail", children: snapshot.request.summary })) : null, snapshot.request?.error ? (_jsx("span", { className: "mcp-connection__status-detail", children: snapshot.request.error })) : null] }), _jsxs("ol", { className: "mcp-connection__timeline", "data-test": "mcp-connection-timeline", children: [_jsxs(TimelineStep, { number: 1, complete: snapshot.listenerActive || snapshot.request !== null, title: "Set up your agent once", children: [_jsx(SegmentedControl, { "aria-label": "MCP setup method", className: "mcp-connection__setup-tabs", "data-test": "mcp-setup-tabs", value: setupMethod, options: [
98
105
  { value: "ai", label: "AI instructions", testId: "mcp-setup-tab-ai" },
99
106
  { value: "terminal", label: "Terminal command", testId: "mcp-setup-tab-terminal" },
100
- ], onChange: setSetupMethod }), setupMethod === "terminal" ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Run this command from the application directory, then start your app normally." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-command", children: setupCommand }) }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-command", type: "button", onClick: () => void handleCopyCommand(), children: [copiedCommand ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedCommand ? "Copied" : "Copy command"] }) })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Copy these setup instructions into your coding agent." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-prompt", children: setupPrompt }) }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-setup-prompt", type: "button", onClick: () => void handleCopyPrompt(), children: [copiedPrompt ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedPrompt ? "Copied" : "Copy prompt"] }) })] }))] }), _jsxs(TimelineStep, { number: 2, complete: snapshot.listenerActive, title: "Listen in this agent session", children: [_jsx("p", { className: "mcp-connection__copy", children: "Tell your coding agent: \u201CListen to Nudge.\u201D Reload the agent if Nudge tools are unavailable." }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-listener", type: "button", onClick: () => void handleCopyListener(), children: [copiedListener ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedListener ? "Copied" : "Copy listening instruction"] }) })] })] }), _jsxs("details", { className: "mcp-connection__details", children: [_jsx("summary", { children: "Connection details and recovery" }), _jsxs("p", { className: "mcp-connection__copy", children: ["The page connects automatically when the development integration is available. To diagnose setup, run ", _jsx("code", { children: "nudge-ui agent doctor" }), " from the application directory."] }), _jsxs("dl", { className: "mcp-connection__diagnostics", "data-test": "mcp-connection-diagnostics", children: [_jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Project ID" }), _jsx("dd", { "data-test": "mcp-project-id", children: _jsx("code", { children: projectId }) })] }), _jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Origin" }), _jsx("dd", { "data-test": "mcp-origin", children: _jsx("code", { children: origin }) })] })] }), _jsxs("div", { className: "mcp-connection__actions", children: [canConnect ? (_jsxs(Button, { variant: "primary", "data-test": "mcp-connect", type: "button", onClick: () => void onConnect(), children: [_jsx(IconPlugConnected, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Connect"] })) : null, _jsxs(Button, { variant: "quiet", "data-test": "mcp-check-again", type: "button", disabled: checking || snapshot.state === "disabled" || snapshot.state === "pairing", onClick: () => void handleCheckAgain(), children: [_jsx(IconRefresh, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), checking ? "Checking…" : "Check again"] }), canDisconnect ? (_jsxs(Button, { variant: "danger", "data-test": "mcp-disconnect", type: "button", onClick: onDisconnect, children: [_jsx(IconUnlink, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), snapshot.paired ? "Disconnect" : "Forget connection"] })) : null] })] }), copyError ? _jsx("p", { className: "mcp-connection__error", role: "alert", children: copyError }) : null, _jsxs("a", { className: "mcp-connection__docs", "data-test": "mcp-docs-link", href: MCP_DOCS_URL, target: "_blank", rel: "noreferrer", children: [_jsx(IconExternalLink, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Read the MCP connection guide"] })] }));
107
+ ], onChange: setSetupMethod }), setupMethod === "terminal" ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Run this command from the application directory, then start your app normally." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-command", children: setupCommand }) }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-command", type: "button", onClick: () => void handleCopyCommand(), children: [copiedCommand ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedCommand ? "Copied" : "Copy command"] }) })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Copy these setup instructions into your coding agent." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-prompt", children: setupPrompt }) }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-setup-prompt", type: "button", onClick: () => void handleCopyPrompt(), children: [copiedPrompt ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedPrompt ? "Copied" : "Copy prompt"] }) })] }))] }), _jsxs(TimelineStep, { number: 2, complete: snapshot.listenerActive, title: "Listen in this agent session", children: [_jsx("p", { className: "mcp-connection__copy", children: "Tell your coding agent: \u201CListen to Nudge.\u201D Reload the agent if Nudge tools are unavailable." }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-listener", type: "button", onClick: () => void handleCopyListener(), children: [copiedListener ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedListener ? "Copied" : "Copy listening instruction"] }) })] })] }), _jsxs(Disclosure, { className: "mcp-connection__details", title: "Connection details and recovery", children: [_jsxs("p", { className: "mcp-connection__copy", children: ["The page connects automatically when the development integration is available. To diagnose setup, run ", _jsx("code", { children: "nudge-ui agent doctor" }), " from the application directory."] }), _jsxs("dl", { className: "mcp-connection__diagnostics", "data-test": "mcp-connection-diagnostics", children: [_jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Project ID" }), _jsx("dd", { "data-test": "mcp-project-id", children: _jsx("code", { children: projectId }) })] }), _jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Origin" }), _jsx("dd", { "data-test": "mcp-origin", children: _jsx("code", { children: origin }) })] })] }), _jsxs("div", { className: "mcp-connection__actions", children: [canTakeOver ? (_jsxs(Button, { variant: "primary", "data-test": "mcp-takeover", type: "button", onClick: () => void onTakeOver(), children: [_jsx(IconPlugConnected, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Take over"] })) : null, canConnect ? (_jsxs(Button, { variant: "primary", "data-test": "mcp-connect", type: "button", onClick: () => void onConnect(), children: [_jsx(IconPlugConnected, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Connect"] })) : null, _jsxs(Button, { variant: "quiet", "data-test": "mcp-check-again", type: "button", disabled: checking || snapshot.state === "disabled" || snapshot.state === "pairing", onClick: () => void handleCheckAgain(), children: [_jsx(IconRefresh, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), checking ? "Checking…" : "Check again"] }), canDisconnect ? (_jsxs(Button, { variant: "danger", "data-test": "mcp-disconnect", type: "button", onClick: onDisconnect, children: [_jsx(IconUnlink, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), snapshot.paired ? "Disconnect" : "Forget connection"] })) : null] })] }), copyError ? _jsx("p", { className: "mcp-connection__error", role: "alert", children: copyError }) : null, _jsxs("a", { className: "mcp-connection__docs", "data-test": "mcp-docs-link", href: MCP_DOCS_URL, target: "_blank", rel: "noreferrer", children: [_jsx(IconExternalLink, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Read the MCP connection guide"] })] }));
101
108
  }
102
109
  /**
103
110
  * Explains and controls the project-scoped MCP companion connection.
@@ -105,7 +112,7 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
105
112
  * This compatibility wrapper keeps the existing standalone MCP surface while
106
113
  * the shared content is also embedded in the general settings modal.
107
114
  */
108
- export function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onDisconnect, onCheckAgain, }) {
115
+ export function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onTakeOver, onDisconnect, onCheckAgain, }) {
109
116
  const closeRef = useRef(null);
110
- return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { className: "mcp-connection__backdrop", "data-test": "mcp-connection-backdrop" }), _jsxs(Dialog.Popup, { className: "mcp-connection__popup", "data-test": "mcp-connection-dialog", initialFocus: closeRef, children: [_jsxs("div", { className: "mcp-connection__header", children: [_jsxs("div", { children: [_jsx(Dialog.Title, { className: "mcp-connection__title", children: "Connect MCP" }), _jsx(Dialog.Description, { className: "mcp-connection__description", children: "Set up your coding agent once, then ask it to listen to Nudge in this worktree." })] }), _jsx(Dialog.Close, { ref: closeRef, className: "icon-button icon-button--quiet mcp-connection__close", "aria-label": "Close MCP connection", "data-test": "mcp-connection-close", type: "button", children: _jsx(IconX, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) })] }), _jsx(McpConnectionContent, { projectId: projectId, origin: origin, snapshot: snapshot, onConnect: onConnect, onDisconnect: onDisconnect, onCheckAgain: onCheckAgain })] })] }) }));
117
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { className: "mcp-connection__backdrop", "data-test": "mcp-connection-backdrop" }), _jsxs(Dialog.Popup, { className: "mcp-connection__popup", "data-test": "mcp-connection-dialog", initialFocus: closeRef, children: [_jsxs("div", { className: "mcp-connection__header", children: [_jsxs("div", { children: [_jsx(Dialog.Title, { className: "mcp-connection__title", children: "Connect MCP" }), _jsx(Dialog.Description, { className: "mcp-connection__description", children: "Set up your coding agent once, then ask it to listen to Nudge in this worktree." })] }), _jsx(Dialog.Close, { ref: closeRef, className: "icon-button icon-button--quiet mcp-connection__close", "aria-label": "Close MCP connection", "data-test": "mcp-connection-close", type: "button", children: _jsx(IconX, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) })] }), _jsx(McpConnectionContent, { projectId: projectId, origin: origin, snapshot: snapshot, onConnect: onConnect, onTakeOver: onTakeOver, onDisconnect: onDisconnect, onCheckAgain: onCheckAgain })] })] }) }));
111
118
  }
@@ -18,6 +18,8 @@ export interface AgentClientSnapshot {
18
18
  /** Whether the last valid discovery response came from the local companion. */
19
19
  readonly companionReachable: boolean;
20
20
  readonly paired: boolean;
21
+ /** Whether another browser currently owns this project's pairing. */
22
+ readonly pairedElsewhere: boolean;
21
23
  readonly request: AgentRequestSnapshot | null;
22
24
  readonly error?: string;
23
25
  }
@@ -51,6 +53,7 @@ export declare class AgentClient {
51
53
  private listenerActive;
52
54
  private companionReachable;
53
55
  private paired;
56
+ private pairedElsewhere;
54
57
  private lastError;
55
58
  private readonly autoConnect;
56
59
  private autoConnectSuppressed;
@@ -70,6 +73,9 @@ export declare class AgentClient {
70
73
  reconnect(): void;
71
74
  /** Pairs the browser with a reachable companion, including an idle agent. */
72
75
  connect(): Promise<boolean>;
76
+ /** Replaces an existing browser pairing after an explicit user action. */
77
+ takeOver(): Promise<boolean>;
78
+ private pairWith;
73
79
  private openEvents;
74
80
  /**
75
81
  * Dispatches one immutable prompt revision. The request enters `working`
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo, useSyncExternalStore } from "react";
2
2
  import { isNudgeUiDev } from "../runtime/devFlag.js";
3
3
  import { isDemoRuntime } from "../runtime/runtimeConfig.js";
4
- import { HttpAgentBridgeTransport, shouldAutoConnectAgentBridge } from "./httpTransport.js";
4
+ import { AgentBridgeHttpError, HttpAgentBridgeTransport, shouldAutoConnectAgentBridge, } from "./httpTransport.js";
5
5
  import { AGENT_PROTOCOL_VERSION, } from "./protocol.js";
6
6
  const DISABLED_SNAPSHOT = Object.freeze({
7
7
  state: "disabled",
@@ -9,6 +9,7 @@ const DISABLED_SNAPSHOT = Object.freeze({
9
9
  listenerActive: false,
10
10
  companionReachable: false,
11
11
  paired: false,
12
+ pairedElsewhere: false,
12
13
  request: null,
13
14
  });
14
15
  const DEFAULT_DISCOVERY_INTERVAL_MS = 2_000;
@@ -154,6 +155,7 @@ export class AgentClient {
154
155
  listenerActive = false;
155
156
  companionReachable = false;
156
157
  paired = false;
158
+ pairedElsewhere = false;
157
159
  lastError;
158
160
  autoConnect;
159
161
  autoConnectSuppressed = false;
@@ -211,6 +213,7 @@ export class AgentClient {
211
213
  this.listenerActive = false;
212
214
  this.companionReachable = false;
213
215
  this.paired = false;
216
+ this.pairedElsewhere = false;
214
217
  this.publish();
215
218
  }
216
219
  async restoreOrDiscover() {
@@ -291,12 +294,22 @@ export class AgentClient {
291
294
  this.listenerActive = false;
292
295
  this.companionReachable = false;
293
296
  this.paired = false;
297
+ this.pairedElsewhere = false;
294
298
  this.lastError = undefined;
295
299
  this.publish();
296
300
  void this.restoreOrDiscover();
297
301
  }
298
302
  /** Pairs the browser with a reachable companion, including an idle agent. */
299
303
  async connect() {
304
+ return this.pairWith((request, signal) => this.transport.pair(request, signal));
305
+ }
306
+ /** Replaces an existing browser pairing after an explicit user action. */
307
+ async takeOver() {
308
+ if (!this.transport.takeOver)
309
+ return false;
310
+ return this.pairWith((request, signal) => this.transport.takeOver(request, signal));
311
+ }
312
+ async pairWith(pair) {
300
313
  if (!this.enabled || !this.started || this.pairingAbort || this.paired || !this.companionReachable)
301
314
  return false;
302
315
  this.autoConnectSuppressed = false;
@@ -317,7 +330,7 @@ export class AgentClient {
317
330
  this.discoveryAbort?.abort();
318
331
  this.pairingAbort = abortController;
319
332
  try {
320
- const pairing = await this.transport.pair(request, abortController.signal);
333
+ const pairing = await pair(request, abortController.signal);
321
334
  if (!this.started || abortController.signal.aborted)
322
335
  return false;
323
336
  if (pairing.protocolVersion !== undefined && pairing.protocolVersion !== AGENT_PROTOCOL_VERSION) {
@@ -333,6 +346,7 @@ export class AgentClient {
333
346
  this.connection = "paired";
334
347
  this.listenerActive = pairing.status.listenerActive;
335
348
  this.paired = true;
349
+ this.pairedElsewhere = false;
336
350
  if (this.discoveryTimer !== null) {
337
351
  clearTimeout(this.discoveryTimer);
338
352
  this.discoveryTimer = null;
@@ -351,6 +365,9 @@ export class AgentClient {
351
365
  this.listenerActive = idleListenerActive;
352
366
  this.connection = idleListenerActive ? "listening" : "offline";
353
367
  this.paired = false;
368
+ this.pairedElsewhere = error instanceof AgentBridgeHttpError
369
+ && error.status === 409
370
+ && error.code === "already_paired";
354
371
  this.publish(idleListenerActive ? "available" : "disconnected");
355
372
  return false;
356
373
  }
@@ -467,6 +484,7 @@ export class AgentClient {
467
484
  this.connection = "offline";
468
485
  this.listenerActive = false;
469
486
  this.paired = false;
487
+ this.pairedElsewhere = false;
470
488
  this.lastError = reason === "reconnect" || reason === "browser_disconnected" ? undefined : reason;
471
489
  this.publish();
472
490
  if (token) {
@@ -503,12 +521,13 @@ export class AgentClient {
503
521
  // pairing, so do not adopt `paired` until this tab restores the
504
522
  // shared token. The next poll runs restore first in case a sibling
505
523
  // tab has just stored that token.
524
+ const pairedElsewhere = status.paired && !this.sessionToken;
506
525
  this.applyStatus(status.paired ? {
507
526
  ...status,
508
527
  connection: status.listenerActive ? "listening" : "offline",
509
528
  paired: false,
510
529
  request: null,
511
- } : status);
530
+ } : status, pairedElsewhere);
512
531
  if (this.autoConnect
513
532
  && !status.paired
514
533
  && !this.autoConnectSuppressed
@@ -522,6 +541,7 @@ export class AgentClient {
522
541
  this.listenerActive = false;
523
542
  this.companionReachable = false;
524
543
  this.paired = false;
544
+ this.pairedElsewhere = false;
525
545
  this.lastError = undefined;
526
546
  this.publish();
527
547
  }
@@ -532,6 +552,7 @@ export class AgentClient {
532
552
  this.listenerActive = false;
533
553
  this.companionReachable = false;
534
554
  this.paired = false;
555
+ this.pairedElsewhere = false;
535
556
  // Discovery failures are expected while the companion is not running;
536
557
  // retain a quiet disconnected state rather than flashing an error.
537
558
  this.lastError = undefined;
@@ -624,11 +645,12 @@ export class AgentClient {
624
645
  // bounded timeout and reports any expired command to its agent host.
625
646
  }
626
647
  }
627
- applyStatus(status) {
648
+ applyStatus(status, pairedElsewhere = false) {
628
649
  this.companionReachable = true;
629
650
  this.lastError = undefined;
630
651
  this.listenerActive = status.listenerActive;
631
652
  this.paired = status.paired;
653
+ this.pairedElsewhere = pairedElsewhere;
632
654
  this.connection = statusForConnection(status);
633
655
  if (status.paired && this.discoveryTimer !== null) {
634
656
  clearTimeout(this.discoveryTimer);
@@ -661,6 +683,7 @@ export class AgentClient {
661
683
  this.listenerActive = false;
662
684
  this.companionReachable = false;
663
685
  this.paired = false;
686
+ this.pairedElsewhere = false;
664
687
  this.publish();
665
688
  }
666
689
  applyRequestStatus(status, requestId, summary, error, changeRevision, clientDispatchId, sketches) {
@@ -712,7 +735,15 @@ export class AgentClient {
712
735
  this.listenerActive = false;
713
736
  this.companionReachable = false;
714
737
  this.paired = false;
738
+ this.pairedElsewhere = false;
715
739
  this.lastError = reason;
740
+ if (reason === "browser_disconnected") {
741
+ // A remote takeover deliberately invalidates this browser's token. Do
742
+ // not immediately race the new browser by auto-pairing again.
743
+ this.autoConnectSuppressed = true;
744
+ if (this.autoConnect)
745
+ setAutoConnectDisabled(this.projectId, true);
746
+ }
716
747
  this.publish();
717
748
  this.scheduleReconnect();
718
749
  }
@@ -725,6 +756,7 @@ export class AgentClient {
725
756
  listenerActive: this.listenerActive,
726
757
  companionReachable: this.companionReachable,
727
758
  paired: this.paired,
759
+ pairedElsewhere: this.pairedElsewhere,
728
760
  request: request ? {
729
761
  requestId: request.requestId,
730
762
  ...(request.changeRevision === undefined ? {} : { changeRevision: request.changeRevision }),
@@ -1,12 +1,12 @@
1
1
  import type { AgentClientSnapshot } from "./client.js";
2
- export type AgentConnectionStatusKind = "unavailable" | "connecting" | "working-disconnected" | "working" | "not-found" | "ready-to-connect" | "connected-not-listening" | "listening";
2
+ export type AgentConnectionStatusKind = "unavailable" | "connecting" | "working-disconnected" | "working" | "paired-elsewhere" | "not-found" | "ready-to-connect" | "connected-not-listening" | "listening";
3
3
  export interface AgentConnectionStatus {
4
4
  readonly kind: AgentConnectionStatusKind;
5
5
  readonly label: string;
6
6
  readonly tone: "neutral" | "accent" | "warning" | "danger";
7
7
  readonly action?: {
8
- readonly kind: "connect" | "setup";
9
- readonly label: "Connect" | "Set up";
8
+ readonly kind: "connect" | "takeover";
9
+ readonly label: "Connect" | "Take over";
10
10
  };
11
11
  }
12
12
  /** Returns the user-facing connection state shared by the prompt UI and MCP panel. */
@@ -6,6 +6,14 @@ export function getAgentConnectionStatus(snapshot) {
6
6
  if (snapshot.state === "pairing") {
7
7
  return { kind: "connecting", label: "Connecting…", tone: "accent" };
8
8
  }
9
+ if (snapshot.pairedElsewhere) {
10
+ return {
11
+ kind: "paired-elsewhere",
12
+ label: "MCP active in another tab or window",
13
+ tone: "warning",
14
+ action: { kind: "takeover", label: "Take over" },
15
+ };
16
+ }
9
17
  if (!snapshot.paired && (snapshot.state === "working" || snapshot.request?.status === "working")) {
10
18
  return { kind: "working-disconnected", label: "Disconnected · Last work status unknown", tone: "warning" };
11
19
  }
@@ -17,7 +25,6 @@ export function getAgentConnectionStatus(snapshot) {
17
25
  kind: "connected-not-listening",
18
26
  label: "Project connected · Ask agent to listen",
19
27
  tone: "accent",
20
- action: { kind: "setup", label: "Set up" },
21
28
  };
22
29
  }
23
30
  if (!snapshot.companionReachable) {
@@ -1,6 +1,11 @@
1
1
  import { type AgentBridgeTransport, type AgentBridgeEndpointConfig, type AgentDiscoveryRequest, type AgentEventHandlers, type AgentEventSubscription, type AgentEventsRequest, type AgentPairRequest, type AgentPromptDispatch, type AgentDisconnectRequest, type AgentCanvasAcknowledgementRequest, type AgentSessionRequest, type AgentStatusSnapshot, type PairingResponse, type PromptDispatchResponse } from "./protocol.js";
2
2
  /** Returns whether the dev host supplied a trusted project bridge endpoint. */
3
3
  export declare function shouldAutoConnectAgentBridge(): boolean;
4
+ export declare class AgentBridgeHttpError extends Error {
5
+ readonly status: number;
6
+ readonly code?: string;
7
+ constructor(status: number, message: string, code?: string);
8
+ }
4
9
  /**
5
10
  * HTTP/SSE browser adapter for the companion's canonical endpoints.
6
11
  *
@@ -13,6 +18,7 @@ export declare class HttpAgentBridgeTransport implements AgentBridgeTransport {
13
18
  constructor(config?: AgentBridgeEndpointConfig);
14
19
  discover(request: AgentDiscoveryRequest, signal?: AbortSignal): Promise<AgentStatusSnapshot | null>;
15
20
  pair(request: AgentPairRequest, signal?: AbortSignal): Promise<PairingResponse>;
21
+ takeOver(request: AgentPairRequest, signal?: AbortSignal): Promise<PairingResponse>;
16
22
  restore(request: AgentSessionRequest, signal?: AbortSignal): Promise<AgentStatusSnapshot | null>;
17
23
  openEvents(request: AgentEventsRequest, handlers: AgentEventHandlers): AgentEventSubscription;
18
24
  dispatch(request: AgentPromptDispatch, signal?: AbortSignal): Promise<PromptDispatchResponse>;