nudge-ui 0.2.1 → 0.2.3
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/README.md +164 -46
- package/dist/client.mjs +252 -54
- package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
- package/dist/css/value-semantics/colorSemantics.js +17 -0
- package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
- package/dist/hosts/astro/clientTransport.js +41 -32
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +9 -4
- package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
- package/dist/hosts/astro/markdownIdentity.js +81 -0
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/manifest.js +1 -1
- package/dist/hosts/next/sidecar.js +12 -2
- 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 +3 -1
- package/dist/hosts/static/server.js +4 -0
- package/dist/hosts/vite/vite.d.ts +4 -0
- package/dist/hosts/vite/vite.js +28 -5
- package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
- package/dist/inspector/agent/McpConnectionDialog.js +23 -22
- package/dist/inspector/agent/activity.d.ts +18 -0
- package/dist/inspector/agent/activity.js +70 -0
- package/dist/inspector/agent/client.d.ts +8 -0
- package/dist/inspector/agent/client.js +65 -5
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +9 -2
- package/dist/inspector/agent/httpTransport.d.ts +6 -0
- package/dist/inspector/agent/httpTransport.js +50 -3
- package/dist/inspector/agent/protocol.d.ts +9 -3
- package/dist/inspector/agent/protocol.js +1 -1
- package/dist/inspector/agent/verification.d.ts +11 -1
- package/dist/inspector/agent/verification.js +53 -25
- package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
- package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
- package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
- package/dist/inspector/canvas/CanvasCard.js +133 -38
- package/dist/inspector/canvas/CanvasElementOverlay.js +200 -22
- package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
- package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
- package/dist/inspector/canvas/CanvasToolbar.js +8 -4
- package/dist/inspector/canvas/CanvasWorkspace.js +243 -56
- package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
- package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
- package/dist/inspector/canvas/agentPresentation.js +64 -52
- package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
- package/dist/inspector/canvas/canvasGestures.js +1 -1
- package/dist/inspector/canvas/canvasStore.d.ts +46 -82
- package/dist/inspector/canvas/canvasStore.js +251 -284
- package/dist/inspector/canvas/frameContent.d.ts +21 -0
- package/dist/inspector/canvas/frameContent.js +44 -0
- package/dist/inspector/canvas/frameProtocol.d.ts +34 -4
- package/dist/inspector/canvas/frameProtocol.js +9 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -1
- package/dist/inspector/canvas/keyboardShortcuts.js +5 -3
- package/dist/inspector/canvas/layout.d.ts +52 -0
- package/dist/inspector/canvas/layout.js +106 -0
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/projection.d.ts +15 -5
- package/dist/inspector/canvas/projection.js +69 -13
- package/dist/inspector/canvas/rendererBootstrap.js +45 -4
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +102 -45
- package/dist/inspector/canvas/rendererStylesheet.js +2 -2
- package/dist/inspector/canvas/resizeHandleScale.js +1 -1
- package/dist/inspector/canvas/sessionStore.d.ts +11 -26
- package/dist/inspector/canvas/sessionStore.js +43 -183
- package/dist/inspector/canvas/staleChangeDetector.js +9 -3
- package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
- package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
- package/dist/inspector/canvas/viewStore.d.ts +38 -0
- package/dist/inspector/canvas/viewStore.js +104 -0
- package/dist/inspector/canvas/wheelPan.d.ts +8 -0
- package/dist/inspector/canvas/wheelPan.js +8 -0
- package/dist/inspector/changes/changesLog.d.ts +6 -5
- package/dist/inspector/changes/changesLog.js +40 -32
- package/dist/inspector/changes/draftChanges.d.ts +56 -0
- package/dist/inspector/changes/draftChanges.js +300 -0
- package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
- package/dist/inspector/changes/previewDiagnostics.js +11 -25
- package/dist/inspector/clientManifest.d.ts +3 -1
- package/dist/inspector/clientManifest.js +1 -0
- package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
- package/dist/inspector/comments/CommentsOverlay.js +250 -0
- package/dist/inspector/comments/element.d.ts +6 -0
- package/dist/inspector/comments/element.js +47 -0
- package/dist/inspector/comments/store.d.ts +29 -0
- package/dist/inspector/comments/store.js +121 -0
- package/dist/inspector/comments/verification.d.ts +5 -0
- package/dist/inspector/comments/verification.js +52 -0
- package/dist/inspector/drafts/model.d.ts +35 -0
- package/dist/inspector/drafts/model.js +45 -0
- package/dist/inspector/drafts/store.d.ts +28 -0
- package/dist/inspector/drafts/store.js +231 -0
- package/dist/inspector/index.js +26 -35
- package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
- package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
- package/dist/inspector/iterations/capture.d.ts +2 -0
- package/dist/inspector/iterations/capture.js +173 -0
- package/dist/inspector/iterations/client.d.ts +7 -0
- package/dist/inspector/iterations/client.js +52 -0
- package/dist/inspector/iterations/coordinator.d.ts +15 -0
- package/dist/inspector/iterations/coordinator.js +130 -0
- package/dist/inspector/iterations/prompt.d.ts +3 -0
- package/dist/inspector/iterations/prompt.js +26 -0
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +46 -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/interactionStyles.js +3 -1
- package/dist/inspector/overlay/spacingGestures.d.ts +15 -3
- package/dist/inspector/overlay/spacingGestures.js +88 -13
- package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
- package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
- package/dist/inspector/projection/renderedInstance.d.ts +2 -1
- package/dist/inspector/projection/renderedInstance.js +9 -5
- package/dist/inspector/projection/structuralProjection.d.ts +2 -1
- package/dist/inspector/projection/structuralProjection.js +19 -15
- package/dist/inspector/projection/textProjection.d.ts +2 -1
- package/dist/inspector/projection/textProjection.js +8 -4
- package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
- package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
- package/dist/inspector/prompt/clipboardHandoff.js +92 -143
- package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
- package/dist/inspector/prompt/generatePrompt.js +17 -16
- package/dist/inspector/runtime/runtimeConfig.d.ts +10 -1
- package/dist/inspector/runtime/runtimeConfig.js +24 -0
- 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 +22 -9
- package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
- package/dist/inspector/shell/AgentStatusButton.js +36 -0
- package/dist/inspector/shell/AppShell.js +4 -2
- package/dist/inspector/shell/ChangesLog.js +47 -27
- package/dist/inspector/shell/CopyPromptButton.js +146 -90
- package/dist/inspector/shell/EmptyState.js +9 -8
- package/dist/inspector/shell/InspectorShell.js +14 -25
- package/dist/inspector/shell/PeekOriginalButton.js +24 -23
- package/dist/inspector/shell/originalPreview.js +5 -5
- package/dist/inspector/shell/shortcuts.d.ts +3 -0
- package/dist/inspector/shell/shortcuts.js +11 -0
- package/dist/inspector/sketch/SketchEditor.js +14 -14
- package/dist/inspector/sketch/SketchEntryButton.js +1 -1
- package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
- package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
- package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
- package/dist/inspector/sketch/SketchOverlay.js +7 -7
- package/dist/inspector/sketch/SketchWorkspace.js +18 -12
- package/dist/inspector/sketch/interaction.d.ts +1 -0
- package/dist/inspector/sketch/interaction.js +8 -4
- package/dist/inspector/sketch/model.d.ts +5 -0
- package/dist/inspector/sketch/model.js +6 -2
- package/dist/inspector/sketch/raster.d.ts +1 -2
- package/dist/inspector/sketch/raster.js +68 -4
- package/dist/inspector/sketch/store.js +48 -23
- package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
- package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
- package/dist/inspector/styleEditors/BorderEditor.js +49 -78
- package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
- package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
- package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
- package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
- package/dist/inspector/styleEditors/ColorPicker.js +10 -4
- package/dist/inspector/styleEditors/GapField.js +3 -5
- package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
- package/dist/inspector/styleEditors/GridChildSection.js +18 -9
- package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
- package/dist/inspector/styleEditors/GridPicker.js +8 -2
- package/dist/inspector/styleEditors/GridSection.js +1 -1
- package/dist/inspector/styleEditors/GridValueField.js +19 -14
- package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
- package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
- package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
- package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
- package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
- package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
- package/dist/inspector/styleEditors/LayoutSection.js +32 -11
- package/dist/inspector/styleEditors/SpacingBox.js +16 -9
- package/dist/inspector/styleEditors/Typography.js +11 -3
- package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
- package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
- package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
- package/dist/inspector/styleEditors/gradientValue.js +114 -0
- package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
- package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
- package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
- package/dist/inspector/styleEditors/layoutValue.js +0 -42
- package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
- package/dist/inspector/styleEditors/nudgeValue.js +41 -11
- package/dist/inspector/tokens/TokenField.d.ts +7 -0
- package/dist/inspector/tokens/TokenField.js +49 -22
- package/dist/inspector/tokens/TokenLabel.js +5 -14
- 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/tokens/editActions.d.ts +1 -1
- package/dist/inspector/tokens/editActions.js +1 -1
- package/dist/inspector/ui/AtRuleContext.js +4 -6
- package/dist/inspector/ui/Button.d.ts +6 -2
- package/dist/inspector/ui/Button.js +4 -3
- package/dist/inspector/ui/ControlSurface.d.ts +2 -2
- package/dist/inspector/ui/ControlSurface.js +4 -3
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
- package/dist/inspector/ui/InlineStyleWarning.js +13 -0
- package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
- package/dist/inspector/ui/InspectorPopover.js +4 -5
- package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
- package/dist/inspector/ui/PopoverListbox.js +8 -7
- package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
- package/dist/inspector/ui/SegmentedControl.js +2 -2
- package/dist/inspector/ui/Select.d.ts +6 -1
- package/dist/inspector/ui/Select.js +7 -11
- package/dist/inspector/ui/SideValuesField.js +1 -1
- package/dist/inspector/ui/Tooltip.d.ts +26 -0
- package/dist/inspector/ui/Tooltip.js +32 -0
- package/dist/inspector/ui/classNames.d.ts +2 -0
- package/dist/inspector/ui/classNames.js +4 -0
- package/dist/inspector/ui/customCursors.d.ts +23 -0
- package/dist/inspector/ui/customCursors.js +122 -0
- package/dist/inspector/ui/dragCursor.d.ts +6 -0
- package/dist/inspector/ui/dragCursor.js +13 -0
- package/dist/inspector/ui/portalContainer.d.ts +2 -0
- package/dist/inspector/ui/portalContainer.js +6 -0
- package/dist/inspector/ui/styles.js +34 -24
- package/dist/inspector/workspace/commands.d.ts +9 -0
- package/dist/inspector/workspace/commands.js +82 -0
- package/dist/inspector/workspace/controller.d.ts +4 -0
- package/dist/inspector/workspace/controller.js +66 -0
- package/dist/inspector/workspace/handoff.d.ts +42 -0
- package/dist/inspector/workspace/handoff.js +115 -0
- package/dist/inspector/workspace/timeline.d.ts +30 -0
- package/dist/inspector/workspace/timeline.js +108 -0
- package/dist/internal/agent-protocol.d.ts +19 -2
- package/dist/internal/agent-protocol.js +13 -0
- package/dist/nudge-ui.mjs +560 -299
- package/dist/project/artifacts.d.ts +13 -0
- package/dist/project/artifacts.js +253 -0
- package/dist/transport/artifacts.d.ts +8 -0
- package/dist/transport/artifacts.js +9 -0
- package/dist/transport/editor.d.ts +20 -1
- package/dist/transport/editor.js +44 -11
- package/dist/transport/routes.d.ts +6 -1
- package/dist/transport/routes.js +6 -1
- package/package.json +6 -2
- package/dist/inspector/changes/workspaceChanges.d.ts +0 -43
- package/dist/inspector/changes/workspaceChanges.js +0 -239
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- package/dist/inspector/shell/DomNavigation.js +0 -30
|
@@ -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;
|
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Dialog } from "@base-ui/react/dialog";
|
|
4
|
-
import { IconCheck,
|
|
4
|
+
import { IconCheck, IconCopy, IconPlugConnected, IconRefresh, IconUnlink, IconX, } from "@tabler/icons-react";
|
|
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 {
|
|
8
|
+
import { Disclosure } from "../ui/Disclosure.js";
|
|
9
|
+
import { IconButton } from "../ui/IconButton.js";
|
|
9
10
|
import { StatusCallout } from "../ui/StatusCallout.js";
|
|
11
|
+
import { portalContainer } from "../ui/portalContainer.js";
|
|
10
12
|
export const MCP_DOCS_URL = "https://github.com/charlessmart/nudge-ui#connect-a-coding-agent";
|
|
11
13
|
/** Uses the guided installer so browser diagnostics never become shell arguments. */
|
|
12
14
|
export function createMcpSetupCommand() {
|
|
13
15
|
return "npx nudge-ui agent setup";
|
|
14
16
|
}
|
|
15
|
-
function portalContainer() {
|
|
16
|
-
return typeof document !== "undefined"
|
|
17
|
-
? document.getElementById("nudge-ui-root")?.shadowRoot ?? document.body
|
|
18
|
-
: null;
|
|
19
|
-
}
|
|
20
17
|
function TimelineStep({ number, complete, title, children, }) {
|
|
21
18
|
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
19
|
}
|
|
23
20
|
/** Renders the MCP setup and pairing flow without owning a dialog surface. */
|
|
24
|
-
export function McpConnectionContent({ projectId, origin, snapshot, onConnect, onDisconnect, onCheckAgain, }) {
|
|
21
|
+
export function McpConnectionContent({ projectId, origin, snapshot, onConnect, onTakeOver, onDisconnect, onCheckAgain, }) {
|
|
25
22
|
const [checking, setChecking] = useState(false);
|
|
26
|
-
const [setupMethod, setSetupMethod] = useState("ai");
|
|
27
23
|
const [copiedCommand, setCopiedCommand] = useState(false);
|
|
28
24
|
const [copiedPrompt, setCopiedPrompt] = useState(false);
|
|
29
|
-
const [
|
|
25
|
+
const [copiedDoctor, setCopiedDoctor] = useState(false);
|
|
30
26
|
const [copyError, setCopyError] = useState();
|
|
31
27
|
const [checkError, setCheckError] = useState();
|
|
32
28
|
const setupCommand = createMcpSetupCommand();
|
|
@@ -38,14 +34,22 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
|
|
|
38
34
|
"",
|
|
39
35
|
"Start the application normally. Reload the agent if needed, then listen to Nudge.",
|
|
40
36
|
].join("\n"), [setupCommand]);
|
|
41
|
-
const
|
|
37
|
+
const doctorCommand = "nudge-ui agent doctor";
|
|
42
38
|
const status = getAgentConnectionStatus(snapshot);
|
|
39
|
+
const showConnectionStatus = status.kind !== "not-found"
|
|
40
|
+
|| Boolean(snapshot.error || checkError || snapshot.request?.summary || snapshot.request?.error);
|
|
43
41
|
const canConnect = snapshot.companionReachable
|
|
44
42
|
&& !snapshot.paired
|
|
43
|
+
&& !snapshot.pairedElsewhere
|
|
45
44
|
&& snapshot.state !== "pairing"
|
|
46
45
|
&& snapshot.state !== "working"
|
|
47
46
|
&& snapshot.request?.status !== "working";
|
|
48
47
|
const canDisconnect = snapshot.paired || snapshot.request?.status === "working";
|
|
48
|
+
const canTakeOver = snapshot.companionReachable
|
|
49
|
+
&& snapshot.pairedElsewhere
|
|
50
|
+
&& snapshot.state !== "pairing"
|
|
51
|
+
&& snapshot.state !== "working"
|
|
52
|
+
&& snapshot.request?.status !== "working";
|
|
49
53
|
async function handleCheckAgain() {
|
|
50
54
|
if (checking)
|
|
51
55
|
return;
|
|
@@ -83,21 +87,18 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
|
|
|
83
87
|
setCopyError("The AI setup prompt could not be copied. Select it and copy it manually.");
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
|
-
async function
|
|
90
|
+
async function handleCopyDoctor() {
|
|
87
91
|
setCopyError(undefined);
|
|
88
92
|
try {
|
|
89
|
-
await copyToClipboard(
|
|
90
|
-
|
|
91
|
-
window.setTimeout(() =>
|
|
93
|
+
await copyToClipboard(doctorCommand);
|
|
94
|
+
setCopiedDoctor(true);
|
|
95
|
+
window.setTimeout(() => setCopiedDoctor(false), 1500);
|
|
92
96
|
}
|
|
93
97
|
catch {
|
|
94
|
-
setCopyError("The
|
|
98
|
+
setCopyError("The diagnostic command could not be copied. Select it and copy it manually.");
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
|
-
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
|
|
98
|
-
{ value: "ai", label: "AI instructions", testId: "mcp-setup-tab-ai" },
|
|
99
|
-
{ 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"] })] }));
|
|
101
|
+
return (_jsxs(_Fragment, { children: [showConnectionStatus ? (_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] })) : null, _jsxs("ol", { className: "mcp-connection__timeline", "data-test": "mcp-connection-timeline", children: [_jsxs(TimelineStep, { number: 1, complete: snapshot.companionReachable || snapshot.listenerActive || snapshot.request !== null, title: "Set up agent", children: [_jsx("p", { className: "mcp-connection__copy", children: "Agent prompt" }), _jsxs("div", { className: "mcp-connection__command-block", children: [_jsx(IconButton, { className: "mcp-connection__copy-button", variant: "quiet", size: "compact", label: copiedPrompt ? "Copied" : "Copy setup instructions", "data-test": "mcp-copy-setup-prompt", onClick: () => void handleCopyPrompt(), children: copiedPrompt ? _jsx(IconCheck, { size: 16, "aria-hidden": "true" }) : _jsx(IconCopy, { size: 16, "aria-hidden": "true" }) }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-prompt", children: setupPrompt }) })] }), _jsx("p", { className: "mcp-connection__copy", children: "Or, terminal command" }), _jsxs("div", { className: "mcp-connection__command-block", children: [_jsx(IconButton, { className: "mcp-connection__copy-button", variant: "quiet", size: "compact", label: copiedCommand ? "Copied" : "Copy command", "data-test": "mcp-copy-command", onClick: () => void handleCopyCommand(), children: copiedCommand ? _jsx(IconCheck, { size: 16, "aria-hidden": "true" }) : _jsx(IconCopy, { size: 16, "aria-hidden": "true" }) }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-command", children: setupCommand }) })] })] }), _jsx(TimelineStep, { number: 2, complete: snapshot.listenerActive, title: "Tell agent to listen for instructions", children: _jsx("p", { className: "mcp-connection__copy", children: "Tell your coding agent: \u201CListen to Nudge.\u201D" }) })] }), _jsxs(Disclosure, { className: "mcp-connection__details", title: "Troubleshooting", defaultOpen: true, children: [_jsx("p", { className: "mcp-connection__copy", children: "To diagnose setup" }), _jsxs("div", { className: "mcp-connection__command-block", children: [_jsx(IconButton, { className: "mcp-connection__copy-button", variant: "quiet", size: "compact", label: copiedDoctor ? "Copied" : "Copy diagnostic command", "data-test": "mcp-copy-doctor", onClick: () => void handleCopyDoctor(), children: copiedDoctor ? _jsx(IconCheck, { size: 16, "aria-hidden": "true" }) : _jsx(IconCopy, { size: 16, "aria-hidden": "true" }) }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { children: doctorCommand }) })] }), _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: projectId })] }), _jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Origin" }), _jsx("dd", { "data-test": "mcp-origin", 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…" : "Refresh connection"] }), 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] }));
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
103
104
|
* Explains and controls the project-scoped MCP companion connection.
|
|
@@ -105,7 +106,7 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
|
|
|
105
106
|
* This compatibility wrapper keeps the existing standalone MCP surface while
|
|
106
107
|
* the shared content is also embedded in the general settings modal.
|
|
107
108
|
*/
|
|
108
|
-
export function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onDisconnect, onCheckAgain, }) {
|
|
109
|
+
export function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onTakeOver, onDisconnect, onCheckAgain, }) {
|
|
109
110
|
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 })] })] }) }));
|
|
111
|
+
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
112
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DraftTarget } from "../drafts/model.js";
|
|
2
|
+
import type { AgentActivity } from "./protocol.js";
|
|
3
|
+
export interface ActivityFile extends AgentActivity {
|
|
4
|
+
readonly expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface AgentActivitySnapshot {
|
|
7
|
+
readonly requestId: string | null;
|
|
8
|
+
readonly targets: readonly string[];
|
|
9
|
+
readonly target?: DraftTarget;
|
|
10
|
+
readonly files: readonly ActivityFile[];
|
|
11
|
+
}
|
|
12
|
+
/** Pins submitted frames before dispatch. Activity never selects or edits a draft. */
|
|
13
|
+
export declare function beginActivityDispatch(projectId: string, dispatchId: string, targets: readonly string[], target?: DraftTarget): void;
|
|
14
|
+
/** Dispatch and status are the only authority for the layer's lifetime. */
|
|
15
|
+
export declare function synchronizeAgentActivity(projectId: string, requestId: string | undefined, working: boolean, dispatchId?: string): void;
|
|
16
|
+
export declare function reportAgentActivity(projectId: string, activity: AgentActivity): void;
|
|
17
|
+
export declare function getAgentActivity(projectId: string): AgentActivitySnapshot;
|
|
18
|
+
export declare function useAgentActivity(projectId: string): AgentActivitySnapshot;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
const EMPTY = Object.freeze({ requestId: null, targets: [], files: [] });
|
|
3
|
+
const projects = new Map();
|
|
4
|
+
function project(id) {
|
|
5
|
+
let state = projects.get(id);
|
|
6
|
+
if (!state) {
|
|
7
|
+
state = { snapshot: EMPTY, listeners: new Set() };
|
|
8
|
+
projects.set(id, state);
|
|
9
|
+
}
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
function publish(state, snapshot) {
|
|
13
|
+
state.snapshot = snapshot;
|
|
14
|
+
for (const listener of state.listeners)
|
|
15
|
+
listener();
|
|
16
|
+
}
|
|
17
|
+
/** Pins submitted frames before dispatch. Activity never selects or edits a draft. */
|
|
18
|
+
export function beginActivityDispatch(projectId, dispatchId, targets, target) {
|
|
19
|
+
project(projectId).dispatch = { id: dispatchId, targets: [...targets], target };
|
|
20
|
+
}
|
|
21
|
+
/** Dispatch and status are the only authority for the layer's lifetime. */
|
|
22
|
+
export function synchronizeAgentActivity(projectId, requestId, working, dispatchId) {
|
|
23
|
+
const state = project(projectId);
|
|
24
|
+
if (!working || !requestId) {
|
|
25
|
+
if (state.timer)
|
|
26
|
+
clearTimeout(state.timer);
|
|
27
|
+
state.timer = undefined;
|
|
28
|
+
state.dispatch = undefined;
|
|
29
|
+
if (state.snapshot !== EMPTY)
|
|
30
|
+
publish(state, EMPTY);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (state.snapshot.requestId === requestId)
|
|
34
|
+
return;
|
|
35
|
+
if (state.timer)
|
|
36
|
+
clearTimeout(state.timer);
|
|
37
|
+
const dispatch = state.dispatch?.id === dispatchId ? state.dispatch : undefined;
|
|
38
|
+
publish(state, { requestId, targets: dispatch?.targets ?? [], ...(dispatch?.target ? { target: dispatch.target } : {}), files: [] });
|
|
39
|
+
}
|
|
40
|
+
export function reportAgentActivity(projectId, activity) {
|
|
41
|
+
const state = project(projectId);
|
|
42
|
+
if (state.snapshot.requestId !== activity.requestId)
|
|
43
|
+
return;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const files = state.snapshot.files.filter((file) => file.expiresAt > now && file.file !== activity.file);
|
|
46
|
+
files.push({ ...activity, expiresAt: now + (activity.operation === "read" ? 8_000 : 12_000) });
|
|
47
|
+
publish(state, { ...state.snapshot, files: files.slice(-64) });
|
|
48
|
+
scheduleExpiry(state);
|
|
49
|
+
}
|
|
50
|
+
function scheduleExpiry(state) {
|
|
51
|
+
if (state.timer)
|
|
52
|
+
clearTimeout(state.timer);
|
|
53
|
+
const deadline = Math.min(...state.snapshot.files.map((file) => file.expiresAt));
|
|
54
|
+
if (!Number.isFinite(deadline)) {
|
|
55
|
+
state.timer = undefined;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
state.timer = setTimeout(() => {
|
|
59
|
+
publish(state, { ...state.snapshot, files: state.snapshot.files.filter((file) => file.expiresAt > Date.now()) });
|
|
60
|
+
scheduleExpiry(state);
|
|
61
|
+
}, Math.max(0, deadline - Date.now()));
|
|
62
|
+
}
|
|
63
|
+
export function getAgentActivity(projectId) { return project(projectId).snapshot; }
|
|
64
|
+
export function useAgentActivity(projectId) {
|
|
65
|
+
return useSyncExternalStore((listener) => {
|
|
66
|
+
const state = project(projectId);
|
|
67
|
+
state.listeners.add(listener);
|
|
68
|
+
return () => { state.listeners.delete(listener); };
|
|
69
|
+
}, () => getAgentActivity(projectId), () => EMPTY);
|
|
70
|
+
}
|
|
@@ -15,9 +15,12 @@ export interface AgentClientSnapshot {
|
|
|
15
15
|
/** Companion state, using the shared protocol vocabulary. */
|
|
16
16
|
readonly connection: AgentStatusSnapshot["connection"];
|
|
17
17
|
readonly listenerActive: boolean;
|
|
18
|
+
readonly agentClientName?: string;
|
|
18
19
|
/** Whether the last valid discovery response came from the local companion. */
|
|
19
20
|
readonly companionReachable: boolean;
|
|
20
21
|
readonly paired: boolean;
|
|
22
|
+
/** Whether another browser currently owns this project's pairing. */
|
|
23
|
+
readonly pairedElsewhere: boolean;
|
|
21
24
|
readonly request: AgentRequestSnapshot | null;
|
|
22
25
|
readonly error?: string;
|
|
23
26
|
}
|
|
@@ -49,8 +52,10 @@ export declare class AgentClient {
|
|
|
49
52
|
private started;
|
|
50
53
|
private connection;
|
|
51
54
|
private listenerActive;
|
|
55
|
+
private agentClientName;
|
|
52
56
|
private companionReachable;
|
|
53
57
|
private paired;
|
|
58
|
+
private pairedElsewhere;
|
|
54
59
|
private lastError;
|
|
55
60
|
private readonly autoConnect;
|
|
56
61
|
private autoConnectSuppressed;
|
|
@@ -70,6 +75,9 @@ export declare class AgentClient {
|
|
|
70
75
|
reconnect(): void;
|
|
71
76
|
/** Pairs the browser with a reachable companion, including an idle agent. */
|
|
72
77
|
connect(): Promise<boolean>;
|
|
78
|
+
/** Replaces an existing browser pairing after an explicit user action. */
|
|
79
|
+
takeOver(): Promise<boolean>;
|
|
80
|
+
private pairWith;
|
|
73
81
|
private openEvents;
|
|
74
82
|
/**
|
|
75
83
|
* Dispatches one immutable prompt revision. The request enters `working`
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { reportAgentActivity, synchronizeAgentActivity } from "./activity.js";
|
|
1
2
|
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
2
3
|
import { isNudgeUiDev } from "../runtime/devFlag.js";
|
|
3
4
|
import { isDemoRuntime } from "../runtime/runtimeConfig.js";
|
|
4
|
-
import { HttpAgentBridgeTransport, shouldAutoConnectAgentBridge } from "./httpTransport.js";
|
|
5
|
+
import { AgentBridgeHttpError, HttpAgentBridgeTransport, shouldAutoConnectAgentBridge, } from "./httpTransport.js";
|
|
5
6
|
import { AGENT_PROTOCOL_VERSION, } from "./protocol.js";
|
|
6
7
|
const DISABLED_SNAPSHOT = Object.freeze({
|
|
7
8
|
state: "disabled",
|
|
@@ -9,6 +10,7 @@ const DISABLED_SNAPSHOT = Object.freeze({
|
|
|
9
10
|
listenerActive: false,
|
|
10
11
|
companionReachable: false,
|
|
11
12
|
paired: false,
|
|
13
|
+
pairedElsewhere: false,
|
|
12
14
|
request: null,
|
|
13
15
|
});
|
|
14
16
|
const DEFAULT_DISCOVERY_INTERVAL_MS = 2_000;
|
|
@@ -152,8 +154,10 @@ export class AgentClient {
|
|
|
152
154
|
started = false;
|
|
153
155
|
connection = "offline";
|
|
154
156
|
listenerActive = false;
|
|
157
|
+
agentClientName;
|
|
155
158
|
companionReachable = false;
|
|
156
159
|
paired = false;
|
|
160
|
+
pairedElsewhere = false;
|
|
157
161
|
lastError;
|
|
158
162
|
autoConnect;
|
|
159
163
|
autoConnectSuppressed = false;
|
|
@@ -211,6 +215,7 @@ export class AgentClient {
|
|
|
211
215
|
this.listenerActive = false;
|
|
212
216
|
this.companionReachable = false;
|
|
213
217
|
this.paired = false;
|
|
218
|
+
this.pairedElsewhere = false;
|
|
214
219
|
this.publish();
|
|
215
220
|
}
|
|
216
221
|
async restoreOrDiscover() {
|
|
@@ -291,12 +296,22 @@ export class AgentClient {
|
|
|
291
296
|
this.listenerActive = false;
|
|
292
297
|
this.companionReachable = false;
|
|
293
298
|
this.paired = false;
|
|
299
|
+
this.pairedElsewhere = false;
|
|
294
300
|
this.lastError = undefined;
|
|
295
301
|
this.publish();
|
|
296
302
|
void this.restoreOrDiscover();
|
|
297
303
|
}
|
|
298
304
|
/** Pairs the browser with a reachable companion, including an idle agent. */
|
|
299
305
|
async connect() {
|
|
306
|
+
return this.pairWith((request, signal) => this.transport.pair(request, signal));
|
|
307
|
+
}
|
|
308
|
+
/** Replaces an existing browser pairing after an explicit user action. */
|
|
309
|
+
async takeOver() {
|
|
310
|
+
if (!this.transport.takeOver)
|
|
311
|
+
return false;
|
|
312
|
+
return this.pairWith((request, signal) => this.transport.takeOver(request, signal));
|
|
313
|
+
}
|
|
314
|
+
async pairWith(pair) {
|
|
300
315
|
if (!this.enabled || !this.started || this.pairingAbort || this.paired || !this.companionReachable)
|
|
301
316
|
return false;
|
|
302
317
|
this.autoConnectSuppressed = false;
|
|
@@ -317,7 +332,7 @@ export class AgentClient {
|
|
|
317
332
|
this.discoveryAbort?.abort();
|
|
318
333
|
this.pairingAbort = abortController;
|
|
319
334
|
try {
|
|
320
|
-
const pairing = await
|
|
335
|
+
const pairing = await pair(request, abortController.signal);
|
|
321
336
|
if (!this.started || abortController.signal.aborted)
|
|
322
337
|
return false;
|
|
323
338
|
if (pairing.protocolVersion !== undefined && pairing.protocolVersion !== AGENT_PROTOCOL_VERSION) {
|
|
@@ -333,6 +348,7 @@ export class AgentClient {
|
|
|
333
348
|
this.connection = "paired";
|
|
334
349
|
this.listenerActive = pairing.status.listenerActive;
|
|
335
350
|
this.paired = true;
|
|
351
|
+
this.pairedElsewhere = false;
|
|
336
352
|
if (this.discoveryTimer !== null) {
|
|
337
353
|
clearTimeout(this.discoveryTimer);
|
|
338
354
|
this.discoveryTimer = null;
|
|
@@ -351,6 +367,9 @@ export class AgentClient {
|
|
|
351
367
|
this.listenerActive = idleListenerActive;
|
|
352
368
|
this.connection = idleListenerActive ? "listening" : "offline";
|
|
353
369
|
this.paired = false;
|
|
370
|
+
this.pairedElsewhere = error instanceof AgentBridgeHttpError
|
|
371
|
+
&& error.status === 409
|
|
372
|
+
&& error.code === "already_paired";
|
|
354
373
|
this.publish(idleListenerActive ? "available" : "disconnected");
|
|
355
374
|
return false;
|
|
356
375
|
}
|
|
@@ -467,6 +486,7 @@ export class AgentClient {
|
|
|
467
486
|
this.connection = "offline";
|
|
468
487
|
this.listenerActive = false;
|
|
469
488
|
this.paired = false;
|
|
489
|
+
this.pairedElsewhere = false;
|
|
470
490
|
this.lastError = reason === "reconnect" || reason === "browser_disconnected" ? undefined : reason;
|
|
471
491
|
this.publish();
|
|
472
492
|
if (token) {
|
|
@@ -503,12 +523,13 @@ export class AgentClient {
|
|
|
503
523
|
// pairing, so do not adopt `paired` until this tab restores the
|
|
504
524
|
// shared token. The next poll runs restore first in case a sibling
|
|
505
525
|
// tab has just stored that token.
|
|
526
|
+
const pairedElsewhere = status.paired && !this.sessionToken;
|
|
506
527
|
this.applyStatus(status.paired ? {
|
|
507
528
|
...status,
|
|
508
529
|
connection: status.listenerActive ? "listening" : "offline",
|
|
509
530
|
paired: false,
|
|
510
531
|
request: null,
|
|
511
|
-
} : status);
|
|
532
|
+
} : status, pairedElsewhere);
|
|
512
533
|
if (this.autoConnect
|
|
513
534
|
&& !status.paired
|
|
514
535
|
&& !this.autoConnectSuppressed
|
|
@@ -522,6 +543,7 @@ export class AgentClient {
|
|
|
522
543
|
this.listenerActive = false;
|
|
523
544
|
this.companionReachable = false;
|
|
524
545
|
this.paired = false;
|
|
546
|
+
this.pairedElsewhere = false;
|
|
525
547
|
this.lastError = undefined;
|
|
526
548
|
this.publish();
|
|
527
549
|
}
|
|
@@ -532,6 +554,7 @@ export class AgentClient {
|
|
|
532
554
|
this.listenerActive = false;
|
|
533
555
|
this.companionReachable = false;
|
|
534
556
|
this.paired = false;
|
|
557
|
+
this.pairedElsewhere = false;
|
|
535
558
|
// Discovery failures are expected while the companion is not running;
|
|
536
559
|
// retain a quiet disconnected state rather than flashing an error.
|
|
537
560
|
this.lastError = undefined;
|
|
@@ -562,6 +585,10 @@ export class AgentClient {
|
|
|
562
585
|
}, this.discoveryIntervalMs);
|
|
563
586
|
}
|
|
564
587
|
handleEvent(event) {
|
|
588
|
+
if (event.type === "activity") {
|
|
589
|
+
reportAgentActivity(this.projectId, event.activity);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
565
592
|
if (event.type === "status" || event.type === "connected") {
|
|
566
593
|
if (event.status.projectId !== this.projectId || event.status.protocolVersion !== AGENT_PROTOCOL_VERSION) {
|
|
567
594
|
this.handleDisconnect("The agent bridge project or protocol does not match this page.");
|
|
@@ -624,11 +651,13 @@ export class AgentClient {
|
|
|
624
651
|
// bounded timeout and reports any expired command to its agent host.
|
|
625
652
|
}
|
|
626
653
|
}
|
|
627
|
-
applyStatus(status) {
|
|
654
|
+
applyStatus(status, pairedElsewhere = false) {
|
|
628
655
|
this.companionReachable = true;
|
|
629
656
|
this.lastError = undefined;
|
|
630
657
|
this.listenerActive = status.listenerActive;
|
|
658
|
+
this.agentClientName = status.agentClientName;
|
|
631
659
|
this.paired = status.paired;
|
|
660
|
+
this.pairedElsewhere = pairedElsewhere;
|
|
632
661
|
this.connection = statusForConnection(status);
|
|
633
662
|
if (status.paired && this.discoveryTimer !== null) {
|
|
634
663
|
clearTimeout(this.discoveryTimer);
|
|
@@ -661,6 +690,7 @@ export class AgentClient {
|
|
|
661
690
|
this.listenerActive = false;
|
|
662
691
|
this.companionReachable = false;
|
|
663
692
|
this.paired = false;
|
|
693
|
+
this.pairedElsewhere = false;
|
|
664
694
|
this.publish();
|
|
665
695
|
}
|
|
666
696
|
applyRequestStatus(status, requestId, summary, error, changeRevision, clientDispatchId, sketches) {
|
|
@@ -712,7 +742,15 @@ export class AgentClient {
|
|
|
712
742
|
this.listenerActive = false;
|
|
713
743
|
this.companionReachable = false;
|
|
714
744
|
this.paired = false;
|
|
745
|
+
this.pairedElsewhere = false;
|
|
715
746
|
this.lastError = reason;
|
|
747
|
+
if (reason === "browser_disconnected") {
|
|
748
|
+
// A remote takeover deliberately invalidates this browser's token. Do
|
|
749
|
+
// not immediately race the new browser by auto-pairing again.
|
|
750
|
+
this.autoConnectSuppressed = true;
|
|
751
|
+
if (this.autoConnect)
|
|
752
|
+
setAutoConnectDisabled(this.projectId, true);
|
|
753
|
+
}
|
|
716
754
|
this.publish();
|
|
717
755
|
this.scheduleReconnect();
|
|
718
756
|
}
|
|
@@ -723,8 +761,10 @@ export class AgentClient {
|
|
|
723
761
|
state,
|
|
724
762
|
connection: this.connection,
|
|
725
763
|
listenerActive: this.listenerActive,
|
|
764
|
+
...(this.agentClientName === undefined ? {} : { agentClientName: this.agentClientName }),
|
|
726
765
|
companionReachable: this.companionReachable,
|
|
727
766
|
paired: this.paired,
|
|
767
|
+
pairedElsewhere: this.pairedElsewhere,
|
|
728
768
|
request: request ? {
|
|
729
769
|
requestId: request.requestId,
|
|
730
770
|
...(request.changeRevision === undefined ? {} : { changeRevision: request.changeRevision }),
|
|
@@ -756,17 +796,20 @@ export class AgentClient {
|
|
|
756
796
|
}
|
|
757
797
|
publish(stateOverride) {
|
|
758
798
|
this.snapshot = Object.freeze(this.makeSnapshot(stateOverride));
|
|
799
|
+
synchronizeAgentActivity(this.projectId, this.snapshot.request?.requestId, this.snapshot.paired && this.snapshot.request?.status === "working", this.snapshot.request?.clientDispatchId);
|
|
759
800
|
for (const listener of this.listeners)
|
|
760
801
|
listener();
|
|
761
802
|
}
|
|
762
803
|
}
|
|
763
804
|
let configuredTransport;
|
|
764
805
|
const clients = new Map();
|
|
806
|
+
const consumerCounts = new Map();
|
|
765
807
|
/** Sets a host/test transport for subsequently created project clients. */
|
|
766
808
|
export function configureAgentBridgeTransport(transport) {
|
|
767
809
|
for (const client of clients.values())
|
|
768
810
|
client.stop();
|
|
769
811
|
clients.clear();
|
|
812
|
+
consumerCounts.clear();
|
|
770
813
|
configuredTransport = transport;
|
|
771
814
|
}
|
|
772
815
|
/** Clears cached clients and their active streams. */
|
|
@@ -774,6 +817,22 @@ export function resetAgentClients() {
|
|
|
774
817
|
for (const client of clients.values())
|
|
775
818
|
client.stop();
|
|
776
819
|
clients.clear();
|
|
820
|
+
consumerCounts.clear();
|
|
821
|
+
}
|
|
822
|
+
function retainAgentClient(client) {
|
|
823
|
+
consumerCounts.set(client, (consumerCounts.get(client) ?? 0) + 1);
|
|
824
|
+
}
|
|
825
|
+
function releaseAgentClient(client) {
|
|
826
|
+
const count = consumerCounts.get(client);
|
|
827
|
+
// A reset already stopped the client and dropped its consumers.
|
|
828
|
+
if (count === undefined)
|
|
829
|
+
return;
|
|
830
|
+
if (count > 1) {
|
|
831
|
+
consumerCounts.set(client, count - 1);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
consumerCounts.delete(client);
|
|
835
|
+
client.stop();
|
|
777
836
|
}
|
|
778
837
|
export function createAgentClient(options) {
|
|
779
838
|
return new AgentClient({ ...options, transport: options.transport ?? configuredTransport });
|
|
@@ -792,8 +851,9 @@ export function getAgentClient(projectId, options = {}) {
|
|
|
792
851
|
export function useAgentClient(projectId, suppliedClient) {
|
|
793
852
|
const client = useMemo(() => suppliedClient ?? getAgentClient(projectId), [projectId, suppliedClient]);
|
|
794
853
|
useEffect(() => {
|
|
854
|
+
retainAgentClient(client);
|
|
795
855
|
client.start();
|
|
796
|
-
return () => client
|
|
856
|
+
return () => releaseAgentClient(client);
|
|
797
857
|
}, [client]);
|
|
798
858
|
return useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
799
859
|
}
|
|
@@ -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
|
}
|
|
@@ -15,9 +23,8 @@ export function getAgentConnectionStatus(snapshot) {
|
|
|
15
23
|
if (snapshot.paired && !snapshot.listenerActive) {
|
|
16
24
|
return {
|
|
17
25
|
kind: "connected-not-listening",
|
|
18
|
-
label: "
|
|
26
|
+
label: "MCP Ready - 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>;
|