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.
Files changed (258) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +252 -54
  3. package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
  4. package/dist/css/value-semantics/colorSemantics.js +17 -0
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
  6. package/dist/hosts/astro/clientTransport.js +41 -32
  7. package/dist/hosts/astro/integration.d.ts +2 -2
  8. package/dist/hosts/astro/integration.js +9 -4
  9. package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
  10. package/dist/hosts/astro/markdownIdentity.js +81 -0
  11. package/dist/hosts/environment.d.ts +15 -0
  12. package/dist/hosts/environment.js +16 -0
  13. package/dist/hosts/next/manifest.js +1 -1
  14. package/dist/hosts/next/sidecar.js +12 -2
  15. package/dist/hosts/next/wrapper.d.ts +2 -0
  16. package/dist/hosts/next/wrapper.js +2 -1
  17. package/dist/hosts/projectBridge.d.ts +1 -0
  18. package/dist/hosts/projectBridge.js +1 -0
  19. package/dist/hosts/static/manifest.js +3 -1
  20. package/dist/hosts/static/server.js +4 -0
  21. package/dist/hosts/vite/vite.d.ts +4 -0
  22. package/dist/hosts/vite/vite.js +28 -5
  23. package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
  24. package/dist/inspector/agent/McpConnectionDialog.js +23 -22
  25. package/dist/inspector/agent/activity.d.ts +18 -0
  26. package/dist/inspector/agent/activity.js +70 -0
  27. package/dist/inspector/agent/client.d.ts +8 -0
  28. package/dist/inspector/agent/client.js +65 -5
  29. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  30. package/dist/inspector/agent/connectionStatus.js +9 -2
  31. package/dist/inspector/agent/httpTransport.d.ts +6 -0
  32. package/dist/inspector/agent/httpTransport.js +50 -3
  33. package/dist/inspector/agent/protocol.d.ts +9 -3
  34. package/dist/inspector/agent/protocol.js +1 -1
  35. package/dist/inspector/agent/verification.d.ts +11 -1
  36. package/dist/inspector/agent/verification.js +53 -25
  37. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
  38. package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
  39. package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
  40. package/dist/inspector/canvas/CanvasCard.js +133 -38
  41. package/dist/inspector/canvas/CanvasElementOverlay.js +200 -22
  42. package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
  43. package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
  44. package/dist/inspector/canvas/CanvasToolbar.js +8 -4
  45. package/dist/inspector/canvas/CanvasWorkspace.js +243 -56
  46. package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
  47. package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
  48. package/dist/inspector/canvas/agentPresentation.js +64 -52
  49. package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
  50. package/dist/inspector/canvas/canvasGestures.js +1 -1
  51. package/dist/inspector/canvas/canvasStore.d.ts +46 -82
  52. package/dist/inspector/canvas/canvasStore.js +251 -284
  53. package/dist/inspector/canvas/frameContent.d.ts +21 -0
  54. package/dist/inspector/canvas/frameContent.js +44 -0
  55. package/dist/inspector/canvas/frameProtocol.d.ts +34 -4
  56. package/dist/inspector/canvas/frameProtocol.js +9 -2
  57. package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -1
  58. package/dist/inspector/canvas/keyboardShortcuts.js +5 -3
  59. package/dist/inspector/canvas/layout.d.ts +52 -0
  60. package/dist/inspector/canvas/layout.js +106 -0
  61. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  62. package/dist/inspector/canvas/linkEligibility.js +0 -29
  63. package/dist/inspector/canvas/projection.d.ts +15 -5
  64. package/dist/inspector/canvas/projection.js +69 -13
  65. package/dist/inspector/canvas/rendererBootstrap.js +45 -4
  66. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  67. package/dist/inspector/canvas/rendererElementSelector.js +102 -45
  68. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  69. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  70. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  71. package/dist/inspector/canvas/sessionStore.js +43 -183
  72. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  73. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  74. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  75. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  76. package/dist/inspector/canvas/viewStore.js +104 -0
  77. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  78. package/dist/inspector/canvas/wheelPan.js +8 -0
  79. package/dist/inspector/changes/changesLog.d.ts +6 -5
  80. package/dist/inspector/changes/changesLog.js +40 -32
  81. package/dist/inspector/changes/draftChanges.d.ts +56 -0
  82. package/dist/inspector/changes/draftChanges.js +300 -0
  83. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  84. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  85. package/dist/inspector/clientManifest.d.ts +3 -1
  86. package/dist/inspector/clientManifest.js +1 -0
  87. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  88. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  89. package/dist/inspector/comments/element.d.ts +6 -0
  90. package/dist/inspector/comments/element.js +47 -0
  91. package/dist/inspector/comments/store.d.ts +29 -0
  92. package/dist/inspector/comments/store.js +121 -0
  93. package/dist/inspector/comments/verification.d.ts +5 -0
  94. package/dist/inspector/comments/verification.js +52 -0
  95. package/dist/inspector/drafts/model.d.ts +35 -0
  96. package/dist/inspector/drafts/model.js +45 -0
  97. package/dist/inspector/drafts/store.d.ts +28 -0
  98. package/dist/inspector/drafts/store.js +231 -0
  99. package/dist/inspector/index.js +26 -35
  100. package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
  101. package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
  102. package/dist/inspector/iterations/capture.d.ts +2 -0
  103. package/dist/inspector/iterations/capture.js +173 -0
  104. package/dist/inspector/iterations/client.d.ts +7 -0
  105. package/dist/inspector/iterations/client.js +52 -0
  106. package/dist/inspector/iterations/coordinator.d.ts +15 -0
  107. package/dist/inspector/iterations/coordinator.js +130 -0
  108. package/dist/inspector/iterations/prompt.d.ts +3 -0
  109. package/dist/inspector/iterations/prompt.js +26 -0
  110. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  111. package/dist/inspector/overlay/DomNavigator.js +46 -0
  112. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  113. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  114. package/dist/inspector/overlay/clickPolicy.js +34 -3
  115. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  116. package/dist/inspector/overlay/elementSelector.js +18 -7
  117. package/dist/inspector/overlay/interactionStyles.js +3 -1
  118. package/dist/inspector/overlay/spacingGestures.d.ts +15 -3
  119. package/dist/inspector/overlay/spacingGestures.js +88 -13
  120. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  121. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  122. package/dist/inspector/projection/renderedInstance.d.ts +2 -1
  123. package/dist/inspector/projection/renderedInstance.js +9 -5
  124. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  125. package/dist/inspector/projection/structuralProjection.js +19 -15
  126. package/dist/inspector/projection/textProjection.d.ts +2 -1
  127. package/dist/inspector/projection/textProjection.js +8 -4
  128. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  129. package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
  130. package/dist/inspector/prompt/clipboardHandoff.js +92 -143
  131. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  132. package/dist/inspector/prompt/generatePrompt.js +17 -16
  133. package/dist/inspector/runtime/runtimeConfig.d.ts +10 -1
  134. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  135. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  136. package/dist/inspector/selection/hierarchy.js +8 -0
  137. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  138. package/dist/inspector/settings/SettingsDialog.js +22 -9
  139. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  140. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  141. package/dist/inspector/shell/AppShell.js +4 -2
  142. package/dist/inspector/shell/ChangesLog.js +47 -27
  143. package/dist/inspector/shell/CopyPromptButton.js +146 -90
  144. package/dist/inspector/shell/EmptyState.js +9 -8
  145. package/dist/inspector/shell/InspectorShell.js +14 -25
  146. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  147. package/dist/inspector/shell/originalPreview.js +5 -5
  148. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  149. package/dist/inspector/shell/shortcuts.js +11 -0
  150. package/dist/inspector/sketch/SketchEditor.js +14 -14
  151. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  152. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  153. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  154. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  155. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  156. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  157. package/dist/inspector/sketch/interaction.d.ts +1 -0
  158. package/dist/inspector/sketch/interaction.js +8 -4
  159. package/dist/inspector/sketch/model.d.ts +5 -0
  160. package/dist/inspector/sketch/model.js +6 -2
  161. package/dist/inspector/sketch/raster.d.ts +1 -2
  162. package/dist/inspector/sketch/raster.js +68 -4
  163. package/dist/inspector/sketch/store.js +48 -23
  164. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  165. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  166. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  167. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  168. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  169. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  170. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  171. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  172. package/dist/inspector/styleEditors/GapField.js +3 -5
  173. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  174. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  175. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  176. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  177. package/dist/inspector/styleEditors/GridSection.js +1 -1
  178. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  179. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  180. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  181. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  182. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  183. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  184. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  185. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  186. package/dist/inspector/styleEditors/SpacingBox.js +16 -9
  187. package/dist/inspector/styleEditors/Typography.js +11 -3
  188. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  189. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  190. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  191. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  192. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  193. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  194. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  195. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  196. package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
  197. package/dist/inspector/styleEditors/nudgeValue.js +41 -11
  198. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  199. package/dist/inspector/tokens/TokenField.js +49 -22
  200. package/dist/inspector/tokens/TokenLabel.js +5 -14
  201. package/dist/inspector/tokens/TokensPanel.js +2 -1
  202. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  203. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  204. package/dist/inspector/tokens/editActions.d.ts +1 -1
  205. package/dist/inspector/tokens/editActions.js +1 -1
  206. package/dist/inspector/ui/AtRuleContext.js +4 -6
  207. package/dist/inspector/ui/Button.d.ts +6 -2
  208. package/dist/inspector/ui/Button.js +4 -3
  209. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  210. package/dist/inspector/ui/ControlSurface.js +4 -3
  211. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  212. package/dist/inspector/ui/Disclosure.js +13 -0
  213. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  214. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  215. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  216. package/dist/inspector/ui/InspectorPopover.js +4 -5
  217. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  218. package/dist/inspector/ui/PopoverListbox.js +8 -7
  219. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  220. package/dist/inspector/ui/SegmentedControl.js +2 -2
  221. package/dist/inspector/ui/Select.d.ts +6 -1
  222. package/dist/inspector/ui/Select.js +7 -11
  223. package/dist/inspector/ui/SideValuesField.js +1 -1
  224. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  225. package/dist/inspector/ui/Tooltip.js +32 -0
  226. package/dist/inspector/ui/classNames.d.ts +2 -0
  227. package/dist/inspector/ui/classNames.js +4 -0
  228. package/dist/inspector/ui/customCursors.d.ts +23 -0
  229. package/dist/inspector/ui/customCursors.js +122 -0
  230. package/dist/inspector/ui/dragCursor.d.ts +6 -0
  231. package/dist/inspector/ui/dragCursor.js +13 -0
  232. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  233. package/dist/inspector/ui/portalContainer.js +6 -0
  234. package/dist/inspector/ui/styles.js +34 -24
  235. package/dist/inspector/workspace/commands.d.ts +9 -0
  236. package/dist/inspector/workspace/commands.js +82 -0
  237. package/dist/inspector/workspace/controller.d.ts +4 -0
  238. package/dist/inspector/workspace/controller.js +66 -0
  239. package/dist/inspector/workspace/handoff.d.ts +42 -0
  240. package/dist/inspector/workspace/handoff.js +115 -0
  241. package/dist/inspector/workspace/timeline.d.ts +30 -0
  242. package/dist/inspector/workspace/timeline.js +108 -0
  243. package/dist/internal/agent-protocol.d.ts +19 -2
  244. package/dist/internal/agent-protocol.js +13 -0
  245. package/dist/nudge-ui.mjs +560 -299
  246. package/dist/project/artifacts.d.ts +13 -0
  247. package/dist/project/artifacts.js +253 -0
  248. package/dist/transport/artifacts.d.ts +8 -0
  249. package/dist/transport/artifacts.js +9 -0
  250. package/dist/transport/editor.d.ts +20 -1
  251. package/dist/transport/editor.js +44 -11
  252. package/dist/transport/routes.d.ts +6 -1
  253. package/dist/transport/routes.js +6 -1
  254. package/package.json +6 -2
  255. package/dist/inspector/changes/workspaceChanges.d.ts +0 -43
  256. package/dist/inspector/changes/workspaceChanges.js +0 -239
  257. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  258. 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, IconClipboard, IconExternalLink, IconPlugConnected, IconRefresh, IconUnlink, IconX, } from "@tabler/icons-react";
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 { SegmentedControl } from "../ui/SegmentedControl.js";
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 [copiedListener, setCopiedListener] = useState(false);
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 listenerInstruction = "Listen to Nudge for this application in the current worktree. Use nudge_list_sessions if selection is ambiguous, then call nudge_listen. After applying each prompt, call nudge_report_status and listen again until I ask you to stop.";
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 handleCopyListener() {
90
+ async function handleCopyDoctor() {
87
91
  setCopyError(undefined);
88
92
  try {
89
- await copyToClipboard(listenerInstruction);
90
- setCopiedListener(true);
91
- window.setTimeout(() => setCopiedListener(false), 1500);
93
+ await copyToClipboard(doctorCommand);
94
+ setCopiedDoctor(true);
95
+ window.setTimeout(() => setCopiedDoctor(false), 1500);
92
96
  }
93
97
  catch {
94
- setCopyError("The listener instruction could not be copied. Select it and copy it manually.");
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 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
- { 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 this.transport.pair(request, abortController.signal);
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.stop();
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" | "setup";
9
- readonly label: "Connect" | "Set up";
8
+ readonly kind: "connect" | "takeover";
9
+ readonly label: "Connect" | "Take over";
10
10
  };
11
11
  }
12
12
  /** Returns the user-facing connection state shared by the prompt UI and MCP panel. */
@@ -6,6 +6,14 @@ export function getAgentConnectionStatus(snapshot) {
6
6
  if (snapshot.state === "pairing") {
7
7
  return { kind: "connecting", label: "Connecting…", tone: "accent" };
8
8
  }
9
+ if (snapshot.pairedElsewhere) {
10
+ return {
11
+ kind: "paired-elsewhere",
12
+ label: "MCP active in another tab or window",
13
+ tone: "warning",
14
+ action: { kind: "takeover", label: "Take over" },
15
+ };
16
+ }
9
17
  if (!snapshot.paired && (snapshot.state === "working" || snapshot.request?.status === "working")) {
10
18
  return { kind: "working-disconnected", label: "Disconnected · Last work status unknown", tone: "warning" };
11
19
  }
@@ -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: "Project connected · Ask agent to listen",
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>;