nudge-ui 0.2.0 → 0.2.1

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 (201) hide show
  1. package/bin/nudge-ui.mjs +3 -1
  2. package/dist/cli/agentCommand.d.ts +8 -0
  3. package/dist/cli/agentCommand.js +59 -0
  4. package/dist/client.mjs +872 -12
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
  6. package/dist/hosts/astro/clientTransport.js +60 -11
  7. package/dist/hosts/next/sidecar.d.ts +4 -0
  8. package/dist/hosts/next/sidecar.js +49 -6
  9. package/dist/hosts/next/wrapper.js +30 -2
  10. package/dist/hosts/projectBridge.d.ts +20 -0
  11. package/dist/hosts/projectBridge.js +97 -0
  12. package/dist/hosts/static/cli.d.ts +1 -1
  13. package/dist/hosts/static/cli.js +6 -1
  14. package/dist/hosts/static/manifest.d.ts +1 -1
  15. package/dist/hosts/static/manifest.js +1 -1
  16. package/dist/hosts/static/server.js +44 -3
  17. package/dist/hosts/vite/vite.d.ts +5 -1
  18. package/dist/hosts/vite/vite.js +78 -14
  19. package/dist/inspector/agent/McpConnectionDialog.d.ts +2 -2
  20. package/dist/inspector/agent/McpConnectionDialog.js +11 -35
  21. package/dist/inspector/agent/client.d.ts +10 -3
  22. package/dist/inspector/agent/client.js +80 -9
  23. package/dist/inspector/agent/connectionStatus.js +3 -3
  24. package/dist/inspector/agent/httpTransport.d.ts +2 -0
  25. package/dist/inspector/agent/httpTransport.js +37 -17
  26. package/dist/inspector/agent/protocol.d.ts +8 -3
  27. package/dist/inspector/agent/protocol.js +1 -1
  28. package/dist/inspector/agent/verification.d.ts +1 -1
  29. package/dist/inspector/agent/verification.js +69 -12
  30. package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
  31. package/dist/inspector/canvas/CanvasCard.js +181 -91
  32. package/dist/inspector/canvas/CanvasElementOverlay.js +246 -52
  33. package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +24 -0
  35. package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
  36. package/dist/inspector/canvas/CanvasWorkspace.js +392 -73
  37. package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
  38. package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
  39. package/dist/inspector/canvas/canvasResize.d.ts +30 -0
  40. package/dist/inspector/canvas/canvasResize.js +52 -0
  41. package/dist/inspector/canvas/canvasStore.d.ts +30 -6
  42. package/dist/inspector/canvas/canvasStore.js +128 -61
  43. package/dist/inspector/canvas/frameProtocol.d.ts +69 -5
  44. package/dist/inspector/canvas/frameProtocol.js +56 -2
  45. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -0
  46. package/dist/inspector/canvas/keyboardShortcuts.js +14 -0
  47. package/dist/inspector/canvas/projection.d.ts +7 -0
  48. package/dist/inspector/canvas/projection.js +118 -25
  49. package/dist/inspector/canvas/rendererBootstrap.js +29 -29
  50. package/dist/inspector/canvas/rendererElementSelector.js +341 -30
  51. package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
  52. package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
  53. package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
  54. package/dist/inspector/canvas/sessionStore.d.ts +1 -0
  55. package/dist/inspector/canvas/sessionStore.js +12 -4
  56. package/dist/inspector/canvas/staleChangeDetector.js +16 -14
  57. package/dist/inspector/changes/changesLog.js +31 -1
  58. package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
  59. package/dist/inspector/changes/managedStyleProjection.js +3 -3
  60. package/dist/inspector/changes/structuralTypes.d.ts +4 -0
  61. package/dist/inspector/clientManifest.d.ts +5 -0
  62. package/dist/inspector/clientManifest.js +11 -0
  63. package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
  64. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
  65. package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
  66. package/dist/inspector/index.d.ts +2 -0
  67. package/dist/inspector/index.js +74 -24
  68. package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
  69. package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
  70. package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
  71. package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
  72. package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
  73. package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
  74. package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
  75. package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
  76. package/dist/inspector/inline-text/textEditChange.js +165 -0
  77. package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
  78. package/dist/inspector/inspection/selectionProperty.js +1 -0
  79. package/dist/inspector/overlay/selectedGeometry.js +21 -2
  80. package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
  81. package/dist/inspector/overlay/spacingGestures.js +353 -0
  82. package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
  83. package/dist/inspector/overlay/structuralGestures.js +12 -10
  84. package/dist/inspector/projection/managedStylesheet.js +4 -1
  85. package/dist/inspector/projection/renderedInstance.d.ts +2 -0
  86. package/dist/inspector/projection/renderedInstance.js +5 -1
  87. package/dist/inspector/projection/structuralProjection.d.ts +2 -0
  88. package/dist/inspector/projection/structuralProjection.js +85 -1
  89. package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
  90. package/dist/inspector/projection/textProjection.d.ts +2 -0
  91. package/dist/inspector/projection/textProjection.js +10 -4
  92. package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
  93. package/dist/inspector/prompt/clipboardHandoff.js +3 -3
  94. package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
  95. package/dist/inspector/prompt/generatePrompt.js +19 -2
  96. package/dist/inspector/runtime/editorShell.d.ts +2 -0
  97. package/dist/inspector/runtime/editorShell.js +4 -0
  98. package/dist/inspector/runtime/runtimeConfig.d.ts +4 -0
  99. package/dist/inspector/runtime/runtimeConfig.js +4 -0
  100. package/dist/inspector/selection/selectionStore.js +2 -0
  101. package/dist/inspector/settings/SettingsDialog.js +10 -0
  102. package/dist/inspector/shell/AppShell.js +5 -1
  103. package/dist/inspector/shell/ChangesLog.js +8 -4
  104. package/dist/inspector/shell/CopyPromptButton.js +95 -19
  105. package/dist/inspector/shell/EmptyState.js +1 -1
  106. package/dist/inspector/shell/InspectorShell.d.ts +0 -1
  107. package/dist/inspector/shell/InspectorShell.js +37 -38
  108. package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
  109. package/dist/inspector/shell/PeekOriginalButton.js +57 -0
  110. package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
  111. package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
  112. package/dist/inspector/shell/originalPreview.d.ts +9 -0
  113. package/dist/inspector/shell/originalPreview.js +61 -0
  114. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  115. package/dist/inspector/shell/shortcuts.js +15 -0
  116. package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
  117. package/dist/inspector/sketch/SketchChanges.js +72 -0
  118. package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
  119. package/dist/inspector/sketch/SketchEditor.js +402 -0
  120. package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
  121. package/dist/inspector/sketch/SketchEntryButton.js +11 -0
  122. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
  123. package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
  124. package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
  125. package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
  126. package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
  127. package/dist/inspector/sketch/SketchOverlay.js +420 -0
  128. package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
  129. package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
  130. package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
  131. package/dist/inspector/sketch/SketchWorkspace.js +152 -0
  132. package/dist/inspector/sketch/capture.d.ts +55 -0
  133. package/dist/inspector/sketch/capture.js +350 -0
  134. package/dist/inspector/sketch/freehand.d.ts +13 -0
  135. package/dist/inspector/sketch/freehand.js +64 -0
  136. package/dist/inspector/sketch/geometry.d.ts +17 -0
  137. package/dist/inspector/sketch/geometry.js +98 -0
  138. package/dist/inspector/sketch/handoff.d.ts +21 -0
  139. package/dist/inspector/sketch/handoff.js +72 -0
  140. package/dist/inspector/sketch/interaction.d.ts +21 -0
  141. package/dist/inspector/sketch/interaction.js +98 -0
  142. package/dist/inspector/sketch/model.d.ts +104 -0
  143. package/dist/inspector/sketch/model.js +282 -0
  144. package/dist/inspector/sketch/persistence.d.ts +21 -0
  145. package/dist/inspector/sketch/persistence.js +205 -0
  146. package/dist/inspector/sketch/raster.d.ts +22 -0
  147. package/dist/inspector/sketch/raster.js +203 -0
  148. package/dist/inspector/sketch/sketchNote.d.ts +10 -0
  149. package/dist/inspector/sketch/sketchNote.js +30 -0
  150. package/dist/inspector/sketch/store.d.ts +47 -0
  151. package/dist/inspector/sketch/store.js +409 -0
  152. package/dist/inspector/styleEditors/BorderEditor.js +15 -19
  153. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
  154. package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
  155. package/dist/inspector/styleEditors/ColorPicker.js +11 -36
  156. package/dist/inspector/styleEditors/GapField.d.ts +3 -2
  157. package/dist/inspector/styleEditors/GapField.js +2 -2
  158. package/dist/inspector/styleEditors/GridSection.js +1 -1
  159. package/dist/inspector/styleEditors/LayoutSection.js +11 -8
  160. package/dist/inspector/styleEditors/SpacingBox.js +23 -30
  161. package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
  162. package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
  163. package/dist/inspector/styleEditors/layoutValue.js +3 -0
  164. package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
  165. package/dist/inspector/styleEditors/nudgeValue.js +91 -5
  166. package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
  167. package/dist/inspector/styleEditors/stylePresence.js +22 -0
  168. package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
  169. package/dist/inspector/styleEditors/textRelevance.js +35 -0
  170. package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
  171. package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
  172. package/dist/inspector/tokens/TokenField.js +111 -7
  173. package/dist/inspector/tokens/editActions.d.ts +2 -0
  174. package/dist/inspector/tokens/editActions.js +2 -2
  175. package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
  176. package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
  177. package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
  178. package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
  179. package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
  180. package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
  181. package/dist/inspector/tokens/resolution.d.ts +3 -26
  182. package/dist/inspector/tokens/resolution.js +44 -735
  183. package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
  184. package/dist/inspector/tokens/tokenAvailability.js +204 -0
  185. package/dist/inspector/ui/Button.d.ts +1 -1
  186. package/dist/inspector/ui/SideValuesField.js +2 -2
  187. package/dist/inspector/ui/StatusCallout.d.ts +4 -1
  188. package/dist/inspector/ui/StatusCallout.js +2 -2
  189. package/dist/inspector/ui/styles.js +13 -11
  190. package/dist/internal/agent-protocol.d.ts +65 -1
  191. package/dist/internal/agent-protocol.js +183 -3
  192. package/dist/nudge-ui.mjs +261 -57
  193. package/dist/transport/editor.d.ts +28 -0
  194. package/dist/transport/editor.js +132 -0
  195. package/dist/transport/index.d.ts +1 -0
  196. package/dist/transport/index.js +1 -0
  197. package/dist/transport/routes.d.ts +7 -0
  198. package/dist/transport/routes.js +7 -0
  199. package/package.json +3 -1
  200. package/dist/inspector/shell/panelLayout.d.ts +0 -2
  201. package/dist/inspector/shell/panelLayout.js +0 -29
@@ -2,11 +2,12 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { basename, join, relative, resolve, sep, } from "node:path";
4
4
  import { createTokenInventory, } from "../../css/token-inventory/index.js";
5
- import { NUDGE_UI_CLIENT_PATH, NUDGE_UI_MANIFEST_PATH } from "../../transport/index.js";
5
+ import { NUDGE_UI_CLIENT_PATH, NUDGE_UI_EDITOR_PATH, NUDGE_UI_MANIFEST_PATH, createNudgeUiEditorDocument, isNudgeUiEditorDocumentRequest, } from "../../transport/index.js";
6
6
  import { discoverCssImportGraph, stripCssQuery } from "./tokens/activeStylesheets.js";
7
7
  import { catalogSourcePath, createViteStylesheetArtifact, isHostApplicationSource, isPackageStylesheet, orderViteStylesheetGraph, } from "./tokens/viteStylesheetArtifacts.js";
8
8
  import { interpretDialects } from "../../css/dialects/index.js";
9
9
  import { detectStylingSystem, } from "../../inspector/clientManifest.js";
10
+ import { loopbackOrigin, startOptionalProjectBridge, } from "../projectBridge.js";
10
11
  function isRecord(value) {
11
12
  return typeof value === "object" && value !== null && !Array.isArray(value);
12
13
  }
@@ -106,6 +107,8 @@ export function createVitePlugins(options = {}, createFramework = null) {
106
107
  let command = "serve";
107
108
  let demoBuild = false;
108
109
  let devServer;
110
+ let projectBridge = null;
111
+ let projectBridgeStart = null;
109
112
  let postTransformPromise = null;
110
113
  const inventory = createTokenInventory();
111
114
  const activeHostCssFiles = new Set();
@@ -473,14 +476,41 @@ export function createVitePlugins(options = {}, createFramework = null) {
473
476
  return postTransformPromise;
474
477
  }
475
478
  async function buildClientManifest() {
479
+ await projectBridgeStart;
476
480
  await ensurePostTransformCss();
477
481
  await ensurePublishedThemeContract();
478
482
  return {
479
483
  version: 1,
480
484
  revision: 0,
481
485
  runtime: buildRuntimeSnapshot(),
486
+ ...(projectBridge ? { agentBridge: projectBridge.browser } : {}),
482
487
  };
483
488
  }
489
+ function startProjectBridge(server) {
490
+ // The public demo uses the shared editor runtime without project-owned
491
+ // sessions, an agent bridge, or filesystem persistence.
492
+ if (options.demo === true || !root || projectBridgeStart)
493
+ return;
494
+ const address = server.httpServer?.address();
495
+ if (!address || typeof address === "string")
496
+ return;
497
+ const origin = server.resolvedUrls?.local[0]
498
+ ? new URL(server.resolvedUrls.local[0]).origin
499
+ : loopbackOrigin(address, server.config.server.https ? "https" : "http");
500
+ const allowedOrigins = [
501
+ ...(server.resolvedUrls?.local ?? []),
502
+ ...(server.resolvedUrls?.network ?? []),
503
+ ].map((url) => new URL(url).origin);
504
+ projectBridgeStart = startOptionalProjectBridge({
505
+ appRoot: root,
506
+ projectId: options.projectId ?? basename(root),
507
+ origin,
508
+ ...(allowedOrigins.length > 0 ? { allowedOrigins } : {}),
509
+ warn: (message) => server.config.logger.warn(message),
510
+ }).then((bridge) => {
511
+ projectBridge = bridge;
512
+ });
513
+ }
484
514
  function buildRuntimeSnapshot() {
485
515
  syncInventoryContributions();
486
516
  const snapshot = inventory.snapshot();
@@ -497,6 +527,9 @@ export function createVitePlugins(options = {}, createFramework = null) {
497
527
  host: framework?.hostLabel ?? "static-html",
498
528
  framework: framework?.framework ?? "HTML",
499
529
  stylingSystem: detectStylingSystem(snapshot.tokens),
530
+ ...(options.demo === true ? { demo: true } : {}),
531
+ ...(options.demoPages === undefined ? {} : { demoPages: options.demoPages }),
532
+ ...(options.demoCardLabels === undefined ? {} : { demoCardLabels: options.demoCardLabels }),
500
533
  capabilities: { canvas: true, componentSemantics: framework !== null },
501
534
  tokenCatalog: snapshot.definitions.map((definition) => ({
502
535
  ...definition,
@@ -536,16 +569,38 @@ export function createVitePlugins(options = {}, createFramework = null) {
536
569
  if (!enabled || command !== "serve")
537
570
  return;
538
571
  devServer = server;
572
+ if (server.httpServer?.listening)
573
+ startProjectBridge(server);
574
+ else
575
+ server.httpServer?.once("listening", () => startProjectBridge(server));
576
+ server.httpServer?.once("close", () => {
577
+ void projectBridgeStart?.then(() => projectBridge?.close()).finally(() => {
578
+ projectBridge = null;
579
+ projectBridgeStart = null;
580
+ });
581
+ });
539
582
  const runtimeWarning = framework?.unresolvedRuntimeWarning();
540
583
  if (runtimeWarning)
541
584
  server.config.logger.warn(runtimeWarning);
542
585
  server.middlewares?.use(async (request, response, next) => {
543
586
  const pathname = new URL(request.url ?? "/", "http://nudge-ui.local").pathname;
544
- if ((pathname === CLIENT_PATH || pathname === MANIFEST_PATH) && request.method !== "GET") {
587
+ const invalidReservedMethod = (pathname === CLIENT_PATH || pathname === MANIFEST_PATH)
588
+ ? request.method !== "GET"
589
+ : pathname === NUDGE_UI_EDITOR_PATH
590
+ && request.method !== "GET"
591
+ && request.method !== "HEAD";
592
+ if (invalidReservedMethod) {
545
593
  response.statusCode = 405;
546
594
  response.end();
547
595
  return;
548
596
  }
597
+ if (isNudgeUiEditorDocumentRequest(request.url ?? "/", request.method, request.headers)) {
598
+ response.statusCode = 200;
599
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
600
+ response.setHeader("Cache-Control", "no-store");
601
+ response.end(request.method === "HEAD" ? undefined : createNudgeUiEditorDocument());
602
+ return;
603
+ }
549
604
  if (pathname === CLIENT_PATH) {
550
605
  try {
551
606
  response.statusCode = 200;
@@ -628,29 +683,39 @@ export function createVitePlugins(options = {}, createFramework = null) {
628
683
  return `export {};\n`;
629
684
  }
630
685
  if (options.demo === true) {
631
- const landingDemoExpression = demoBuild
686
+ const demoControllerExpression = demoBuild
632
687
  ? "true"
633
688
  : '(import.meta.env.DEV && window.location.pathname === "/")';
634
689
  // Taken from the composed framework, so the demo bootstrap cannot drift
635
690
  // from the manifest the transport serves.
636
691
  const identity = buildRuntimeSnapshot();
637
692
  return [
638
- 'import { bootstrapNudgeUi, configureNudgeUiRuntime, detectFramework, setInspectorOpen } from "nudge-ui/internal/inspector";',
693
+ 'import { bootstrapNudgeUi, configureNudgeUiRuntime, createNudgeUiEditorUrl, detectFramework, hasNudgeUiDirectTabIntent, isCanvasRenderer, isNudgeUiDirectUrl, readNudgeUiEditorTarget, rememberNudgeUiDirectTabIntent } from "nudge-ui/internal/inspector";',
639
694
  'import { tokenCatalog, tokens, tokenDiagnostics, tokenGeneration, nudgeUiProjectId } from "virtual:design-tokens";',
640
695
  ...(framework
641
696
  ? [`import { componentContracts } from ${JSON.stringify(framework.virtualModuleId)};`]
642
697
  : []),
643
- 'const __nudge_ui_demo_frame = new URLSearchParams(window.location.search).get("nudgeDemo") === "1";',
644
- `const __nudge_ui_landing_demo = ${landingDemoExpression};`,
645
- 'const __nudge_ui_demo_runtime = __nudge_ui_demo_frame || __nudge_ui_landing_demo;',
646
- 'if (__nudge_ui_demo_runtime || (import.meta.env.DEV && window.location.pathname !== "/demo")) {',
698
+ 'const __nudge_ui_renderer = isCanvasRenderer();',
699
+ 'const __nudge_ui_explicit_direct = isNudgeUiDirectUrl(window.location.href);',
700
+ 'if (__nudge_ui_explicit_direct) rememberNudgeUiDirectTabIntent();',
701
+ 'const __nudge_ui_direct_tab = hasNudgeUiDirectTabIntent();',
702
+ `const __nudge_ui_demo_controller = !__nudge_ui_renderer && !__nudge_ui_explicit_direct && !__nudge_ui_direct_tab && ${demoControllerExpression};`,
703
+ 'const __nudge_ui_editor_target = readNudgeUiEditorTarget(window.location.href) ?? (__nudge_ui_demo_controller ? window.location.href : null);',
704
+ 'if (__nudge_ui_demo_controller && readNudgeUiEditorTarget(window.location.href) === null) {',
705
+ ' window.history.replaceState(window.history.state, "", createNudgeUiEditorUrl(window.location.href));',
706
+ '}',
707
+ 'if (__nudge_ui_editor_target) document.documentElement.setAttribute("data-nudge-ui-editor", "");',
708
+ 'if (__nudge_ui_renderer) document.documentElement.setAttribute("data-nudge-ui-renderer", "");',
709
+ 'if (__nudge_ui_editor_target || __nudge_ui_renderer) {',
647
710
  ' configureNudgeUiRuntime({',
648
711
  ' projectId: nudgeUiProjectId,',
649
712
  ` host: ${JSON.stringify(identity.host)},`,
650
713
  ` framework: ${JSON.stringify(identity.framework)},`,
651
714
  ' stylingSystem: detectFramework(tokens).stylingSystem,',
652
- ' ...(__nudge_ui_demo_runtime ? { demo: true } : {}),',
653
- ` capabilities: { canvas: __nudge_ui_demo_runtime ? false : true, componentSemantics: ${String(framework !== null)} },`,
715
+ ' demo: true,',
716
+ ` demoPages: ${JSON.stringify(options.demoPages ?? [])},`,
717
+ ` demoCardLabels: ${JSON.stringify(options.demoCardLabels ?? [])},`,
718
+ ` capabilities: { canvas: true, componentSemantics: ${String(framework !== null)} },`,
654
719
  ' tokenCatalog,',
655
720
  ' tokens,',
656
721
  ' tokenDiagnostics,',
@@ -660,12 +725,11 @@ export function createVitePlugins(options = {}, createFramework = null) {
660
725
  ' const __dt_root = document.getElementById("nudge-ui-root");',
661
726
  ' if (__dt_root) {',
662
727
  ' bootstrapNudgeUi(__dt_root);',
663
- ' if (__nudge_ui_demo_runtime) {',
664
- ' setInspectorOpen(false);',
665
- ' window.addEventListener("nudge-ui:open", () => setInspectorOpen(true));',
666
- ' }',
667
728
  ' }',
668
729
  '}',
730
+ 'window.addEventListener("nudge-ui:open", () => {',
731
+ ' if (!__nudge_ui_renderer && (__nudge_ui_explicit_direct || __nudge_ui_direct_tab)) window.location.assign(createNudgeUiEditorUrl(window.location.href));',
732
+ '});',
669
733
  ].join("\n");
670
734
  }
671
735
  return null;
@@ -1,8 +1,8 @@
1
1
  import type { ReactElement } from "react";
2
2
  import type { AgentClientSnapshot } from "./client.js";
3
3
  export declare const MCP_DOCS_URL = "https://github.com/charlessmart/nudge-ui#connect-a-coding-agent";
4
- /** Builds the project-local add-mcp command shown by the connection panel. */
5
- export declare function createMcpSetupCommand(projectId: string, origin: string): string;
4
+ /** Uses the guided installer so browser diagnostics never become shell arguments. */
5
+ export declare function createMcpSetupCommand(): string;
6
6
  export interface McpConnectionContentProps {
7
7
  readonly projectId: string;
8
8
  readonly origin: string;
@@ -8,32 +8,9 @@ import { Button } from "../ui/Button.js";
8
8
  import { SegmentedControl } from "../ui/SegmentedControl.js";
9
9
  import { StatusCallout } from "../ui/StatusCallout.js";
10
10
  export const MCP_DOCS_URL = "https://github.com/charlessmart/nudge-ui#connect-a-coding-agent";
11
- /** Quotes one value for a POSIX shell command. */
12
- function shellQuote(value) {
13
- return `'${value.replaceAll("'", "'\\''")}'`;
14
- }
15
- function shellArgument(value) {
16
- // Keep the usual command readable while quoting values that could be
17
- // interpreted as shell syntax when add-mcp runs the nested command.
18
- return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : shellQuote(value);
19
- }
20
- /** Builds the project-local add-mcp command shown by the connection panel. */
21
- export function createMcpSetupCommand(projectId, origin) {
22
- const serverCommand = [
23
- "npx -y @nudge-ui/mcp@latest",
24
- "--project-id",
25
- shellArgument(projectId),
26
- "--origin",
27
- shellArgument(origin),
28
- "--workspace-root .",
29
- ].join(" ");
30
- return `npx add-mcp ${shellQuote(serverCommand)} --name nudge_ui`;
31
- }
32
- function formatMcpSetupCommand(command) {
33
- return command
34
- .replace(" --project-id ", "\n --project-id ")
35
- .replace(" --origin ", "\n --origin ")
36
- .replace(" --workspace-root ", "\n --workspace-root ");
11
+ /** Uses the guided installer so browser diagnostics never become shell arguments. */
12
+ export function createMcpSetupCommand() {
13
+ return "npx nudge-ui agent setup";
37
14
  }
38
15
  function portalContainer() {
39
16
  return typeof document !== "undefined"
@@ -52,17 +29,16 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
52
29
  const [copiedListener, setCopiedListener] = useState(false);
53
30
  const [copyError, setCopyError] = useState();
54
31
  const [checkError, setCheckError] = useState();
55
- const setupCommand = useMemo(() => createMcpSetupCommand(projectId, origin), [origin, projectId]);
56
- const displaySetupCommand = useMemo(() => formatMcpSetupCommand(setupCommand), [setupCommand]);
32
+ const setupCommand = createMcpSetupCommand();
57
33
  const setupPrompt = useMemo(() => [
58
- "Please configure the Nudge MCP companion for this project.",
34
+ "Please set up the Nudge coding-agent integration for this application.",
59
35
  "",
60
- "Run this command from the project root:",
36
+ "Run this command from the application directory and select this coding agent:",
61
37
  setupCommand,
62
38
  "",
63
- "Then restart or refresh the MCP server so Nudge can connect.",
39
+ "Start the application normally. Reload the agent if needed, then listen to Nudge.",
64
40
  ].join("\n"), [setupCommand]);
65
- const listenerInstruction = "Call nudge_listen now and wait for my Nudge UI prompt. After applying it, call nudge_report_status and listen again.";
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.";
66
42
  const status = getAgentConnectionStatus(snapshot);
67
43
  const canConnect = snapshot.companionReachable
68
44
  && !snapshot.paired
@@ -118,10 +94,10 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
118
94
  setCopyError("The listener instruction could not be copied. Select it and copy it manually.");
119
95
  }
120
96
  }
121
- 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.companionReachable, title: "Configure the MCP host", children: [_jsx(SegmentedControl, { "aria-label": "MCP setup method", className: "mcp-connection__setup-tabs", "data-test": "mcp-setup-tabs", value: setupMethod, options: [
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: [
122
98
  { value: "ai", label: "AI instructions", testId: "mcp-setup-tab-ai" },
123
99
  { value: "terminal", label: "Terminal command", testId: "mcp-setup-tab-terminal" },
124
- ], onChange: setSetupMethod }), setupMethod === "terminal" ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Run this command from your project root." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-command", children: displaySetupCommand }) }), _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 this prompt into your coding agent to configure Nudge MCP." }), _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.paired, title: "Connect this page", children: [_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] })] }), _jsxs(TimelineStep, { number: 3, complete: snapshot.listenerActive, title: "Start the agent listener", children: [_jsxs("p", { className: "mcp-connection__copy", children: ["Ask your coding agent to call ", _jsx("code", { children: "nudge_listen" }), " and keep it active."] }), _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 listener instruction"] }) })] })] }), 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"] })] }));
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"] })] }));
125
101
  }
126
102
  /**
127
103
  * Explains and controls the project-scoped MCP companion connection.
@@ -131,5 +107,5 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
131
107
  */
132
108
  export function McpConnectionDialog({ open, projectId, origin, snapshot, onOpenChange, onConnect, onDisconnect, onCheckAgain, }) {
133
109
  const closeRef = useRef(null);
134
- 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: "Pair this page with your coding agent. You can connect before the agent starts listening." })] }), _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 })] })] }) }));
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 })] })] }) }));
135
111
  }
@@ -1,8 +1,10 @@
1
- import { type AgentBridgeTransport, type AgentClientOptions, type AgentRequestStatus, type AgentStatusSnapshot, type PromptDispatchResponse } from "./protocol.js";
1
+ import { type AgentBridgeTransport, type AgentClientOptions, type AgentSketchAttachment, type AgentSketchMetadata, type AgentRequestStatus, type AgentStatusSnapshot, type PromptDispatchResponse } from "./protocol.js";
2
2
  export type AgentClientState = "disabled" | "disconnected" | "available" | "pairing" | "connected" | "working" | "completed" | "failed" | "interrupted";
3
3
  export interface AgentRequestSnapshot {
4
4
  readonly requestId: string;
5
5
  readonly changeRevision?: number;
6
+ readonly clientDispatchId?: string;
7
+ readonly sketches?: readonly AgentSketchMetadata[];
6
8
  readonly status: AgentRequestStatus;
7
9
  readonly summary?: string;
8
10
  readonly error?: string;
@@ -20,7 +22,7 @@ export interface AgentClientSnapshot {
20
22
  readonly error?: string;
21
23
  }
22
24
  /** Creates a stable numeric revision for one prompt snapshot. */
23
- export declare function createPromptRevision(changes: readonly unknown[], structuralChanges?: readonly unknown[]): number;
25
+ export declare function createPromptRevision(changes: readonly unknown[], structuralChanges?: readonly unknown[], sketches?: readonly unknown[]): number;
24
26
  /**
25
27
  * Browser-side companion client.
26
28
  *
@@ -50,6 +52,8 @@ export declare class AgentClient {
50
52
  private companionReachable;
51
53
  private paired;
52
54
  private lastError;
55
+ private readonly autoConnect;
56
+ private autoConnectSuppressed;
53
57
  constructor(options: AgentClientOptions);
54
58
  /** Installs the controller callback used to acknowledge Canvas commands. */
55
59
  setCanvasCommandHandler(handler: AgentClientOptions["canvasCommandHandler"]): void;
@@ -72,7 +76,10 @@ export declare class AgentClient {
72
76
  * synchronously, before the transport promise resolves, so the UI cannot
73
77
  * issue a second prompt while the first one is in flight.
74
78
  */
75
- dispatchPrompt(prompt: string, changeRevision?: number): Promise<PromptDispatchResponse | null>;
79
+ dispatchPrompt(prompt: string, changeRevision?: number, options?: {
80
+ readonly clientDispatchId?: string;
81
+ readonly attachments?: readonly AgentSketchAttachment[];
82
+ }): Promise<PromptDispatchResponse | null>;
76
83
  /** Interrupts local request state and asks the companion to revoke pairing. */
77
84
  disconnect(reason?: string): void;
78
85
  private discover;
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo, useSyncExternalStore } from "react";
2
2
  import { isNudgeUiDev } from "../runtime/devFlag.js";
3
3
  import { isDemoRuntime } from "../runtime/runtimeConfig.js";
4
- import { HttpAgentBridgeTransport } from "./httpTransport.js";
4
+ import { HttpAgentBridgeTransport, shouldAutoConnectAgentBridge } from "./httpTransport.js";
5
5
  import { AGENT_PROTOCOL_VERSION, } from "./protocol.js";
6
6
  const DISABLED_SNAPSHOT = Object.freeze({
7
7
  state: "disabled",
@@ -13,6 +13,7 @@ const DISABLED_SNAPSHOT = Object.freeze({
13
13
  });
14
14
  const DEFAULT_DISCOVERY_INTERVAL_MS = 2_000;
15
15
  const SESSION_STORAGE_PREFIX = "nudge-ui-agent-session:";
16
+ const AUTO_CONNECT_DISABLED_PREFIX = "nudge-ui-agent-auto-connect-disabled:";
16
17
  function randomId(prefix) {
17
18
  const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
18
19
  ? crypto.randomUUID()
@@ -68,6 +69,31 @@ function clearStoredSession(projectId) {
68
69
  // Ignore storage failures; disconnect still clears the live connection.
69
70
  }
70
71
  }
72
+ function autoConnectDisabled(projectId) {
73
+ if (typeof localStorage === "undefined")
74
+ return false;
75
+ try {
76
+ return localStorage.getItem(`${AUTO_CONNECT_DISABLED_PREFIX}${encodeURIComponent(projectId)}`) === "true";
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ function setAutoConnectDisabled(projectId, disabled) {
83
+ if (typeof localStorage === "undefined")
84
+ return;
85
+ try {
86
+ const key = `${AUTO_CONNECT_DISABLED_PREFIX}${encodeURIComponent(projectId)}`;
87
+ if (disabled)
88
+ localStorage.setItem(key, "true");
89
+ else
90
+ localStorage.removeItem(key);
91
+ }
92
+ catch {
93
+ // Privacy settings can disable storage. The current document still keeps
94
+ // the explicit disconnect preference in memory.
95
+ }
96
+ }
71
97
  function statusForConnection(status) {
72
98
  if (status.connection === "working" || status.request?.status === "working")
73
99
  return "working";
@@ -90,10 +116,10 @@ function hashRevision(value) {
90
116
  return (hash >>> 0) & 0x7fff_ffff;
91
117
  }
92
118
  /** Creates a stable numeric revision for one prompt snapshot. */
93
- export function createPromptRevision(changes, structuralChanges = []) {
119
+ export function createPromptRevision(changes, structuralChanges = [], sketches = []) {
94
120
  let serialized;
95
121
  try {
96
- serialized = JSON.stringify({ changes, structuralChanges }) ?? "";
122
+ serialized = JSON.stringify({ changes, structuralChanges, sketches }) ?? "";
97
123
  }
98
124
  catch {
99
125
  serialized = `${changes.length}:${structuralChanges.length}`;
@@ -129,12 +155,16 @@ export class AgentClient {
129
155
  companionReachable = false;
130
156
  paired = false;
131
157
  lastError;
158
+ autoConnect;
159
+ autoConnectSuppressed = false;
132
160
  constructor(options) {
133
161
  this.projectId = options.projectId;
134
162
  this.origin = options.origin ?? (typeof window !== "undefined" ? window.location.origin : "http://localhost");
135
163
  this.transport = options.transport ?? new HttpAgentBridgeTransport(options.endpoint);
136
164
  this.discoveryIntervalMs = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
137
165
  this.canvasCommandHandler = options.canvasCommandHandler;
166
+ this.autoConnect = options.autoConnect ?? shouldAutoConnectAgentBridge();
167
+ this.autoConnectSuppressed = this.autoConnect && autoConnectDisabled(this.projectId);
138
168
  // The public demo renders the real inspector but intentionally keeps the
139
169
  // local MCP/agent bridge inert.
140
170
  this.enabled = isNudgeUiDev() && !isDemoRuntime();
@@ -269,6 +299,8 @@ export class AgentClient {
269
299
  async connect() {
270
300
  if (!this.enabled || !this.started || this.pairingAbort || this.paired || !this.companionReachable)
271
301
  return false;
302
+ this.autoConnectSuppressed = false;
303
+ setAutoConnectDisabled(this.projectId, false);
272
304
  if (this.connection === "working" || this.activeRequest?.status === "working")
273
305
  return false;
274
306
  const idleListenerActive = this.listenerActive;
@@ -351,7 +383,7 @@ export class AgentClient {
351
383
  * synchronously, before the transport promise resolves, so the UI cannot
352
384
  * issue a second prompt while the first one is in flight.
353
385
  */
354
- async dispatchPrompt(prompt, changeRevision) {
386
+ async dispatchPrompt(prompt, changeRevision, options = {}) {
355
387
  if (!this.enabled || !this.started || !this.paired || !this.listenerActive || !this.sessionToken)
356
388
  return null;
357
389
  if (!prompt || this.activeRequest?.status === "working")
@@ -362,12 +394,17 @@ export class AgentClient {
362
394
  sessionToken: this.sessionToken,
363
395
  prompt,
364
396
  ...(changeRevision === undefined ? {} : { changeRevision }),
397
+ ...(options.clientDispatchId === undefined ? {} : { clientDispatchId: options.clientDispatchId }),
398
+ ...(options.attachments === undefined ? {} : { attachments: options.attachments }),
365
399
  };
400
+ const sketches = options.attachments?.map(({ data: _data, ...metadata }) => metadata);
366
401
  this.activeRequest = {
367
402
  localRequestId,
368
403
  requestId: localRequestId,
369
404
  prompt,
370
405
  ...(changeRevision === undefined ? {} : { changeRevision }),
406
+ ...(options.clientDispatchId === undefined ? {} : { clientDispatchId: options.clientDispatchId }),
407
+ ...(sketches === undefined ? {} : { sketches }),
371
408
  status: "working",
372
409
  };
373
410
  this.connection = "working";
@@ -381,7 +418,12 @@ export class AgentClient {
381
418
  return null;
382
419
  if (response?.request?.requestId) {
383
420
  this.activeRequest = this.activeRequest && this.activeRequest.localRequestId === localRequestId
384
- ? { ...this.activeRequest, requestId: response.request.requestId }
421
+ ? {
422
+ ...this.activeRequest,
423
+ requestId: response.request.requestId,
424
+ ...(response.request.clientDispatchId === undefined ? {} : { clientDispatchId: response.request.clientDispatchId }),
425
+ ...(response.request.sketches === undefined ? {} : { sketches: response.request.sketches }),
426
+ }
385
427
  : this.activeRequest;
386
428
  }
387
429
  if (response?.status && response.status !== "working") {
@@ -405,6 +447,9 @@ export class AgentClient {
405
447
  }
406
448
  /** Interrupts local request state and asks the companion to revoke pairing. */
407
449
  disconnect(reason = "browser_disconnected") {
450
+ this.autoConnectSuppressed = true;
451
+ if (this.autoConnect)
452
+ setAutoConnectDisabled(this.projectId, true);
408
453
  const token = this.sessionToken;
409
454
  const active = this.activeRequest;
410
455
  if (active?.status === "working") {
@@ -454,7 +499,21 @@ export class AgentClient {
454
499
  else {
455
500
  this.companionReachable = true;
456
501
  if (this.connection !== "paired" && this.connection !== "working" && !this.pairingAbort) {
457
- this.applyStatus(status);
502
+ // Discovery is unauthenticated. Another browser tab can own the
503
+ // pairing, so do not adopt `paired` until this tab restores the
504
+ // shared token. The next poll runs restore first in case a sibling
505
+ // tab has just stored that token.
506
+ this.applyStatus(status.paired ? {
507
+ ...status,
508
+ connection: status.listenerActive ? "listening" : "offline",
509
+ paired: false,
510
+ request: null,
511
+ } : status);
512
+ if (this.autoConnect
513
+ && !status.paired
514
+ && !this.autoConnectSuppressed
515
+ && !autoConnectDisabled(this.projectId))
516
+ void this.connect();
458
517
  }
459
518
  }
460
519
  }
@@ -491,7 +550,7 @@ export class AgentClient {
491
550
  return;
492
551
  this.discoveryTimer = setTimeout(() => {
493
552
  this.discoveryTimer = null;
494
- void this.discover();
553
+ void this.restoreOrDiscover();
495
554
  }, this.discoveryIntervalMs);
496
555
  }
497
556
  scheduleReconnect() {
@@ -538,6 +597,12 @@ export class AgentClient {
538
597
  },
539
598
  };
540
599
  }
600
+ // Every tab with the shared browser session receives the SSE command, but
601
+ // only the Canvas lease owner may answer it. A read-only tab reports this
602
+ // local sentinel; leave the command pending for the owning tab instead of
603
+ // racing its acknowledgement with a false failure.
604
+ if (!acknowledgement.ok && acknowledgement.error?.code === "workspace-locked")
605
+ return;
541
606
  if (acknowledgement.commandId !== command.commandId) {
542
607
  acknowledgement = {
543
608
  commandId: command.commandId,
@@ -570,7 +635,7 @@ export class AgentClient {
570
635
  this.discoveryTimer = null;
571
636
  }
572
637
  if (status.request) {
573
- this.applyRequestStatus(status.request.status, status.request.requestId, status.request.summary, status.request.error, status.request.changeRevision);
638
+ this.applyRequestStatus(status.request.status, status.request.requestId, status.request.summary, status.request.error, status.request.changeRevision, status.request.clientDispatchId, status.request.sketches);
574
639
  }
575
640
  else if (status.listenerActive && status.paired && this.activeRequest?.status !== "working") {
576
641
  // A re-armed MCP listen call is the lifecycle boundary for the previous
@@ -598,7 +663,7 @@ export class AgentClient {
598
663
  this.paired = false;
599
664
  this.publish();
600
665
  }
601
- applyRequestStatus(status, requestId, summary, error, changeRevision) {
666
+ applyRequestStatus(status, requestId, summary, error, changeRevision, clientDispatchId, sketches) {
602
667
  const active = this.activeRequest;
603
668
  if (active && active.requestId !== requestId && active.localRequestId !== requestId) {
604
669
  // A fresh server id can arrive before the POST response. Associate it
@@ -613,6 +678,8 @@ export class AgentClient {
613
678
  requestId,
614
679
  prompt: "",
615
680
  ...(changeRevision === undefined ? {} : { changeRevision }),
681
+ ...(clientDispatchId === undefined ? {} : { clientDispatchId }),
682
+ ...(sketches === undefined ? {} : { sketches }),
616
683
  status,
617
684
  ...(summary === undefined ? {} : { summary }),
618
685
  ...(error === undefined ? {} : { error }),
@@ -624,6 +691,8 @@ export class AgentClient {
624
691
  requestId,
625
692
  status,
626
693
  ...(changeRevision === undefined ? {} : { changeRevision }),
694
+ ...(clientDispatchId === undefined ? {} : { clientDispatchId }),
695
+ ...(sketches === undefined ? {} : { sketches }),
627
696
  ...(summary === undefined ? {} : { summary }),
628
697
  ...(error === undefined ? {} : { error }),
629
698
  };
@@ -659,6 +728,8 @@ export class AgentClient {
659
728
  request: request ? {
660
729
  requestId: request.requestId,
661
730
  ...(request.changeRevision === undefined ? {} : { changeRevision: request.changeRevision }),
731
+ ...(request.clientDispatchId === undefined ? {} : { clientDispatchId: request.clientDispatchId }),
732
+ ...(request.sketches === undefined ? {} : { sketches: request.sketches }),
662
733
  status: request.status,
663
734
  ...(request.summary === undefined ? {} : { summary: request.summary }),
664
735
  ...(request.error === undefined ? {} : { error: request.error }),
@@ -15,20 +15,20 @@ export function getAgentConnectionStatus(snapshot) {
15
15
  if (snapshot.paired && !snapshot.listenerActive) {
16
16
  return {
17
17
  kind: "connected-not-listening",
18
- label: "Connected, not listening",
18
+ label: "Project connected · Ask agent to listen",
19
19
  tone: "accent",
20
20
  action: { kind: "setup", label: "Set up" },
21
21
  };
22
22
  }
23
23
  if (!snapshot.companionReachable) {
24
- return { kind: "not-found", label: "Companion not found", tone: "neutral" };
24
+ return { kind: "not-found", label: "Project bridge unavailable", tone: "neutral" };
25
25
  }
26
26
  if (snapshot.listenerActive) {
27
27
  return { kind: "listening", label: "Agent listening", tone: "accent" };
28
28
  }
29
29
  return {
30
30
  kind: "ready-to-connect",
31
- label: "Ready to connect agent",
31
+ label: "Project available · Connect this page",
32
32
  tone: "accent",
33
33
  action: { kind: "connect", label: "Connect" },
34
34
  };
@@ -1,4 +1,6 @@
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
+ /** Returns whether the dev host supplied a trusted project bridge endpoint. */
3
+ export declare function shouldAutoConnectAgentBridge(): boolean;
2
4
  /**
3
5
  * HTTP/SSE browser adapter for the companion's canonical endpoints.
4
6
  *