nudge-ui 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +252 -54
  3. package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
  4. package/dist/css/value-semantics/colorSemantics.js +17 -0
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
  6. package/dist/hosts/astro/clientTransport.js +41 -32
  7. package/dist/hosts/astro/integration.d.ts +2 -2
  8. package/dist/hosts/astro/integration.js +9 -4
  9. package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
  10. package/dist/hosts/astro/markdownIdentity.js +81 -0
  11. package/dist/hosts/environment.d.ts +15 -0
  12. package/dist/hosts/environment.js +16 -0
  13. package/dist/hosts/next/manifest.js +1 -1
  14. package/dist/hosts/next/sidecar.js +12 -2
  15. package/dist/hosts/next/wrapper.d.ts +2 -0
  16. package/dist/hosts/next/wrapper.js +2 -1
  17. package/dist/hosts/projectBridge.d.ts +1 -0
  18. package/dist/hosts/projectBridge.js +1 -0
  19. package/dist/hosts/static/manifest.js +3 -1
  20. package/dist/hosts/static/server.js +4 -0
  21. package/dist/hosts/vite/vite.d.ts +4 -0
  22. package/dist/hosts/vite/vite.js +28 -5
  23. package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
  24. package/dist/inspector/agent/McpConnectionDialog.js +23 -22
  25. package/dist/inspector/agent/activity.d.ts +18 -0
  26. package/dist/inspector/agent/activity.js +70 -0
  27. package/dist/inspector/agent/client.d.ts +8 -0
  28. package/dist/inspector/agent/client.js +65 -5
  29. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  30. package/dist/inspector/agent/connectionStatus.js +9 -2
  31. package/dist/inspector/agent/httpTransport.d.ts +6 -0
  32. package/dist/inspector/agent/httpTransport.js +50 -3
  33. package/dist/inspector/agent/protocol.d.ts +9 -3
  34. package/dist/inspector/agent/protocol.js +1 -1
  35. package/dist/inspector/agent/verification.d.ts +11 -1
  36. package/dist/inspector/agent/verification.js +53 -25
  37. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
  38. package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
  39. package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
  40. package/dist/inspector/canvas/CanvasCard.js +133 -38
  41. package/dist/inspector/canvas/CanvasElementOverlay.js +200 -22
  42. package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
  43. package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
  44. package/dist/inspector/canvas/CanvasToolbar.js +8 -4
  45. package/dist/inspector/canvas/CanvasWorkspace.js +243 -56
  46. package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
  47. package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
  48. package/dist/inspector/canvas/agentPresentation.js +64 -52
  49. package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
  50. package/dist/inspector/canvas/canvasGestures.js +1 -1
  51. package/dist/inspector/canvas/canvasStore.d.ts +46 -82
  52. package/dist/inspector/canvas/canvasStore.js +251 -284
  53. package/dist/inspector/canvas/frameContent.d.ts +21 -0
  54. package/dist/inspector/canvas/frameContent.js +44 -0
  55. package/dist/inspector/canvas/frameProtocol.d.ts +34 -4
  56. package/dist/inspector/canvas/frameProtocol.js +9 -2
  57. package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -1
  58. package/dist/inspector/canvas/keyboardShortcuts.js +5 -3
  59. package/dist/inspector/canvas/layout.d.ts +52 -0
  60. package/dist/inspector/canvas/layout.js +106 -0
  61. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  62. package/dist/inspector/canvas/linkEligibility.js +0 -29
  63. package/dist/inspector/canvas/projection.d.ts +15 -5
  64. package/dist/inspector/canvas/projection.js +69 -13
  65. package/dist/inspector/canvas/rendererBootstrap.js +45 -4
  66. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  67. package/dist/inspector/canvas/rendererElementSelector.js +102 -45
  68. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  69. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  70. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  71. package/dist/inspector/canvas/sessionStore.js +43 -183
  72. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  73. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  74. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  75. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  76. package/dist/inspector/canvas/viewStore.js +104 -0
  77. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  78. package/dist/inspector/canvas/wheelPan.js +8 -0
  79. package/dist/inspector/changes/changesLog.d.ts +6 -5
  80. package/dist/inspector/changes/changesLog.js +40 -32
  81. package/dist/inspector/changes/draftChanges.d.ts +56 -0
  82. package/dist/inspector/changes/draftChanges.js +300 -0
  83. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  84. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  85. package/dist/inspector/clientManifest.d.ts +3 -1
  86. package/dist/inspector/clientManifest.js +1 -0
  87. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  88. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  89. package/dist/inspector/comments/element.d.ts +6 -0
  90. package/dist/inspector/comments/element.js +47 -0
  91. package/dist/inspector/comments/store.d.ts +29 -0
  92. package/dist/inspector/comments/store.js +121 -0
  93. package/dist/inspector/comments/verification.d.ts +5 -0
  94. package/dist/inspector/comments/verification.js +52 -0
  95. package/dist/inspector/drafts/model.d.ts +35 -0
  96. package/dist/inspector/drafts/model.js +45 -0
  97. package/dist/inspector/drafts/store.d.ts +28 -0
  98. package/dist/inspector/drafts/store.js +231 -0
  99. package/dist/inspector/index.js +26 -35
  100. package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
  101. package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
  102. package/dist/inspector/iterations/capture.d.ts +2 -0
  103. package/dist/inspector/iterations/capture.js +173 -0
  104. package/dist/inspector/iterations/client.d.ts +7 -0
  105. package/dist/inspector/iterations/client.js +52 -0
  106. package/dist/inspector/iterations/coordinator.d.ts +15 -0
  107. package/dist/inspector/iterations/coordinator.js +130 -0
  108. package/dist/inspector/iterations/prompt.d.ts +3 -0
  109. package/dist/inspector/iterations/prompt.js +26 -0
  110. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  111. package/dist/inspector/overlay/DomNavigator.js +46 -0
  112. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  113. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  114. package/dist/inspector/overlay/clickPolicy.js +34 -3
  115. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  116. package/dist/inspector/overlay/elementSelector.js +18 -7
  117. package/dist/inspector/overlay/interactionStyles.js +3 -1
  118. package/dist/inspector/overlay/spacingGestures.d.ts +15 -3
  119. package/dist/inspector/overlay/spacingGestures.js +88 -13
  120. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  121. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  122. package/dist/inspector/projection/renderedInstance.d.ts +2 -1
  123. package/dist/inspector/projection/renderedInstance.js +9 -5
  124. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  125. package/dist/inspector/projection/structuralProjection.js +19 -15
  126. package/dist/inspector/projection/textProjection.d.ts +2 -1
  127. package/dist/inspector/projection/textProjection.js +8 -4
  128. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  129. package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
  130. package/dist/inspector/prompt/clipboardHandoff.js +92 -143
  131. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  132. package/dist/inspector/prompt/generatePrompt.js +17 -16
  133. package/dist/inspector/runtime/runtimeConfig.d.ts +10 -1
  134. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  135. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  136. package/dist/inspector/selection/hierarchy.js +8 -0
  137. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  138. package/dist/inspector/settings/SettingsDialog.js +22 -9
  139. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  140. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  141. package/dist/inspector/shell/AppShell.js +4 -2
  142. package/dist/inspector/shell/ChangesLog.js +47 -27
  143. package/dist/inspector/shell/CopyPromptButton.js +146 -90
  144. package/dist/inspector/shell/EmptyState.js +9 -8
  145. package/dist/inspector/shell/InspectorShell.js +14 -25
  146. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  147. package/dist/inspector/shell/originalPreview.js +5 -5
  148. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  149. package/dist/inspector/shell/shortcuts.js +11 -0
  150. package/dist/inspector/sketch/SketchEditor.js +14 -14
  151. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  152. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  153. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  154. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  155. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  156. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  157. package/dist/inspector/sketch/interaction.d.ts +1 -0
  158. package/dist/inspector/sketch/interaction.js +8 -4
  159. package/dist/inspector/sketch/model.d.ts +5 -0
  160. package/dist/inspector/sketch/model.js +6 -2
  161. package/dist/inspector/sketch/raster.d.ts +1 -2
  162. package/dist/inspector/sketch/raster.js +68 -4
  163. package/dist/inspector/sketch/store.js +48 -23
  164. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  165. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  166. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  167. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  168. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  169. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  170. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  171. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  172. package/dist/inspector/styleEditors/GapField.js +3 -5
  173. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  174. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  175. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  176. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  177. package/dist/inspector/styleEditors/GridSection.js +1 -1
  178. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  179. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  180. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  181. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  182. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  183. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  184. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  185. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  186. package/dist/inspector/styleEditors/SpacingBox.js +16 -9
  187. package/dist/inspector/styleEditors/Typography.js +11 -3
  188. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  189. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  190. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  191. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  192. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  193. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  194. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  195. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  196. package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
  197. package/dist/inspector/styleEditors/nudgeValue.js +41 -11
  198. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  199. package/dist/inspector/tokens/TokenField.js +49 -22
  200. package/dist/inspector/tokens/TokenLabel.js +5 -14
  201. package/dist/inspector/tokens/TokensPanel.js +2 -1
  202. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  203. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  204. package/dist/inspector/tokens/editActions.d.ts +1 -1
  205. package/dist/inspector/tokens/editActions.js +1 -1
  206. package/dist/inspector/ui/AtRuleContext.js +4 -6
  207. package/dist/inspector/ui/Button.d.ts +6 -2
  208. package/dist/inspector/ui/Button.js +4 -3
  209. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  210. package/dist/inspector/ui/ControlSurface.js +4 -3
  211. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  212. package/dist/inspector/ui/Disclosure.js +13 -0
  213. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  214. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  215. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  216. package/dist/inspector/ui/InspectorPopover.js +4 -5
  217. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  218. package/dist/inspector/ui/PopoverListbox.js +8 -7
  219. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  220. package/dist/inspector/ui/SegmentedControl.js +2 -2
  221. package/dist/inspector/ui/Select.d.ts +6 -1
  222. package/dist/inspector/ui/Select.js +7 -11
  223. package/dist/inspector/ui/SideValuesField.js +1 -1
  224. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  225. package/dist/inspector/ui/Tooltip.js +32 -0
  226. package/dist/inspector/ui/classNames.d.ts +2 -0
  227. package/dist/inspector/ui/classNames.js +4 -0
  228. package/dist/inspector/ui/customCursors.d.ts +23 -0
  229. package/dist/inspector/ui/customCursors.js +122 -0
  230. package/dist/inspector/ui/dragCursor.d.ts +6 -0
  231. package/dist/inspector/ui/dragCursor.js +13 -0
  232. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  233. package/dist/inspector/ui/portalContainer.js +6 -0
  234. package/dist/inspector/ui/styles.js +34 -24
  235. package/dist/inspector/workspace/commands.d.ts +9 -0
  236. package/dist/inspector/workspace/commands.js +82 -0
  237. package/dist/inspector/workspace/controller.d.ts +4 -0
  238. package/dist/inspector/workspace/controller.js +66 -0
  239. package/dist/inspector/workspace/handoff.d.ts +42 -0
  240. package/dist/inspector/workspace/handoff.js +115 -0
  241. package/dist/inspector/workspace/timeline.d.ts +30 -0
  242. package/dist/inspector/workspace/timeline.js +108 -0
  243. package/dist/internal/agent-protocol.d.ts +19 -2
  244. package/dist/internal/agent-protocol.js +13 -0
  245. package/dist/nudge-ui.mjs +560 -299
  246. package/dist/project/artifacts.d.ts +13 -0
  247. package/dist/project/artifacts.js +253 -0
  248. package/dist/transport/artifacts.d.ts +8 -0
  249. package/dist/transport/artifacts.js +9 -0
  250. package/dist/transport/editor.d.ts +20 -1
  251. package/dist/transport/editor.js +44 -11
  252. package/dist/transport/routes.d.ts +6 -1
  253. package/dist/transport/routes.js +6 -1
  254. package/package.json +6 -2
  255. package/dist/inspector/changes/workspaceChanges.d.ts +0 -43
  256. package/dist/inspector/changes/workspaceChanges.js +0 -239
  257. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  258. package/dist/inspector/shell/DomNavigation.js +0 -30
@@ -1,16 +1,15 @@
1
- import { getWorkspaceChanges, subscribeWorkspaceChanges } from "./workspaceChanges.js";
1
+ import { getActiveDraftChanges, getDraftChanges } from "./draftChanges.js";
2
2
  const HOST_DOCUMENT = {
3
3
  logicalDocument: "host",
4
4
  sessionId: "host-document",
5
5
  };
6
- let workspaceRevision = getWorkspaceChanges().revision;
7
6
  let activeSessions = new Map([[HOST_DOCUMENT.logicalDocument, HOST_DOCUMENT.sessionId]]);
8
7
  let attemptsBySession = new Map();
9
8
  let diagnostics = new Map();
10
9
  let diagnosticRevision = 0;
11
10
  const listeners = new Set();
12
11
  function storageKey(document, changeKey) {
13
- return `${document.logicalDocument}\u0000${document.sessionId}\u0000${changeKey}`;
12
+ return `${document.draftId ?? getActiveDraftChanges().draftId}\u0000${document.logicalDocument}\u0000${document.sessionId}\u0000${changeKey}`;
14
13
  }
15
14
  function sessionKey(document) {
16
15
  return `${document.logicalDocument}\u0000${document.sessionId}`;
@@ -26,23 +25,12 @@ function notify() {
26
25
  for (const listener of listeners)
27
26
  listener();
28
27
  }
29
- function syncWorkspaceRevision() {
30
- const nextRevision = getWorkspaceChanges().revision;
31
- if (nextRevision === workspaceRevision)
32
- return;
33
- // Track the revision so late publishes are rejected per key. Do not clear
34
- // here: a new commit must preserve earlier diagnostics until they are
35
- // re-verified, otherwise every edit deletes every other change's status.
36
- workspaceRevision = nextRevision;
37
- }
38
- subscribeWorkspaceChanges(syncWorkspaceRevision);
39
28
  /** Returns the stable host-document identity for this inspector lifetime. */
40
29
  export function getHostPreviewDocument() {
41
- return HOST_DOCUMENT;
30
+ return { ...HOST_DOCUMENT, draftId: getActiveDraftChanges().draftId };
42
31
  }
43
32
  /** Starts or replaces the live session for one logical document. */
44
33
  export function startPreviewDocumentSession(document) {
45
- syncWorkspaceRevision();
46
34
  const previousSession = activeSessions.get(document.logicalDocument);
47
35
  if (previousSession === document.sessionId)
48
36
  return;
@@ -76,9 +64,9 @@ export function invalidatePreviewDocumentSession(logicalDocument, sessionId) {
76
64
  notify();
77
65
  }
78
66
  /** Opens a verification attempt for the currently active document session. */
79
- export function beginPreviewAttempt(document, revision = getWorkspaceChanges().revision) {
80
- syncWorkspaceRevision();
81
- if (revision !== workspaceRevision || activeSessions.get(document.logicalDocument) !== document.sessionId) {
67
+ export function beginPreviewAttempt(document, revision = getDraftChanges(document.draftId ?? getActiveDraftChanges().draftId).revision) {
68
+ const draftId = document.draftId ?? getActiveDraftChanges().draftId;
69
+ if (revision !== getDraftChanges(draftId).revision || activeSessions.get(document.logicalDocument) !== document.sessionId) {
82
70
  return null;
83
71
  }
84
72
  const attempt = (attemptsBySession.get(sessionKey(document)) ?? 0) + 1;
@@ -87,12 +75,11 @@ export function beginPreviewAttempt(document, revision = getWorkspaceChanges().r
87
75
  // rejects late publishes per key; wiping would drop every other change's
88
76
  // status before it is re-verified (and drop all host status when Canvas
89
77
  // verification returns null).
90
- return { ...document, workspaceRevision: revision, attempt };
78
+ return { ...document, draftId, draftRevision: revision, attempt };
91
79
  }
92
80
  /** Publishes only a result from the active revision, session, and attempt. */
93
81
  export function publishPreviewDiagnostic(attempt, changeKey, result) {
94
- syncWorkspaceRevision();
95
- if (attempt.workspaceRevision !== workspaceRevision
82
+ if (attempt.draftRevision !== getDraftChanges(attempt.draftId).revision
96
83
  || activeSessions.get(attempt.logicalDocument) !== attempt.sessionId
97
84
  || attemptsBySession.get(sessionKey(attempt)) !== attempt.attempt) {
98
85
  return false;
@@ -101,7 +88,7 @@ export function publishPreviewDiagnostic(attempt, changeKey, result) {
101
88
  const previous = diagnostics.get(key);
102
89
  if (previous
103
90
  && previous.changeKey === changeKey
104
- && previous.workspaceRevision === attempt.workspaceRevision
91
+ && previous.draftRevision === attempt.draftRevision
105
92
  && previous.attempt === attempt.attempt
106
93
  && sameResult(previous.result, result)) {
107
94
  return true;
@@ -127,7 +114,7 @@ export function clearPreviewDiagnostics(changeKeys) {
127
114
  export function notifyPreviewDiagnostics() {
128
115
  notify();
129
116
  }
130
- export function getPreviewDiagnostic(changeKey, logicalDocument = HOST_DOCUMENT.logicalDocument) {
117
+ export function getPreviewDiagnostic(changeKey, logicalDocument = HOST_DOCUMENT.logicalDocument, draftId = getActiveDraftChanges().draftId) {
131
118
  // Pure getter: never sync or notify. It runs on the React render path via
132
119
  // StaleChangeIndicator, so side effects here would clear state mid-render.
133
120
  // Diagnostics survive workspace revisions until overwritten or explicitly
@@ -135,7 +122,7 @@ export function getPreviewDiagnostic(changeKey, logicalDocument = HOST_DOCUMENT.
135
122
  const sessionId = activeSessions.get(logicalDocument);
136
123
  if (!sessionId)
137
124
  return undefined;
138
- return diagnostics.get(storageKey({ logicalDocument, sessionId }, changeKey));
125
+ return diagnostics.get(storageKey({ logicalDocument, sessionId, draftId }, changeKey));
139
126
  }
140
127
  /**
141
128
  * Host-first read across live documents. The per-document store is observable
@@ -164,7 +151,6 @@ export function getPreviewDiagnosticRevision() {
164
151
  }
165
152
  /** Test and teardown reset; it does not alter canonical workspace state. */
166
153
  export function resetPreviewDiagnostics() {
167
- workspaceRevision = getWorkspaceChanges().revision;
168
154
  activeSessions = new Map([[HOST_DOCUMENT.logicalDocument, HOST_DOCUMENT.sessionId]]);
169
155
  attemptsBySession = new Map();
170
156
  diagnostics = new Map();
@@ -1,5 +1,5 @@
1
1
  import { type NudgeUiRuntimeConfig } from "./runtime/runtimeConfig.js";
2
- export type { NudgeUiRuntimeConfig } from "./runtime/runtimeConfig.js";
2
+ export type { NudgeUiRuntimeConfig, NudgeUiRuntimeFeatures } from "./runtime/runtimeConfig.js";
3
3
  export { detectStylingSystem } from "./runtime/runtimeConfig.js";
4
4
  /** Version of the host-to-inspector runtime manifest Interface. */
5
5
  export declare const NUDGE_UI_CLIENT_MANIFEST_VERSION = 1;
@@ -24,6 +24,8 @@ export interface NudgeUiClientManifest {
24
24
  readonly strategy: "refresh-manifest" | "reload-document";
25
25
  readonly events?: readonly string[];
26
26
  };
27
+ /** Set by `NUDGE_UI=1`: automated browsers open the editor instead of the plain app. */
28
+ readonly inspectAutomatedBrowsers?: true;
27
29
  }
28
30
  /** Parses and normalizes an external manifest before it replaces inspector state. */
29
31
  export declare function parseNudgeUiClientManifest(value: unknown): NudgeUiClientManifest | null;
@@ -23,6 +23,7 @@ value) {
23
23
  ...(isAgentBridge(value.agentBridge) ? { agentBridge: value.agentBridge } : {}),
24
24
  ...(value.document === undefined ? {} : { document: value.document }),
25
25
  ...(value.reload === undefined ? {} : { reload: value.reload }),
26
+ ...(value.inspectAutomatedBrowsers === true ? { inspectAutomatedBrowsers: true } : {}),
26
27
  };
27
28
  }
28
29
  catch {
@@ -0,0 +1,2 @@
1
+ import { type ReactElement } from "react";
2
+ export declare function CommentsOverlay(): ReactElement | null;
@@ -0,0 +1,250 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { IconCheck, IconTrash } from "@tabler/icons-react";
4
+ import { useCanvasMode } from "../canvas/viewStore.js";
5
+ import { getRegisteredFrames } from "../canvas/projection.js";
6
+ import { subscribeCanvasRendererMessages } from "../canvas/rendererMessageRouter.js";
7
+ import { RENDERER_ELEMENT_ID_ATTR } from "../canvas/rendererCidIndex.js";
8
+ import { captureRenderedInstance } from "../projection/renderedInstance.js";
9
+ import { IconButton } from "../ui/IconButton.js";
10
+ import { TextInput } from "../ui/TextInput.js";
11
+ import { commentFingerprint, commentViewport, resolveCommentElement } from "./element.js";
12
+ import { commentRoute, getCommentClearRevision, isCommentToolActive, removeComment, saveComment, subscribeComments, useComments, useCommentToolActive } from "./store.js";
13
+ import { isReadingCommentSource, readCommentSource, reconcileComments } from "./verification.js";
14
+ const styles = ".comment-pin {\n position: fixed;\n z-index: 10;\n display: grid;\n place-items: center;\n width: var(--icon-size-large);\n height: var(--icon-size-large);\n padding: 0;\n border: 2px solid var(--surface-raised-2x);\n border-radius: 50%;\n background: var(--accent);\n box-shadow: var(--shadow-popup);\n cursor: pointer;\n pointer-events: auto;\n}\n\n.comment-editor-backdrop {\n position: fixed;\n inset: 0;\n z-index: 11;\n pointer-events: auto;\n}\n\n.comment-editor {\n position: fixed;\n z-index: 12;\n display: flex;\n align-items: center;\n gap: var(--space-4);\n width: min(384px, calc(100vw - 16px));\n box-sizing: border-box;\n padding: var(--space-8);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.comment-editor .text-input { flex: 1; min-width: 0; padding-inline: var(--space-8); }\n.comment-editor__error { position: absolute; top: 100%; left: 0; padding: var(--space-8); background: var(--surface-raised-2x); color: var(--text-primary); border-radius: var(--radius-control); }\n";
15
+ function sameAnchor(left, right) {
16
+ return left?.left === right?.left && left?.top === right?.top;
17
+ }
18
+ function anchorFor(iframe, element) {
19
+ if (!element.isConnected || iframe.contentDocument !== element.ownerDocument)
20
+ return null;
21
+ // Focus view keeps other frames mounted, but their pins must stay hidden.
22
+ if (getComputedStyle(iframe).visibility !== "visible")
23
+ return null;
24
+ const frame = iframe.getBoundingClientRect();
25
+ const rect = element.getBoundingClientRect();
26
+ const scale = frame.width / (iframe.clientWidth || frame.width);
27
+ const left = frame.left + rect.right * scale;
28
+ const top = frame.top + rect.top * scale;
29
+ if (top < frame.top || top > frame.bottom || left < frame.left || left > frame.right)
30
+ return null;
31
+ return { left, top };
32
+ }
33
+ export function CommentsOverlay() {
34
+ const mode = useCanvasMode();
35
+ const comments = useComments();
36
+ const commentActive = useCommentToolActive();
37
+ const [draft, setDraft] = useState(null);
38
+ const [note, setNote] = useState("");
39
+ const [saving, setSaving] = useState(false);
40
+ const [error, setError] = useState(null);
41
+ const [anchors, setAnchors] = useState(new Map());
42
+ const [draftAnchor, setDraftAnchor] = useState(null);
43
+ const draftRef = useRef(draft);
44
+ draftRef.current = draft;
45
+ const bindings = useRef(new Map());
46
+ const observations = useRef(new Map());
47
+ function openDraft(next) {
48
+ setDraft(next);
49
+ setNote(next.existing?.note ?? "");
50
+ setError(null);
51
+ }
52
+ useEffect(() => {
53
+ if (!commentActive)
54
+ setDraft(null);
55
+ }, [commentActive]);
56
+ useEffect(() => {
57
+ let revision = getCommentClearRevision();
58
+ return subscribeComments(() => {
59
+ if (revision === getCommentClearRevision())
60
+ return;
61
+ revision = getCommentClearRevision();
62
+ setDraft(null);
63
+ });
64
+ }, []);
65
+ useEffect(() => {
66
+ if (!draft)
67
+ return;
68
+ function dismissOnEscape(event) {
69
+ if (event.key !== "Escape")
70
+ return;
71
+ event.preventDefault();
72
+ event.stopPropagation();
73
+ setDraft(null);
74
+ }
75
+ window.addEventListener("keydown", dismissOnEscape, true);
76
+ return () => window.removeEventListener("keydown", dismissOnEscape, true);
77
+ }, [draft]);
78
+ useEffect(() => subscribeCanvasRendererMessages(({ iframe, message }) => {
79
+ if (message.type === "element-click" && isCommentToolActive()) {
80
+ const element = iframe.contentDocument?.querySelector(`[${RENDERER_ELEMENT_ID_ATTR}="${message.elementId}"]`);
81
+ if (element && element.getAttribute("data-cid") === message.cid && (element.getAttribute("data-src") ?? "") === message.src) {
82
+ openDraft({ element, iframe });
83
+ }
84
+ }
85
+ if (message.type === "element-deselect")
86
+ setDraft(null);
87
+ if (message.type === "frame-ready" && iframe.contentDocument) {
88
+ void reconcileComments(iframe.contentDocument).catch(() => undefined);
89
+ }
90
+ }), []);
91
+ useEffect(() => {
92
+ if (comments.length === 0 && !draft) {
93
+ setAnchors(new Map());
94
+ setDraftAnchor(null);
95
+ bindings.current.clear();
96
+ return;
97
+ }
98
+ let animationFrame = 0;
99
+ let previousAnchors = null;
100
+ let previousDraftAnchor;
101
+ const refresh = () => {
102
+ const next = new Map();
103
+ const offsets = new Map();
104
+ for (const key of bindings.current.keys()) {
105
+ if (!comments.some((comment) => key.startsWith(`${comment.id}:`)))
106
+ bindings.current.delete(key);
107
+ }
108
+ for (const comment of comments) {
109
+ for (const [cardId, iframe] of getRegisteredFrames()) {
110
+ const doc = iframe.contentDocument;
111
+ if (!doc || commentRoute(doc) !== comment.route)
112
+ continue;
113
+ const key = `${comment.id}:${cardId}`;
114
+ const bound = bindings.current.get(key);
115
+ const element = bound?.iframe === iframe && bound.element.isConnected && bound.element.ownerDocument === doc
116
+ ? bound.element : resolveCommentElement(doc, comment.target);
117
+ if (!element)
118
+ continue;
119
+ bindings.current.set(key, { element, iframe });
120
+ const anchor = anchorFor(iframe, element);
121
+ if (anchor) {
122
+ const positionKey = `${cardId}:${anchor.left}:${anchor.top}`;
123
+ const offset = offsets.get(positionKey) ?? 0;
124
+ next.set(key, { ...anchor, top: anchor.top + offset });
125
+ offsets.set(positionKey, offset + 30);
126
+ }
127
+ }
128
+ }
129
+ if (!previousAnchors || previousAnchors.size !== next.size
130
+ || [...next].some(([key, anchor]) => !sameAnchor(previousAnchors?.get(key), anchor))) {
131
+ previousAnchors = next;
132
+ setAnchors(next);
133
+ }
134
+ const current = draftRef.current;
135
+ const nextDraftAnchor = current ? anchorFor(current.iframe, current.element) : null;
136
+ if (previousDraftAnchor === undefined || !sameAnchor(previousDraftAnchor, nextDraftAnchor)) {
137
+ previousDraftAnchor = nextDraftAnchor;
138
+ setDraftAnchor(nextDraftAnchor);
139
+ }
140
+ if (current && (!current.element.isConnected || current.iframe.contentDocument !== current.element.ownerDocument))
141
+ setDraft(null);
142
+ animationFrame = requestAnimationFrame(refresh);
143
+ };
144
+ refresh();
145
+ return () => cancelAnimationFrame(animationFrame);
146
+ }, [comments, draft]);
147
+ useEffect(() => {
148
+ if (!comments.some((comment) => comment.handedOff))
149
+ return;
150
+ const refresh = () => {
151
+ const documents = new Set([...getRegisteredFrames()].map(([, iframe]) => iframe.contentDocument).filter((doc) => doc !== null));
152
+ for (const [doc, observation] of observations.current) {
153
+ if (documents.has(doc))
154
+ continue;
155
+ observation.observer.disconnect();
156
+ if (observation.timer)
157
+ clearTimeout(observation.timer);
158
+ observations.current.delete(doc);
159
+ }
160
+ for (const doc of documents) {
161
+ if (observations.current.has(doc) || !comments.some((item) => item.handedOff && item.route === commentRoute(doc)))
162
+ continue;
163
+ const observation = { observer: new MutationObserver((records) => {
164
+ if (isReadingCommentSource(doc))
165
+ return;
166
+ const sourceChanged = records.some((record) => {
167
+ const element = record.target.nodeType === 1 ? record.target : record.target.parentElement;
168
+ if (element?.closest('[data-nudge-ui], #nudge-ui-root'))
169
+ return false;
170
+ if (record.type === "attributes" && (record.attributeName?.startsWith("data-nudge") || record.attributeName?.startsWith("data-projection") || record.attributeName === "data-renderer-id"))
171
+ return false;
172
+ return true;
173
+ });
174
+ if (!sourceChanged)
175
+ return;
176
+ if (observation.timer)
177
+ clearTimeout(observation.timer);
178
+ observation.timer = setTimeout(() => { void reconcileComments(doc).catch(() => undefined); }, 200);
179
+ }), timer: null };
180
+ observation.observer.observe(doc.documentElement, { subtree: true, childList: true, characterData: true, attributes: true });
181
+ observations.current.set(doc, observation);
182
+ void reconcileComments(doc).catch(() => undefined);
183
+ }
184
+ };
185
+ refresh();
186
+ const timer = setInterval(refresh, 500);
187
+ return () => {
188
+ clearInterval(timer);
189
+ for (const observation of observations.current.values()) {
190
+ observation.observer.disconnect();
191
+ if (observation.timer)
192
+ clearTimeout(observation.timer);
193
+ }
194
+ observations.current.clear();
195
+ };
196
+ }, [comments]);
197
+ async function confirm() {
198
+ if (!draft || saving || !note.trim())
199
+ return;
200
+ const currentDraft = draft;
201
+ const clearRevision = getCommentClearRevision();
202
+ const text = note.trim();
203
+ setSaving(true);
204
+ setError(null);
205
+ try {
206
+ const comment = await readCommentSource(draft.element.ownerDocument, () => {
207
+ if (!draft.element.isConnected)
208
+ return null;
209
+ const target = captureRenderedInstance(draft.element);
210
+ if (!target)
211
+ return null;
212
+ return {
213
+ id: draft.existing?.id ?? crypto.randomUUID(),
214
+ route: commentRoute(draft.element.ownerDocument),
215
+ target,
216
+ tag: draft.element.tagName.toLowerCase(),
217
+ note: text,
218
+ baseline: commentFingerprint(draft.element),
219
+ viewport: commentViewport(draft.element.ownerDocument),
220
+ handedOff: false,
221
+ };
222
+ });
223
+ if (getCommentClearRevision() !== clearRevision)
224
+ return;
225
+ if (!comment) {
226
+ setError("The element changed. Select it again to save your comment.");
227
+ return;
228
+ }
229
+ saveComment(comment);
230
+ if (draftRef.current === currentDraft)
231
+ setDraft(null);
232
+ }
233
+ catch {
234
+ setError("Couldn't save this comment. Try again.");
235
+ }
236
+ finally {
237
+ setSaving(false);
238
+ }
239
+ }
240
+ if (mode !== "canvas")
241
+ return null;
242
+ return _jsxs(_Fragment, { children: [_jsx("style", { children: styles }), comments.flatMap((comment) => [...anchors].filter(([key]) => key.startsWith(`${comment.id}:`)).map(([key, anchor]) => (_jsx("button", { type: "button", className: "comment-pin", "data-test": "comment-pin", "aria-label": `Comment: ${comment.note}`, title: comment.note, style: anchor, onClick: () => {
243
+ const bound = bindings.current.get(key);
244
+ if (bound)
245
+ openDraft({ ...bound, existing: comment });
246
+ } }, key)))), draft && draftAnchor ? _jsx("div", { className: "comment-editor-backdrop", "aria-hidden": "true", onClick: () => setDraft(null) }) : null, draft && draftAnchor ? _jsxs("form", { className: "comment-editor", "data-test": "comment-editor", role: "dialog", "aria-label": "Element comment", style: { left: Math.max(8, Math.min(draftAnchor.left + 12, window.innerWidth - 400)), top: Math.max(8, Math.min(draftAnchor.top, window.innerHeight - 90)) }, onSubmit: (event) => { event.preventDefault(); void confirm(); }, children: [_jsx(TextInput, { appearance: "embedded", autoFocus: true, "aria-label": "Comment", "data-test": "comment-input", placeholder: "Leave a comment", maxLength: 2000, value: note, disabled: saving, onChange: (event) => setNote(event.target.value) }), _jsx(IconButton, { label: "Confirm comment", variant: "primary", type: "submit", disabled: saving || !note.trim(), "data-test": "comment-confirm", children: _jsx(IconCheck, { size: 18 }) }), draft.existing ? _jsx(IconButton, { label: "Delete comment", variant: "quiet", disabled: saving, onClick: () => {
247
+ removeComment(draft.existing.id);
248
+ setDraft(null);
249
+ }, children: _jsx(IconTrash, { size: 16 }) }) : null, error ? _jsx("span", { className: "comment-editor__error", role: "alert", children: error }) : null] }) : null] });
250
+ }
@@ -0,0 +1,6 @@
1
+ import type { RenderedInstanceRef } from "../changes/editModel.js";
2
+ /** Retains a changed target only when its source site still identifies one output. */
3
+ export declare function resolveCommentElement(doc: Document, target: RenderedInstanceRef): HTMLElement | null;
4
+ export declare function commentViewport(doc: Document): string;
5
+ /** Captures rendered content and appearance, excluding inspector identity and transient cursors. */
6
+ export declare function commentFingerprint(element: HTMLElement): string;
@@ -0,0 +1,47 @@
1
+ import { resolveRenderedInstance } from "../projection/renderedInstance.js";
2
+ import { sourceSiteSelector } from "../selection/sourceSite.js";
3
+ /** Retains a changed target only when its source site still identifies one output. */
4
+ export function resolveCommentElement(doc, target) {
5
+ const exact = resolveRenderedInstance(doc, target);
6
+ if (exact.status === "resolved")
7
+ return exact.element;
8
+ const selector = sourceSiteSelector(target.sourceSite.cid, target.sourceSite.src);
9
+ if (!selector)
10
+ return null;
11
+ const candidates = Array.from(doc.querySelectorAll(selector));
12
+ if (candidates.length === 1)
13
+ return candidates[0];
14
+ const { props, ariaLabel } = target.locator;
15
+ if (!props && !ariaLabel)
16
+ return null;
17
+ const matching = candidates.filter((element) => element.getAttribute("data-cprops") === props
18
+ && element.getAttribute("aria-label") === (ariaLabel ?? null));
19
+ return matching.length === 1 ? matching[0] : null;
20
+ }
21
+ export function commentViewport(doc) {
22
+ return `${doc.defaultView?.innerWidth}:${doc.defaultView?.innerHeight}`;
23
+ }
24
+ /** Captures rendered content and appearance, excluding inspector identity and transient cursors. */
25
+ export function commentFingerprint(element) {
26
+ const clone = element.cloneNode(true);
27
+ for (const node of [clone, ...clone.querySelectorAll("*")]) {
28
+ for (const attribute of [...node.attributes]) {
29
+ if (attribute.name.startsWith("data-nudge") || attribute.name.startsWith("data-projection")
30
+ || ["data-cid", "data-src", "data-cprops", "data-renderer-id"].includes(attribute.name))
31
+ node.removeAttribute(attribute.name);
32
+ }
33
+ if (node instanceof element.ownerDocument.defaultView.HTMLElement) {
34
+ node.style.removeProperty("cursor");
35
+ if (node.getAttribute("style") === "")
36
+ node.removeAttribute("style");
37
+ }
38
+ }
39
+ const style = element.ownerDocument.defaultView?.getComputedStyle(element);
40
+ const properties = ["color", "background-color", "font-family", "font-size", "font-weight", "line-height",
41
+ "padding", "margin", "gap", "display", "border", "border-radius", "box-shadow", "opacity", "width", "height"];
42
+ const value = JSON.stringify([clone.outerHTML, properties.map((property) => style?.getPropertyValue(property) ?? "")]);
43
+ let hash = 2166136261;
44
+ for (let index = 0; index < value.length; index += 1)
45
+ hash = Math.imul(hash ^ value.charCodeAt(index), 16777619);
46
+ return (hash >>> 0).toString(16);
47
+ }
@@ -0,0 +1,29 @@
1
+ import { type RenderedInstanceRef } from "../changes/editModel.js";
2
+ import { type DraftTarget } from "../drafts/model.js";
3
+ export interface ElementComment {
4
+ readonly id: string;
5
+ readonly route: string;
6
+ readonly target: RenderedInstanceRef;
7
+ readonly tag: string;
8
+ readonly note: string;
9
+ readonly baseline: string;
10
+ readonly viewport: string;
11
+ readonly handedOff: boolean;
12
+ }
13
+ export declare function getComments(): readonly ElementComment[];
14
+ export declare function loadComments(): void;
15
+ export declare function subscribeComments(listener: () => void): () => void;
16
+ export declare function useComments(): readonly ElementComment[];
17
+ export declare function getCommentsForTarget(target: DraftTarget): readonly ElementComment[];
18
+ export declare function useFrameComments(): readonly ElementComment[];
19
+ export declare function saveComment(comment: ElementComment): void;
20
+ export declare function removeComment(id: string): void;
21
+ export declare function getCommentClearRevision(): number;
22
+ /** Clears saved notes and invalidates any open or saving comment draft. */
23
+ export declare function clearComments(target?: DraftTarget): void;
24
+ /** Arms only the exact notes successfully copied or accepted by an agent. */
25
+ export declare function markCommentsHandedOff(sent: readonly ElementComment[]): void;
26
+ export declare function setCommentToolActive(active: boolean): void;
27
+ export declare function isCommentToolActive(): boolean;
28
+ export declare function useCommentToolActive(): boolean;
29
+ export declare function commentRoute(doc: Document): string;
@@ -0,0 +1,121 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { isRenderedInstanceRef } from "../changes/editModel.js";
3
+ import { useCanvasCards, useFocusedCardId, useSelectedCardId } from "../canvas/canvasStore.js";
4
+ import { canWriteWorkspace } from "../canvas/workspaceLease.js";
5
+ import { contentEditTarget } from "../canvas/frameContent.js";
6
+ import { documentTarget, targetKey } from "../drafts/model.js";
7
+ import { getNudgeUiRuntimeConfig } from "../runtime/runtimeConfig.js";
8
+ let comments = [];
9
+ let loadedKey = null;
10
+ let loadedDemo = false;
11
+ const listeners = new Set();
12
+ const toolListeners = new Set();
13
+ let commentToolActive = false;
14
+ let clearRevision = 0;
15
+ function storageKey() {
16
+ return `nudge-ui:${getNudgeUiRuntimeConfig().projectId}:comments:v1`;
17
+ }
18
+ function isComment(value) {
19
+ if (!value || typeof value !== "object")
20
+ return false;
21
+ const item = value;
22
+ return ["id", "route", "tag", "note", "baseline", "viewport"].every((key) => typeof item[key] === "string")
23
+ && typeof item.handedOff === "boolean" && isRenderedInstanceRef(item.target);
24
+ }
25
+ export function getComments() {
26
+ const key = storageKey();
27
+ const demo = getNudgeUiRuntimeConfig().demo === true;
28
+ if (loadedKey !== key || loadedDemo !== demo) {
29
+ loadedKey = key;
30
+ loadedDemo = demo;
31
+ try {
32
+ const saved = demo ? [] : JSON.parse(localStorage.getItem(key) ?? "[]");
33
+ comments = Array.isArray(saved) ? saved.filter(isComment) : [];
34
+ }
35
+ catch {
36
+ comments = [];
37
+ }
38
+ }
39
+ return comments;
40
+ }
41
+ export function loadComments() {
42
+ loadedKey = null;
43
+ clearRevision += 1;
44
+ getComments();
45
+ for (const listener of listeners)
46
+ listener();
47
+ }
48
+ function publish(next) {
49
+ if (!canWriteWorkspace())
50
+ return;
51
+ comments = next;
52
+ if (getNudgeUiRuntimeConfig().demo !== true) {
53
+ try {
54
+ localStorage.setItem(storageKey(), JSON.stringify(next));
55
+ }
56
+ catch { }
57
+ }
58
+ for (const listener of listeners)
59
+ listener();
60
+ }
61
+ export function subscribeComments(listener) {
62
+ listeners.add(listener);
63
+ return () => listeners.delete(listener);
64
+ }
65
+ export function useComments() {
66
+ return useSyncExternalStore(subscribeComments, getComments, getComments);
67
+ }
68
+ export function getCommentsForTarget(target) {
69
+ return getComments().filter((comment) => targetKey(documentTarget(comment.route)) === targetKey(target));
70
+ }
71
+ export function useFrameComments() {
72
+ useComments();
73
+ const cards = useCanvasCards();
74
+ const selected = useSelectedCardId();
75
+ const focused = useFocusedCardId();
76
+ const card = cards.find((card) => card.id === selected) ?? cards.find((card) => card.id === focused) ?? cards[0];
77
+ return getCommentsForTarget(card ? contentEditTarget(card.content) : documentTarget(window.location.href));
78
+ }
79
+ export function saveComment(comment) {
80
+ const current = getComments();
81
+ publish([...current.filter((item) => item.id !== comment.id), comment]);
82
+ }
83
+ export function removeComment(id) {
84
+ publish(getComments().filter((item) => item.id !== id));
85
+ }
86
+ export function getCommentClearRevision() { return clearRevision; }
87
+ /** Clears saved notes and invalidates any open or saving comment draft. */
88
+ export function clearComments(target) {
89
+ if (!canWriteWorkspace())
90
+ return;
91
+ getComments();
92
+ clearRevision += 1;
93
+ const cleared = new Set((target ? getCommentsForTarget(target) : getComments()).map((comment) => comment.id));
94
+ publish(getComments().filter((comment) => !cleared.has(comment.id)));
95
+ }
96
+ /** Arms only the exact notes successfully copied or accepted by an agent. */
97
+ export function markCommentsHandedOff(sent) {
98
+ publish(getComments().map((item) => sent.some((candidate) => candidate.id === item.id && candidate.note === item.note)
99
+ ? { ...item, handedOff: true } : item));
100
+ }
101
+ export function setCommentToolActive(active) {
102
+ if (commentToolActive === active)
103
+ return;
104
+ commentToolActive = active;
105
+ for (const listener of toolListeners)
106
+ listener();
107
+ }
108
+ export function isCommentToolActive() { return commentToolActive; }
109
+ export function useCommentToolActive() {
110
+ return useSyncExternalStore((listener) => {
111
+ toolListeners.add(listener);
112
+ return () => toolListeners.delete(listener);
113
+ }, isCommentToolActive, isCommentToolActive);
114
+ }
115
+ export function commentRoute(doc) {
116
+ const url = new URL(doc.location.href);
117
+ url.hash = "";
118
+ url.searchParams.delete("nudge-ui");
119
+ url.searchParams.sort();
120
+ return url.href;
121
+ }
@@ -0,0 +1,5 @@
1
+ export declare function isReadingCommentSource(doc: Document): boolean;
2
+ /** Serializes clean reads and restores the latest previews even if the read fails. */
3
+ export declare function readCommentSource<T>(doc: Document, read: () => T): Promise<T | null>;
4
+ /** Changed, uniquely located elements resolve after handoff; missing and ambiguous targets remain. */
5
+ export declare function reconcileComments(doc: Document): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { getDraftChanges } from "../changes/draftChanges.js";
2
+ import { documentTarget, targetKey } from "../drafts/model.js";
3
+ import { isCanvasProjectionRevisionCurrent, projectDraftToDocument } from "../canvas/projection.js";
4
+ import { commentFingerprint, commentViewport, resolveCommentElement } from "./element.js";
5
+ import { commentRoute, getCommentClearRevision, getComments, removeComment } from "./store.js";
6
+ const documentReads = new WeakMap();
7
+ const readingDocuments = new WeakSet();
8
+ export function isReadingCommentSource(doc) { return readingDocuments.has(doc); }
9
+ /** Serializes clean reads and restores the latest previews even if the read fails. */
10
+ export function readCommentSource(doc, read) {
11
+ const previous = documentReads.get(doc) ?? Promise.resolve();
12
+ const operation = previous.catch(() => undefined).then(async () => {
13
+ readingDocuments.add(doc);
14
+ try {
15
+ const revision = await projectDraftToDocument(doc, { changes: [], structuralChanges: [] });
16
+ if (revision === null || !isCanvasProjectionRevisionCurrent(doc, revision))
17
+ return null;
18
+ return read();
19
+ }
20
+ finally {
21
+ try {
22
+ await projectDraftToDocument(doc, getDraftChanges(targetKey(documentTarget(doc.location.href))));
23
+ // Deliver projection mutation records before accepting another source signal.
24
+ await new Promise((resolve) => setTimeout(resolve, 0));
25
+ }
26
+ finally {
27
+ readingDocuments.delete(doc);
28
+ }
29
+ }
30
+ });
31
+ documentReads.set(doc, operation);
32
+ return operation;
33
+ }
34
+ /** Changed, uniquely located elements resolve after handoff; missing and ambiguous targets remain. */
35
+ export async function reconcileComments(doc) {
36
+ const revision = getCommentClearRevision();
37
+ const sent = getComments().filter((item) => item.handedOff && item.route === commentRoute(doc));
38
+ if (sent.length === 0)
39
+ return;
40
+ const resolved = await readCommentSource(doc, () => sent.filter((item) => {
41
+ if (item.viewport !== commentViewport(doc))
42
+ return false;
43
+ const element = resolveCommentElement(doc, item.target);
44
+ return element !== null && commentFingerprint(element) !== item.baseline;
45
+ }));
46
+ if (revision !== getCommentClearRevision())
47
+ return;
48
+ for (const item of resolved ?? []) {
49
+ if (getComments().some((current) => current.id === item.id && current.note === item.note && current.handedOff))
50
+ removeComment(item.id);
51
+ }
52
+ }