nudge-ui 0.2.2 → 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 (226) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +252 -61
  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 +38 -32
  7. package/dist/hosts/astro/integration.js +5 -1
  8. package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
  9. package/dist/hosts/astro/markdownIdentity.js +81 -0
  10. package/dist/hosts/next/manifest.js +1 -1
  11. package/dist/hosts/next/sidecar.js +5 -1
  12. package/dist/hosts/static/manifest.js +1 -1
  13. package/dist/hosts/static/server.js +3 -0
  14. package/dist/hosts/vite/vite.d.ts +3 -0
  15. package/dist/hosts/vite/vite.js +24 -4
  16. package/dist/inspector/agent/McpConnectionDialog.js +13 -19
  17. package/dist/inspector/agent/activity.d.ts +18 -0
  18. package/dist/inspector/agent/activity.js +70 -0
  19. package/dist/inspector/agent/client.d.ts +2 -0
  20. package/dist/inspector/agent/client.js +29 -1
  21. package/dist/inspector/agent/connectionStatus.js +1 -1
  22. package/dist/inspector/agent/httpTransport.js +9 -2
  23. package/dist/inspector/agent/protocol.d.ts +6 -3
  24. package/dist/inspector/agent/protocol.js +1 -1
  25. package/dist/inspector/agent/verification.d.ts +11 -1
  26. package/dist/inspector/agent/verification.js +53 -25
  27. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
  28. package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
  29. package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
  30. package/dist/inspector/canvas/CanvasCard.js +133 -38
  31. package/dist/inspector/canvas/CanvasElementOverlay.js +152 -16
  32. package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
  33. package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +8 -4
  35. package/dist/inspector/canvas/CanvasWorkspace.js +225 -57
  36. package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
  37. package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
  38. package/dist/inspector/canvas/agentPresentation.js +64 -52
  39. package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
  40. package/dist/inspector/canvas/canvasGestures.js +1 -1
  41. package/dist/inspector/canvas/canvasStore.d.ts +46 -82
  42. package/dist/inspector/canvas/canvasStore.js +251 -284
  43. package/dist/inspector/canvas/frameContent.d.ts +21 -0
  44. package/dist/inspector/canvas/frameContent.js +44 -0
  45. package/dist/inspector/canvas/frameProtocol.d.ts +21 -4
  46. package/dist/inspector/canvas/frameProtocol.js +3 -3
  47. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -1
  48. package/dist/inspector/canvas/keyboardShortcuts.js +4 -4
  49. package/dist/inspector/canvas/layout.d.ts +52 -0
  50. package/dist/inspector/canvas/layout.js +106 -0
  51. package/dist/inspector/canvas/projection.d.ts +15 -5
  52. package/dist/inspector/canvas/projection.js +69 -13
  53. package/dist/inspector/canvas/rendererBootstrap.js +32 -4
  54. package/dist/inspector/canvas/rendererElementSelector.js +47 -6
  55. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  56. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  57. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  58. package/dist/inspector/canvas/sessionStore.js +43 -183
  59. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  60. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  61. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  62. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  63. package/dist/inspector/canvas/viewStore.js +104 -0
  64. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  65. package/dist/inspector/canvas/wheelPan.js +8 -0
  66. package/dist/inspector/changes/changesLog.d.ts +6 -5
  67. package/dist/inspector/changes/changesLog.js +40 -32
  68. package/dist/inspector/changes/draftChanges.d.ts +56 -0
  69. package/dist/inspector/changes/draftChanges.js +300 -0
  70. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  71. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  72. package/dist/inspector/clientManifest.d.ts +1 -1
  73. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  74. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  75. package/dist/inspector/comments/element.d.ts +6 -0
  76. package/dist/inspector/comments/element.js +47 -0
  77. package/dist/inspector/comments/store.d.ts +29 -0
  78. package/dist/inspector/comments/store.js +121 -0
  79. package/dist/inspector/comments/verification.d.ts +5 -0
  80. package/dist/inspector/comments/verification.js +52 -0
  81. package/dist/inspector/drafts/model.d.ts +35 -0
  82. package/dist/inspector/drafts/model.js +45 -0
  83. package/dist/inspector/drafts/store.d.ts +28 -0
  84. package/dist/inspector/drafts/store.js +231 -0
  85. package/dist/inspector/index.js +20 -27
  86. package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
  87. package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
  88. package/dist/inspector/iterations/capture.d.ts +2 -0
  89. package/dist/inspector/iterations/capture.js +173 -0
  90. package/dist/inspector/iterations/client.d.ts +7 -0
  91. package/dist/inspector/iterations/client.js +52 -0
  92. package/dist/inspector/iterations/coordinator.d.ts +15 -0
  93. package/dist/inspector/iterations/coordinator.js +130 -0
  94. package/dist/inspector/iterations/prompt.d.ts +3 -0
  95. package/dist/inspector/iterations/prompt.js +26 -0
  96. package/dist/inspector/overlay/DomNavigator.js +21 -15
  97. package/dist/inspector/overlay/interactionStyles.js +3 -1
  98. package/dist/inspector/overlay/spacingGestures.d.ts +13 -1
  99. package/dist/inspector/overlay/spacingGestures.js +82 -5
  100. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  101. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  102. package/dist/inspector/projection/renderedInstance.d.ts +2 -1
  103. package/dist/inspector/projection/renderedInstance.js +9 -5
  104. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  105. package/dist/inspector/projection/structuralProjection.js +19 -15
  106. package/dist/inspector/projection/textProjection.d.ts +2 -1
  107. package/dist/inspector/projection/textProjection.js +8 -4
  108. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  109. package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
  110. package/dist/inspector/prompt/clipboardHandoff.js +92 -143
  111. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  112. package/dist/inspector/prompt/generatePrompt.js +17 -16
  113. package/dist/inspector/runtime/runtimeConfig.d.ts +9 -0
  114. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  115. package/dist/inspector/settings/SettingsDialog.js +21 -8
  116. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  117. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  118. package/dist/inspector/shell/AppShell.js +4 -2
  119. package/dist/inspector/shell/ChangesLog.js +46 -26
  120. package/dist/inspector/shell/CopyPromptButton.js +138 -89
  121. package/dist/inspector/shell/EmptyState.js +9 -8
  122. package/dist/inspector/shell/InspectorShell.js +13 -21
  123. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  124. package/dist/inspector/shell/originalPreview.js +5 -5
  125. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  126. package/dist/inspector/shell/shortcuts.js +11 -0
  127. package/dist/inspector/sketch/SketchEditor.js +14 -14
  128. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  129. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  130. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  131. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  132. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  133. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  134. package/dist/inspector/sketch/interaction.d.ts +1 -0
  135. package/dist/inspector/sketch/interaction.js +8 -4
  136. package/dist/inspector/sketch/model.d.ts +5 -0
  137. package/dist/inspector/sketch/model.js +6 -2
  138. package/dist/inspector/sketch/raster.d.ts +1 -2
  139. package/dist/inspector/sketch/raster.js +68 -4
  140. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  141. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  142. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  143. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  144. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  145. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  146. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  147. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  148. package/dist/inspector/styleEditors/GapField.js +3 -5
  149. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  150. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  151. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  152. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  153. package/dist/inspector/styleEditors/GridSection.js +1 -1
  154. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  155. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  156. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  157. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  158. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  159. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  160. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  161. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  162. package/dist/inspector/styleEditors/SpacingBox.js +7 -5
  163. package/dist/inspector/styleEditors/Typography.js +11 -3
  164. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  165. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  166. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  167. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  168. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  169. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  170. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  171. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  172. package/dist/inspector/styleEditors/nudgeValue.d.ts +2 -2
  173. package/dist/inspector/styleEditors/nudgeValue.js +36 -8
  174. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  175. package/dist/inspector/tokens/TokenField.js +29 -21
  176. package/dist/inspector/tokens/TokenLabel.js +5 -14
  177. package/dist/inspector/tokens/editActions.d.ts +1 -1
  178. package/dist/inspector/tokens/editActions.js +1 -1
  179. package/dist/inspector/ui/AtRuleContext.js +4 -6
  180. package/dist/inspector/ui/Button.d.ts +6 -2
  181. package/dist/inspector/ui/Button.js +4 -3
  182. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  183. package/dist/inspector/ui/ControlSurface.js +4 -3
  184. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  185. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  186. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  187. package/dist/inspector/ui/InspectorPopover.js +4 -5
  188. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  189. package/dist/inspector/ui/PopoverListbox.js +8 -7
  190. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  191. package/dist/inspector/ui/SegmentedControl.js +2 -2
  192. package/dist/inspector/ui/Select.d.ts +6 -1
  193. package/dist/inspector/ui/Select.js +7 -11
  194. package/dist/inspector/ui/SideValuesField.js +1 -1
  195. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  196. package/dist/inspector/ui/Tooltip.js +32 -0
  197. package/dist/inspector/ui/classNames.d.ts +2 -0
  198. package/dist/inspector/ui/classNames.js +4 -0
  199. package/dist/inspector/ui/customCursors.d.ts +23 -0
  200. package/dist/inspector/ui/customCursors.js +122 -0
  201. package/dist/inspector/ui/dragCursor.d.ts +0 -2
  202. package/dist/inspector/ui/dragCursor.js +2 -10
  203. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  204. package/dist/inspector/ui/portalContainer.js +6 -0
  205. package/dist/inspector/ui/styles.js +29 -21
  206. package/dist/inspector/workspace/commands.d.ts +9 -0
  207. package/dist/inspector/workspace/commands.js +82 -0
  208. package/dist/inspector/workspace/controller.d.ts +4 -0
  209. package/dist/inspector/workspace/controller.js +66 -0
  210. package/dist/inspector/workspace/handoff.d.ts +42 -0
  211. package/dist/inspector/workspace/handoff.js +115 -0
  212. package/dist/inspector/workspace/timeline.d.ts +30 -0
  213. package/dist/inspector/workspace/timeline.js +108 -0
  214. package/dist/internal/agent-protocol.d.ts +19 -2
  215. package/dist/internal/agent-protocol.js +13 -0
  216. package/dist/nudge-ui.mjs +558 -311
  217. package/dist/project/artifacts.d.ts +13 -0
  218. package/dist/project/artifacts.js +253 -0
  219. package/dist/transport/artifacts.d.ts +8 -0
  220. package/dist/transport/artifacts.js +9 -0
  221. package/dist/transport/editor.js +3 -1
  222. package/dist/transport/routes.d.ts +2 -0
  223. package/dist/transport/routes.js +2 -0
  224. package/package.json +6 -2
  225. package/dist/inspector/changes/workspaceChanges.d.ts +0 -47
  226. package/dist/inspector/changes/workspaceChanges.js +0 -265
@@ -78,6 +78,10 @@ export declare function interpretColorValue(value: string, ctx: ColorSemanticsCo
78
78
  * unparseable opacities return an explicit `ok: false`.
79
79
  */
80
80
  export declare function applyColorOpacity(value: string, opacity: string): ColorEditResult;
81
+ /** Applies opacity using RGB channels for hex colors so field edits retain percentage precision. */
82
+ export declare function withColorOpacity(color: string, opacity: string): ColorEditResult;
83
+ /** Preserves the previous opacity when the replacement color has no explicit alpha. */
84
+ export declare function replaceColorPreservingOpacity(previous: string, color: string, context: ColorSemanticsContext): ColorEditResult;
81
85
  /**
82
86
  * Replaces only the color token in a separable color expression. Keeping this
83
87
  * separate from a plain token swap preserves authored alpha modifiers, for
@@ -398,6 +398,23 @@ export function applyColorOpacity(value, opacity) {
398
398
  }
399
399
  return { ok: false, reason: "unsupported" };
400
400
  }
401
+ /** Applies opacity using RGB channels for hex colors so field edits retain percentage precision. */
402
+ export function withColorOpacity(color, opacity) {
403
+ const hex = /^#([\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i.exec(color.trim());
404
+ if (hex) {
405
+ const raw = hex[1];
406
+ const channels = rgbTuple(`#${raw.slice(0, raw.length <= 4 ? 3 : 6)}`);
407
+ color = `rgb(${channels.join(" ")} / 100%)`;
408
+ }
409
+ return applyColorOpacity(color, opacity);
410
+ }
411
+ /** Preserves the previous opacity when the replacement color has no explicit alpha. */
412
+ export function replaceColorPreservingOpacity(previous, color, context) {
413
+ const opacity = interpretColorOpacity(previous, context);
414
+ return opacity && !colorValueHasEmbeddedAlpha(color)
415
+ ? withColorOpacity(color, opacity.value)
416
+ : { ok: true, value: color };
417
+ }
401
418
  function tokenReferenceName(entry) {
402
419
  const name = entry.cssName ?? entry.name;
403
420
  return name.startsWith("--") ? name : null;
@@ -14,6 +14,7 @@ export function createAstroRuntimeConfig(knowledge) {
14
14
  capabilities: {
15
15
  canvas: true,
16
16
  componentSemantics: true,
17
+ domNavigation: true,
17
18
  sourceCoordinates: ASTRO_SOURCE_COORDINATES,
18
19
  scopingSelectorPattern: ASTRO_SCOPING_SELECTOR_PATTERN,
19
20
  },
@@ -1,3 +1,4 @@
1
+ import { handleHtmlArtifactRequest } from "../../project/artifacts.js";
1
2
  import {} from "../../inspector/clientManifest.js";
2
3
  import { createAstroRuntimeConfig } from "./astroRuntimeConfig.js";
3
4
  import { createAstroClientAssetHandler } from "./clientAsset.js";
@@ -17,38 +18,9 @@ export function createAstroClientTransportPlugin() {
17
18
  apply: "serve",
18
19
  configureServer(server) {
19
20
  server.middlewares.use((request, response, next) => {
20
- const pathname = new URL(request.url ?? "/", "http://nudge-ui.local").pathname;
21
- if (isNudgeUiEditorDocumentRequest(request.url ?? "/", request.method, request.headers)) {
22
- if (request.method !== "GET" && request.method !== "HEAD") {
23
- response.statusCode = 405;
24
- response.setHeader("Allow", "GET, HEAD");
25
- response.end();
26
- return;
27
- }
28
- response.statusCode = 200;
29
- response.setHeader("Content-Type", "text/html; charset=utf-8");
30
- response.setHeader("Cache-Control", "no-store");
31
- response.end(request.method === "HEAD" ? undefined : createNudgeUiEditorDocument());
32
- return;
33
- }
34
- if (pathname === NUDGE_UI_CLIENT_PATH) {
35
- void serveClient(request, response).catch(next);
36
- return;
37
- }
38
- if (pathname === NUDGE_UI_MANIFEST_PATH) {
39
- void createManifest(server, request, (bridge) => {
40
- projectBridge = bridge;
41
- })
42
- .then((manifest) => {
43
- response.statusCode = 200;
44
- response.setHeader("Content-Type", "application/json; charset=utf-8");
45
- response.setHeader("Cache-Control", "no-store");
46
- response.end(JSON.stringify(manifest));
47
- })
48
- .catch(next);
49
- return;
50
- }
51
- next();
21
+ void handleHtmlArtifactRequest(request, response, server.config.root)
22
+ .then((handled) => { if (!handled)
23
+ handleClientRequest(server, request, response, next); }).catch(next);
52
24
  });
53
25
  },
54
26
  async closeBundle() {
@@ -56,6 +28,40 @@ export function createAstroClientTransportPlugin() {
56
28
  projectBridge = null;
57
29
  },
58
30
  };
31
+ function handleClientRequest(server, request, response, next) {
32
+ const pathname = new URL(request.url ?? "/", "http://nudge-ui.local").pathname;
33
+ if (isNudgeUiEditorDocumentRequest(request.url ?? "/", request.method, request.headers)) {
34
+ if (request.method !== "GET" && request.method !== "HEAD") {
35
+ response.statusCode = 405;
36
+ response.setHeader("Allow", "GET, HEAD");
37
+ response.end();
38
+ return;
39
+ }
40
+ response.statusCode = 200;
41
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
42
+ response.setHeader("Cache-Control", "no-store");
43
+ response.end(request.method === "HEAD" ? undefined : createNudgeUiEditorDocument());
44
+ return;
45
+ }
46
+ if (pathname === NUDGE_UI_CLIENT_PATH) {
47
+ void serveClient(request, response).catch(next);
48
+ return;
49
+ }
50
+ if (pathname === NUDGE_UI_MANIFEST_PATH) {
51
+ void createManifest(server, request, (bridge) => {
52
+ projectBridge = bridge;
53
+ })
54
+ .then((manifest) => {
55
+ response.statusCode = 200;
56
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
57
+ response.setHeader("Cache-Control", "no-store");
58
+ response.end(JSON.stringify(manifest));
59
+ })
60
+ .catch(next);
61
+ return;
62
+ }
63
+ next();
64
+ }
59
65
  }
60
66
  async function createManifest(server, request, setBridge) {
61
67
  // SAFETY: these virtual modules are generated by the shared Nudge UI plugin
@@ -1,8 +1,10 @@
1
+ import { fileURLToPath } from "node:url";
1
2
  import { nudgeUi } from "../vite/index.js";
2
3
  import { NUDGE_UI_CLIENT_PATH, NUDGE_UI_MANIFEST_PATH } from "../../transport/index.js";
3
4
  import { createAstroClientTransportPlugin, } from "./clientTransport.js";
4
5
  import { createProjectContextPlugin } from "./projectContext.js";
5
6
  import { isNudgeUiEnabled } from "../environment.js";
7
+ import { installMarkdownIdentity } from "./markdownIdentity.js";
6
8
  /**
7
9
  * Adds the prebuilt client as an external module. The inspector dependency
8
10
  * graph therefore never enters Astro's Vite optimizer.
@@ -43,14 +45,16 @@ export function nudgeUiAstro(options = {}) {
43
45
  return {
44
46
  name: "nudge-ui",
45
47
  hooks: {
46
- "astro:config:setup"({ command, updateConfig, injectScript, addMiddleware }) {
48
+ "astro:config:setup"({ command, config, updateConfig, injectScript, addMiddleware }) {
47
49
  if (!enabled || command !== "dev")
48
50
  return;
51
+ const markdown = installMarkdownIdentity(config.markdown, fileURLToPath(config.root));
49
52
  // The shared plugin objects are plain Vite plugin records, but this
50
53
  // monorepo typechecks them against a different Vite major than the
51
54
  // installed Astro resolves internally; the runtime contract
52
55
  // (config/resolveId/load/transform hooks) is identical.
53
56
  updateConfig({
57
+ ...(markdown ? { markdown } : {}),
54
58
  vite: {
55
59
  // SAFETY: Astro and this package resolve different Vite type
56
60
  // versions, but both consume the same runtime Plugin contract.
@@ -0,0 +1,28 @@
1
+ import type { Element, Root } from "hast";
2
+ /** Adds authored Markdown positions through the remark/rehype pipeline. */
3
+ export declare function createMarkdownIdentityPlugin(projectRoot: string): () => (tree: Root, file: {
4
+ path?: string;
5
+ }) => void;
6
+ /** Adds the same identity through Astro's Sätteri visitor API. */
7
+ export declare function createSatteriIdentityPlugin(projectRoot: string): {
8
+ name: string;
9
+ options: {
10
+ position: boolean;
11
+ };
12
+ element: {
13
+ filter: string[];
14
+ visit(node: Readonly<Element>, context: {
15
+ fileURL: URL | undefined;
16
+ setProperty(node: Readonly<Element>, key: string, value: string): void;
17
+ }): void;
18
+ };
19
+ };
20
+ /** Extends supported processors without replacing the consumer's pipeline. */
21
+ export declare function installMarkdownIdentity(markdown: {
22
+ processor?: {
23
+ name: string;
24
+ options: object;
25
+ };
26
+ }, projectRoot: string): {
27
+ rehypePlugins: ReturnType<typeof createMarkdownIdentityPlugin>[];
28
+ } | undefined;
@@ -0,0 +1,81 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const PROSE_TAGS = [
4
+ "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li",
5
+ "blockquote", "a", "em", "strong", "del", "code", "pre", "hr", "br",
6
+ "table", "thead", "tbody", "tr", "th", "td",
7
+ ];
8
+ const proseTags = new Set(PROSE_TAGS);
9
+ function attributesFor(node, file, projectRoot) {
10
+ const start = node.position?.start;
11
+ if (!start || start.line < 1 || start.column < 1 || !proseTags.has(node.tagName))
12
+ return {};
13
+ const relative = path.relative(projectRoot, file).replaceAll("\\", "/");
14
+ const attributes = {};
15
+ const properties = node.properties;
16
+ if (properties["data-cid"] === undefined && properties.dataCid === undefined) {
17
+ attributes["data-cid"] = `astro:${node.tagName.charAt(0).toUpperCase()}${node.tagName.slice(1)}`;
18
+ }
19
+ if (properties["data-src"] === undefined && properties.dataSrc === undefined) {
20
+ attributes["data-src"] = `${relative}:${start.line}:${start.column}`;
21
+ }
22
+ return attributes;
23
+ }
24
+ /** Adds authored Markdown positions through the remark/rehype pipeline. */
25
+ export function createMarkdownIdentityPlugin(projectRoot) {
26
+ return function nudgeMarkdownIdentity() {
27
+ return (tree, file) => {
28
+ if (!file.path || !/\.mdx?$/.test(file.path))
29
+ return;
30
+ const sourceFile = file.path;
31
+ const visit = (node) => {
32
+ if (node.type === "element") {
33
+ Object.assign(node.properties, attributesFor(node, sourceFile, projectRoot));
34
+ }
35
+ if ("children" in node) {
36
+ for (const child of node.children)
37
+ visit(child);
38
+ }
39
+ };
40
+ visit(tree);
41
+ };
42
+ };
43
+ }
44
+ /** Adds the same identity through Astro's Sätteri visitor API. */
45
+ export function createSatteriIdentityPlugin(projectRoot) {
46
+ return {
47
+ name: "nudge-ui-markdown-identity",
48
+ options: { position: true },
49
+ element: {
50
+ filter: PROSE_TAGS,
51
+ visit(node, context) {
52
+ if (!context.fileURL || context.fileURL.protocol !== "file:")
53
+ return;
54
+ const file = fileURLToPath(context.fileURL);
55
+ if (!/\.mdx?$/.test(file))
56
+ return;
57
+ for (const [key, value] of Object.entries(attributesFor(node, file, projectRoot))) {
58
+ context.setProperty(node, key, value);
59
+ }
60
+ },
61
+ },
62
+ };
63
+ }
64
+ /** Extends supported processors without replacing the consumer's pipeline. */
65
+ export function installMarkdownIdentity(markdown, projectRoot) {
66
+ const processor = markdown.processor;
67
+ if (!processor)
68
+ return { rehypePlugins: [createMarkdownIdentityPlugin(projectRoot)] };
69
+ if (processor.name !== "unified" && processor.name !== "satteri")
70
+ return undefined;
71
+ // SAFETY: Astro's supported processors expose mutable plugin arrays. This
72
+ // adapter only appends plugins and does not inspect existing entries.
73
+ const options = processor.options;
74
+ if (processor.name === "unified") {
75
+ (options.rehypePlugins ??= []).push(createMarkdownIdentityPlugin(projectRoot));
76
+ }
77
+ else if (processor.name === "satteri") {
78
+ (options.hastPlugins ??= []).push(createSatteriIdentityPlugin(projectRoot));
79
+ }
80
+ return undefined;
81
+ }
@@ -15,7 +15,7 @@ export function buildManifest(input) {
15
15
  stylingSystem: "CSS custom properties",
16
16
  // Canvas shares the Vite host's controller/renderer runtime (ADR-0006);
17
17
  // the mount in every document bootstraps as a renderer inside cards.
18
- capabilities: { canvas: true, componentSemantics: true },
18
+ capabilities: { canvas: true, componentSemantics: true, domNavigation: true },
19
19
  tokenCatalog: [],
20
20
  tokens: [],
21
21
  tokenDiagnostics: [],
@@ -1,3 +1,4 @@
1
+ import { handleHtmlArtifactRequest } from "../../project/artifacts.js";
1
2
  import { createServer } from "node:http";
2
3
  import { createRequire } from "node:module";
3
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -299,7 +300,10 @@ export async function ensureSidecar(root, options = {}) {
299
300
  contractsTimer = setTimeout(flushContracts, 120);
300
301
  };
301
302
  const server = createServer((req, res) => {
302
- respond(req, res, () => manifest, () => generation, streams, receiveContracts);
303
+ void handleHtmlArtifactRequest(req, res, fsRoot).then((handled) => {
304
+ if (!handled)
305
+ respond(req, res, () => manifest, () => generation, streams, receiveContracts);
306
+ });
303
307
  });
304
308
  const port = await new Promise((resolvePort, rejectPort) => {
305
309
  server.once("error", rejectPort);
@@ -24,7 +24,7 @@ export function createStandaloneRuntimeManifest(projectId, tokenSnapshot = empty
24
24
  // Canvas shares the Vite host's controller/renderer runtime (ADR-0012):
25
25
  // every served document carries the bootstrap, so card iframes boot as
26
26
  // renderers without host-specific code.
27
- capabilities: { canvas: true, componentSemantics: false },
27
+ capabilities: { canvas: true, componentSemantics: false, domNavigation: true },
28
28
  tokenCatalog: tokenSnapshot.tokenCatalog,
29
29
  tokens: tokenSnapshot.tokens,
30
30
  tokenDiagnostics: tokenSnapshot.tokenDiagnostics,
@@ -1,3 +1,4 @@
1
+ import { handleHtmlArtifactRequest } from "../../project/artifacts.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { createServer, } from "node:http";
3
4
  import { constants, existsSync, realpathSync, statSync, } from "node:fs";
@@ -383,6 +384,8 @@ function isWithin(root, candidate) {
383
384
  }
384
385
  async function handleRequest(input) {
385
386
  const { request, response } = input;
387
+ if (await handleHtmlArtifactRequest(request, response, input.rootDirectory))
388
+ return;
386
389
  if (request.method !== "GET" && request.method !== "HEAD") {
387
390
  sendText(response, 405, "Method Not Allowed", { Allow: "GET, HEAD" });
388
391
  return;
@@ -1,5 +1,6 @@
1
1
  import type { Plugin } from "vite";
2
2
  import type { TailwindV3Config, ThemeContract } from "../../css/dialects/index.js";
3
+ import { type NudgeUiRuntimeConfig } from "../../inspector/clientManifest.js";
3
4
  import type { FrameworkHost, FrameworkSupport, ReactOptions } from "./react.js";
4
5
  /**
5
6
  * How to find the project's vanilla-extract theme contract. Loading a module
@@ -28,6 +29,8 @@ export interface NudgeUiOptions extends ReactOptions {
28
29
  demoPages?: readonly string[];
29
30
  /** Labels for the seeded demo cards in display order, including the primary card. */
30
31
  demoCardLabels?: readonly string[];
32
+ /** Product features exposed by the inspector runtime. Omitted flags remain enabled. */
33
+ features?: NudgeUiRuntimeConfig["features"];
31
34
  /** Explicit project ID for browser-storage keys (defaults to root directory basename). */
32
35
  projectId?: string;
33
36
  /** Optional static v3 config for fixture/app integrations; dynamic configs are not executed. */
@@ -1,3 +1,4 @@
1
+ import { handleHtmlArtifactRequest } from "../../project/artifacts.js";
1
2
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
3
  import { createRequire } from "node:module";
3
4
  import { basename, join, relative, resolve, sep, } from "node:path";
@@ -81,6 +82,8 @@ function scanCssFiles(rootDir, files = [], dir = rootDir, ignoredDirectory) {
81
82
  export function transformIndexHtmlHtml(html, command, { debug = false, demoBuild = false, demo = false } = {}) {
82
83
  if (command === "build" && !demoBuild)
83
84
  return null;
85
+ if (html.includes("data-nudge-ui-client"))
86
+ return null;
84
87
  const script = demo || demoBuild ? LEGACY_INSPECTOR_SCRIPT : INSPECTOR_SCRIPT;
85
88
  const inject = `\n${debug ? DEBUG_MOUNT_DIV : MOUNT_DIV}\n${script}\n`;
86
89
  if (html.includes("</body>")) {
@@ -533,7 +536,8 @@ export function createVitePlugins(options = {}, createFramework = null) {
533
536
  ...(options.demo === true ? { demo: true } : {}),
534
537
  ...(options.demoPages === undefined ? {} : { demoPages: options.demoPages }),
535
538
  ...(options.demoCardLabels === undefined ? {} : { demoCardLabels: options.demoCardLabels }),
536
- capabilities: { canvas: true, componentSemantics: framework !== null },
539
+ ...(options.features === undefined ? {} : { features: options.features }),
540
+ capabilities: { canvas: true, componentSemantics: framework !== null, domNavigation: true },
537
541
  tokenCatalog: snapshot.definitions.map((definition) => ({
538
542
  ...definition,
539
543
  declarations: definition.declarations.map((declaration) => ({ ...declaration })),
@@ -554,11 +558,24 @@ export function createVitePlugins(options = {}, createFramework = null) {
554
558
  if (!enabled || env.command !== "serve")
555
559
  return;
556
560
  // Module resolution is a framework concern; without one this host asks nothing of the resolver.
557
- return framework?.viteConfig({
561
+ const frameworkConfig = framework?.viteConfig({
558
562
  projectRoot: userConfig.root ?? process.cwd(),
559
563
  demo: options.demo === true,
560
564
  existingDedupe: userConfig.resolve?.dedupe ?? [],
561
565
  });
566
+ const ignored = userConfig.server?.watch?.ignored;
567
+ return {
568
+ ...frameworkConfig,
569
+ server: {
570
+ watch: {
571
+ ignored: [
572
+ ...(ignored === undefined ? [] : Array.isArray(ignored) ? ignored : [ignored]),
573
+ "**/.nudge",
574
+ "**/.nudge/**",
575
+ ],
576
+ },
577
+ },
578
+ };
562
579
  },
563
580
  configResolved(config) {
564
581
  root = config.root;
@@ -586,6 +603,8 @@ export function createVitePlugins(options = {}, createFramework = null) {
586
603
  if (runtimeWarning)
587
604
  server.config.logger.warn(runtimeWarning);
588
605
  server.middlewares?.use(async (request, response, next) => {
606
+ if (await handleHtmlArtifactRequest(request, response, root ?? process.cwd(), (html) => server.transformIndexHtml(request.url ?? "/", html)))
607
+ return;
589
608
  const pathname = new URL(request.url ?? "/", "http://nudge-ui.local").pathname;
590
609
  const invalidReservedMethod = (pathname === CLIENT_PATH || pathname === MANIFEST_PATH)
591
610
  ? request.method !== "GET"
@@ -601,7 +620,7 @@ export function createVitePlugins(options = {}, createFramework = null) {
601
620
  response.statusCode = 200;
602
621
  response.setHeader("Content-Type", "text/html; charset=utf-8");
603
622
  response.setHeader("Cache-Control", "no-store");
604
- response.end(request.method === "HEAD" ? undefined : createNudgeUiEditorDocument());
623
+ response.end(request.method === "HEAD" ? undefined : await server.transformIndexHtml(request.url ?? NUDGE_UI_EDITOR_PATH, createNudgeUiEditorDocument()));
605
624
  return;
606
625
  }
607
626
  if (pathname === CLIENT_PATH) {
@@ -718,7 +737,8 @@ export function createVitePlugins(options = {}, createFramework = null) {
718
737
  ' demo: true,',
719
738
  ` demoPages: ${JSON.stringify(options.demoPages ?? [])},`,
720
739
  ` demoCardLabels: ${JSON.stringify(options.demoCardLabels ?? [])},`,
721
- ` capabilities: { canvas: true, componentSemantics: ${String(framework !== null)} },`,
740
+ ` features: ${JSON.stringify(options.features ?? {})},`,
741
+ ` capabilities: { canvas: true, componentSemantics: ${String(framework !== null)}, domNavigation: true },`,
722
742
  ' tokenCatalog,',
723
743
  ' tokens,',
724
744
  ' tokenDiagnostics,',
@@ -1,33 +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
8
  import { Disclosure } from "../ui/Disclosure.js";
9
- import { SegmentedControl } from "../ui/SegmentedControl.js";
9
+ import { IconButton } from "../ui/IconButton.js";
10
10
  import { StatusCallout } from "../ui/StatusCallout.js";
11
+ import { portalContainer } from "../ui/portalContainer.js";
11
12
  export const MCP_DOCS_URL = "https://github.com/charlessmart/nudge-ui#connect-a-coding-agent";
12
13
  /** Uses the guided installer so browser diagnostics never become shell arguments. */
13
14
  export function createMcpSetupCommand() {
14
15
  return "npx nudge-ui agent setup";
15
16
  }
16
- function portalContainer() {
17
- return typeof document !== "undefined"
18
- ? document.getElementById("nudge-ui-root")?.shadowRoot ?? document.body
19
- : null;
20
- }
21
17
  function TimelineStep({ number, complete, title, children, }) {
22
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] })] }));
23
19
  }
24
20
  /** Renders the MCP setup and pairing flow without owning a dialog surface. */
25
21
  export function McpConnectionContent({ projectId, origin, snapshot, onConnect, onTakeOver, onDisconnect, onCheckAgain, }) {
26
22
  const [checking, setChecking] = useState(false);
27
- const [setupMethod, setSetupMethod] = useState("ai");
28
23
  const [copiedCommand, setCopiedCommand] = useState(false);
29
24
  const [copiedPrompt, setCopiedPrompt] = useState(false);
30
- const [copiedListener, setCopiedListener] = useState(false);
25
+ const [copiedDoctor, setCopiedDoctor] = useState(false);
31
26
  const [copyError, setCopyError] = useState();
32
27
  const [checkError, setCheckError] = useState();
33
28
  const setupCommand = createMcpSetupCommand();
@@ -39,8 +34,10 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
39
34
  "",
40
35
  "Start the application normally. Reload the agent if needed, then listen to Nudge.",
41
36
  ].join("\n"), [setupCommand]);
42
- 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";
43
38
  const status = getAgentConnectionStatus(snapshot);
39
+ const showConnectionStatus = status.kind !== "not-found"
40
+ || Boolean(snapshot.error || checkError || snapshot.request?.summary || snapshot.request?.error);
44
41
  const canConnect = snapshot.companionReachable
45
42
  && !snapshot.paired
46
43
  && !snapshot.pairedElsewhere
@@ -90,21 +87,18 @@ export function McpConnectionContent({ projectId, origin, snapshot, onConnect, o
90
87
  setCopyError("The AI setup prompt could not be copied. Select it and copy it manually.");
91
88
  }
92
89
  }
93
- async function handleCopyListener() {
90
+ async function handleCopyDoctor() {
94
91
  setCopyError(undefined);
95
92
  try {
96
- await copyToClipboard(listenerInstruction);
97
- setCopiedListener(true);
98
- window.setTimeout(() => setCopiedListener(false), 1500);
93
+ await copyToClipboard(doctorCommand);
94
+ setCopiedDoctor(true);
95
+ window.setTimeout(() => setCopiedDoctor(false), 1500);
99
96
  }
100
97
  catch {
101
- 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.");
102
99
  }
103
100
  }
104
- return (_jsxs(_Fragment, { children: [_jsxs(StatusCallout, { className: "mcp-connection__status", tone: status.tone, "data-test": "mcp-connection-status", children: [_jsx("span", { className: "mcp-connection__status-label", role: "status", children: status.label }), snapshot.error ? (_jsx("span", { className: "mcp-connection__status-detail", children: snapshot.error })) : null, checkError ? _jsx("span", { role: "alert", children: checkError }) : null, snapshot.request?.summary ? (_jsx("span", { className: "mcp-connection__status-detail", children: snapshot.request.summary })) : null, snapshot.request?.error ? (_jsx("span", { className: "mcp-connection__status-detail", children: snapshot.request.error })) : null] }), _jsxs("ol", { className: "mcp-connection__timeline", "data-test": "mcp-connection-timeline", children: [_jsxs(TimelineStep, { number: 1, complete: snapshot.listenerActive || snapshot.request !== null, title: "Set up your agent once", children: [_jsx(SegmentedControl, { "aria-label": "MCP setup method", className: "mcp-connection__setup-tabs", "data-test": "mcp-setup-tabs", value: setupMethod, options: [
105
- { value: "ai", label: "AI instructions", testId: "mcp-setup-tab-ai" },
106
- { value: "terminal", label: "Terminal command", testId: "mcp-setup-tab-terminal" },
107
- ], onChange: setSetupMethod }), setupMethod === "terminal" ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Run this command from the application directory, then start your app normally." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-command", children: setupCommand }) }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-command", type: "button", onClick: () => void handleCopyCommand(), children: [copiedCommand ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedCommand ? "Copied" : "Copy command"] }) })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "mcp-connection__copy", children: "Copy these setup instructions into your coding agent." }), _jsx("pre", { className: "mcp-connection__command", children: _jsx("code", { "data-test": "mcp-setup-prompt", children: setupPrompt }) }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-setup-prompt", type: "button", onClick: () => void handleCopyPrompt(), children: [copiedPrompt ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedPrompt ? "Copied" : "Copy prompt"] }) })] }))] }), _jsxs(TimelineStep, { number: 2, complete: snapshot.listenerActive, title: "Listen in this agent session", children: [_jsx("p", { className: "mcp-connection__copy", children: "Tell your coding agent: \u201CListen to Nudge.\u201D Reload the agent if Nudge tools are unavailable." }), _jsx("div", { className: "mcp-connection__command-actions", children: _jsxs(Button, { variant: "secondary", "data-test": "mcp-copy-listener", type: "button", onClick: () => void handleCopyListener(), children: [copiedListener ? _jsx(IconCheck, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) : _jsx(IconClipboard, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), copiedListener ? "Copied" : "Copy listening instruction"] }) })] })] }), _jsxs(Disclosure, { className: "mcp-connection__details", title: "Connection details and recovery", children: [_jsxs("p", { className: "mcp-connection__copy", children: ["The page connects automatically when the development integration is available. To diagnose setup, run ", _jsx("code", { children: "nudge-ui agent doctor" }), " from the application directory."] }), _jsxs("dl", { className: "mcp-connection__diagnostics", "data-test": "mcp-connection-diagnostics", children: [_jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Project ID" }), _jsx("dd", { "data-test": "mcp-project-id", children: _jsx("code", { children: projectId }) })] }), _jsxs("div", { className: "mcp-connection__diagnostic", children: [_jsx("dt", { children: "Origin" }), _jsx("dd", { "data-test": "mcp-origin", children: _jsx("code", { children: origin }) })] })] }), _jsxs("div", { className: "mcp-connection__actions", children: [canTakeOver ? (_jsxs(Button, { variant: "primary", "data-test": "mcp-takeover", type: "button", onClick: () => void onTakeOver(), children: [_jsx(IconPlugConnected, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Take over"] })) : null, canConnect ? (_jsxs(Button, { variant: "primary", "data-test": "mcp-connect", type: "button", onClick: () => void onConnect(), children: [_jsx(IconPlugConnected, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Connect"] })) : null, _jsxs(Button, { variant: "quiet", "data-test": "mcp-check-again", type: "button", disabled: checking || snapshot.state === "disabled" || snapshot.state === "pairing", onClick: () => void handleCheckAgain(), children: [_jsx(IconRefresh, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), checking ? "Checking…" : "Check again"] }), canDisconnect ? (_jsxs(Button, { variant: "danger", "data-test": "mcp-disconnect", type: "button", onClick: onDisconnect, children: [_jsx(IconUnlink, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), snapshot.paired ? "Disconnect" : "Forget connection"] })) : null] })] }), copyError ? _jsx("p", { className: "mcp-connection__error", role: "alert", children: copyError }) : null, _jsxs("a", { className: "mcp-connection__docs", "data-test": "mcp-docs-link", href: MCP_DOCS_URL, target: "_blank", rel: "noreferrer", children: [_jsx(IconExternalLink, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }), "Read the MCP connection guide"] })] }));
101
+ 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] }));
108
102
  }
109
103
  /**
110
104
  * Explains and controls the project-scoped MCP companion connection.
@@ -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,6 +15,7 @@ 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;
@@ -51,6 +52,7 @@ export declare class AgentClient {
51
52
  private started;
52
53
  private connection;
53
54
  private listenerActive;
55
+ private agentClientName;
54
56
  private companionReachable;
55
57
  private paired;
56
58
  private pairedElsewhere;