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.
- package/dist/client.mjs +16 -9
- package/dist/hosts/astro/clientTransport.js +3 -0
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +4 -3
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/sidecar.js +7 -1
- package/dist/hosts/next/wrapper.d.ts +2 -0
- package/dist/hosts/next/wrapper.js +2 -1
- package/dist/hosts/projectBridge.d.ts +1 -0
- package/dist/hosts/projectBridge.js +1 -0
- package/dist/hosts/static/manifest.js +2 -0
- package/dist/hosts/static/server.js +1 -0
- package/dist/hosts/vite/vite.d.ts +1 -0
- package/dist/hosts/vite/vite.js +4 -1
- package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
- package/dist/inspector/agent/McpConnectionDialog.js +11 -4
- package/dist/inspector/agent/client.d.ts +6 -0
- package/dist/inspector/agent/client.js +36 -4
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +8 -1
- package/dist/inspector/agent/httpTransport.d.ts +6 -0
- package/dist/inspector/agent/httpTransport.js +41 -1
- package/dist/inspector/agent/protocol.d.ts +3 -0
- package/dist/inspector/canvas/CanvasElementOverlay.js +49 -7
- package/dist/inspector/canvas/CanvasToolbar.js +4 -4
- package/dist/inspector/canvas/CanvasWorkspace.js +30 -11
- package/dist/inspector/canvas/frameProtocol.d.ts +16 -3
- package/dist/inspector/canvas/frameProtocol.js +9 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +2 -1
- package/dist/inspector/canvas/keyboardShortcuts.js +3 -1
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/rendererBootstrap.js +13 -0
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +55 -39
- package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.js +29 -3
- package/dist/inspector/clientManifest.d.ts +2 -0
- package/dist/inspector/clientManifest.js +1 -0
- package/dist/inspector/index.js +6 -8
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +40 -0
- package/dist/inspector/overlay/InspectorOverlay.js +6 -2
- package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
- package/dist/inspector/overlay/clickPolicy.js +34 -3
- package/dist/inspector/overlay/elementSelector.d.ts +1 -1
- package/dist/inspector/overlay/elementSelector.js +18 -7
- package/dist/inspector/overlay/spacingGestures.d.ts +3 -3
- package/dist/inspector/overlay/spacingGestures.js +15 -17
- package/dist/inspector/runtime/runtimeConfig.d.ts +1 -1
- package/dist/inspector/selection/hierarchy.d.ts +6 -0
- package/dist/inspector/selection/hierarchy.js +8 -0
- package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
- package/dist/inspector/settings/SettingsDialog.js +2 -2
- package/dist/inspector/shell/ChangesLog.js +20 -20
- package/dist/inspector/shell/CopyPromptButton.js +9 -2
- package/dist/inspector/shell/InspectorShell.js +2 -5
- package/dist/inspector/sketch/store.js +48 -23
- package/dist/inspector/styleEditors/SpacingBox.js +9 -4
- package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
- package/dist/inspector/styleEditors/nudgeValue.js +9 -7
- package/dist/inspector/tokens/TokenField.js +21 -2
- package/dist/inspector/tokens/TokensPanel.js +2 -1
- package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
- package/dist/inspector/tokens/dragPointerLock.js +70 -0
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/dragCursor.d.ts +8 -0
- package/dist/inspector/ui/dragCursor.js +21 -0
- package/dist/inspector/ui/styles.js +11 -9
- package/dist/nudge-ui.mjs +15 -1
- package/dist/transport/editor.d.ts +20 -1
- package/dist/transport/editor.js +42 -11
- package/dist/transport/routes.d.ts +4 -1
- package/dist/transport/routes.js +4 -1
- package/package.json +1 -1
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- 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"
|
|
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"
|
|
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
|
|
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({
|
|
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;
|
package/dist/hosts/vite/vite.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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" | "
|
|
9
|
-
readonly label: "Connect" | "
|
|
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>;
|