@jini-ai/ui 0.5.5 → 0.6.10

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 (263) hide show
  1. package/CHANGELOG.md +91 -2
  2. package/dist/core/theme/validation.js +40 -38
  3. package/dist/features/a2ui/renderer.js +15 -13
  4. package/dist/features/about/react/components/AboutTab.js +64 -34
  5. package/dist/features/about/rules.d.ts +2 -2
  6. package/dist/features/about/rules.js +156 -129
  7. package/dist/features/admin-widgets/components/ImagePreviewModal.d.ts +4 -3
  8. package/dist/features/admin-widgets/components/ImagePreviewModal.js +6 -10
  9. package/dist/features/admin-widgets/components/InfoTip.d.ts +3 -2
  10. package/dist/features/admin-widgets/components/InfoTip.hooks.js +2 -0
  11. package/dist/features/admin-widgets/components/InfoTip.js +34 -7
  12. package/dist/features/admin-widgets/components/InfoTip.rules.d.ts +13 -0
  13. package/dist/features/admin-widgets/components/InfoTip.rules.js +43 -10
  14. package/dist/features/admin-widgets/components/Lightbox/Lightbox.d.ts +30 -0
  15. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.d.ts +40 -0
  16. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +128 -0
  17. package/dist/features/admin-widgets/components/Lightbox/Lightbox.js +18 -0
  18. package/dist/features/admin-widgets/components/MiddleTruncate.d.ts +14 -0
  19. package/dist/features/admin-widgets/components/MiddleTruncate.js +12 -0
  20. package/dist/features/admin-widgets/components/MiddleTruncate.rules.d.ts +20 -0
  21. package/dist/features/admin-widgets/components/MiddleTruncate.rules.js +21 -0
  22. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.d.ts +18 -0
  23. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.d.ts +16 -0
  24. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +45 -0
  25. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.js +18 -0
  26. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.d.ts +32 -0
  27. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.js +26 -0
  28. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +29 -14
  29. package/dist/features/admin-widgets/index.d.ts +14 -2
  30. package/dist/features/admin-widgets/index.js +7 -1
  31. package/dist/features/admin-widgets/styles/admin-widgets.css +1 -0
  32. package/dist/features/asset-grid/react/components/AssetCard.js +7 -1
  33. package/dist/features/asset-grid/react/components/AssetGrid.d.ts +1 -1
  34. package/dist/features/asset-grid/react/components/AssetGrid.js +38 -18
  35. package/dist/features/asset-grid/react/components/useAssetGridFilters.d.ts +21 -0
  36. package/dist/features/asset-grid/react/components/useAssetGridFilters.js +19 -0
  37. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +16 -11
  38. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.d.ts +2 -0
  39. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +35 -11
  40. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.d.ts +6 -6
  41. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.js +10 -129
  42. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.d.ts +50 -0
  43. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.js +95 -0
  44. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.d.ts +56 -0
  45. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.js +80 -0
  46. package/dist/features/asset-tree-browser/rules.js +40 -22
  47. package/dist/features/browser-chrome/rules.js +74 -70
  48. package/dist/features/charts/browser.js +11 -5
  49. package/dist/features/charts/react.js +2 -2
  50. package/dist/features/charts/table.js +2 -2
  51. package/dist/features/command-palette/react/hooks/useCommandPalette.js +3 -9
  52. package/dist/features/connectors/ConnectorsBrowser.d.ts +4 -4
  53. package/dist/features/connectors/ConnectorsBrowser.js +26 -14
  54. package/dist/features/connectors/components/ConnectorCard.js +67 -38
  55. package/dist/features/connectors/components/ConnectorDetailDrawer.js +62 -31
  56. package/dist/features/connectors/components/ConnectorGrid.js +11 -6
  57. package/dist/features/connectors/hooks/useConnectorAuthorization.js +6 -0
  58. package/dist/features/connectors/hooks/useConnectorCatalog.d.ts +2 -0
  59. package/dist/features/connectors/hooks/useConnectorCatalog.js +32 -13
  60. package/dist/features/connectors/hooks/useWiredConnectorsBrowser.d.ts +3 -3
  61. package/dist/features/connectors/rules.d.ts +8 -1
  62. package/dist/features/connectors/rules.js +42 -20
  63. package/dist/features/connectors/types.d.ts +11 -0
  64. package/dist/features/execution/constants.d.ts +4 -10
  65. package/dist/features/execution/constants.js +9 -94
  66. package/dist/features/execution/react/components/ByokProviderForm.d.ts +2 -4
  67. package/dist/features/execution/react/components/ByokProviderForm.js +140 -43
  68. package/dist/features/execution/react/components/ExecutionTab.d.ts +1 -2
  69. package/dist/features/execution/react/components/ExecutionTab.js +52 -30
  70. package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -3
  71. package/dist/features/execution/react/components/LocalCliAgentList.js +14 -3
  72. package/dist/features/execution/react/components/LocalCliAgentSettings.js +14 -3
  73. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +54 -36
  74. package/dist/features/execution/react/hooks/useReasoningControl.js +114 -78
  75. package/dist/features/execution/rules.d.ts +2 -2
  76. package/dist/features/execution/rules.js +31 -15
  77. package/dist/features/execution/types.d.ts +4 -42
  78. package/dist/features/execution/types.js +0 -17
  79. package/dist/features/external-mcp/react/components/ExternalMcpTab.d.ts +6 -2
  80. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +9 -2
  81. package/dist/features/file-dropzone/react/components/FileDropzone.js +24 -25
  82. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.d.ts +2 -2
  83. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.js +35 -30
  84. package/dist/features/file-dropzone/react/components/FileDropzoneThumbnailGrid.js +30 -24
  85. package/dist/features/file-dropzone/react/components/FileDropzoneZone.d.ts +14 -0
  86. package/dist/features/file-dropzone/react/components/FileDropzoneZone.js +26 -0
  87. package/dist/features/html-editor/canvas-content-wrapper.js +13 -13
  88. package/dist/features/html-editor/canvas-embed-placeholders.js +46 -37
  89. package/dist/features/html-editor/css.js +76 -69
  90. package/dist/features/html-editor/editor-blocks.js +47 -33
  91. package/dist/features/html-editor/editor-serialization.js +6 -5
  92. package/dist/features/html-editor/protected-elements.js +39 -28
  93. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +4 -4
  94. package/dist/features/html-editor/source-splice.js +51 -30
  95. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +1 -0
  96. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +1 -0
  97. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +1 -0
  98. package/dist/features/i18n/locale.js +20 -16
  99. package/dist/features/iframe-pool/react/dom-sync.js +8 -6
  100. package/dist/features/integrations/react/components/CodexInstallToggleButton.js +19 -4
  101. package/dist/features/integrations/react/components/IntegrationsTab.js +60 -14
  102. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  103. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -7
  104. package/dist/features/lexical-rich-text-editor/mention-parser.js +60 -57
  105. package/dist/features/lexical-rich-text-editor/react/hooks/useKeyboardCommands.js +33 -38
  106. package/dist/features/lexical-rich-text-editor/rules.js +87 -74
  107. package/dist/features/lexical-rich-text-editor/serialize.js +27 -25
  108. package/dist/features/mcp-ui/secret-card.js +111 -61
  109. package/dist/features/mcp-ui/surfaces/confirmation.js +39 -21
  110. package/dist/features/mcp-ui/surfaces/document.d.ts +1 -1
  111. package/dist/features/mcp-ui/surfaces/document.js +10 -2
  112. package/dist/features/mcp-ui/surfaces/fields.js +30 -50
  113. package/dist/features/mcp-ui/surfaces/form.d.ts +6 -0
  114. package/dist/features/mcp-ui/surfaces/form.js +43 -21
  115. package/dist/features/mcp-ui/surfaces/outcome.js +30 -20
  116. package/dist/features/mcp-ui/surfaces/text-input.js +33 -21
  117. package/dist/features/mcp-ui/surfaces/tokens.d.ts +6 -1
  118. package/dist/features/mcp-ui/surfaces/tokens.js +5 -2
  119. package/dist/features/media-providers/constants.d.ts +1 -0
  120. package/dist/features/media-providers/constants.js +2 -81
  121. package/dist/features/media-providers/index.d.ts +1 -1
  122. package/dist/features/media-providers/ports.d.ts +2 -0
  123. package/dist/features/media-providers/react/components/MediaProviderCard.d.ts +22 -0
  124. package/dist/features/media-providers/react/components/MediaProviderCard.js +50 -0
  125. package/dist/features/media-providers/react/components/MediaProvidersTab.d.ts +5 -6
  126. package/dist/features/media-providers/react/components/MediaProvidersTab.js +40 -72
  127. package/dist/features/media-providers/react/components/media-providers-tab-labels.d.ts +11 -0
  128. package/dist/features/media-providers/react/components/media-providers-tab-labels.js +57 -0
  129. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.d.ts +4 -2
  130. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +37 -23
  131. package/dist/features/media-providers/rules.js +17 -10
  132. package/dist/features/media-providers/types.d.ts +4 -22
  133. package/dist/features/media-providers/types.js +3 -8
  134. package/dist/features/memory/formatters.js +127 -92
  135. package/dist/features/memory/react/components/MemoryConnectedPanel.js +92 -36
  136. package/dist/features/memory/react/components/MemoryList.js +37 -18
  137. package/dist/features/memory/react/hooks/useMemoryExtractions.store.js +42 -36
  138. package/dist/features/notifications/react/components/NotificationsTab.js +28 -28
  139. package/dist/features/observability/long-task.d.ts +0 -12
  140. package/dist/features/observability/long-task.js +15 -19
  141. package/dist/features/observability/resource-error.d.ts +0 -10
  142. package/dist/features/observability/resource-error.js +18 -13
  143. package/dist/features/observability/stuck-run.js +10 -6
  144. package/dist/features/observability/telemetry-url.d.ts +6 -0
  145. package/dist/features/observability/telemetry-url.js +16 -0
  146. package/dist/features/observability/white-screen.js +12 -24
  147. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
  148. package/dist/features/panel-kit/fetch-query/adapter.react.js +4 -2
  149. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +46 -33
  150. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +3 -1
  151. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +3 -1
  152. package/dist/features/privacy/react/components/PrivacyTab.js +24 -13
  153. package/dist/features/progress-card/components/ProgressCard.js +7 -3
  154. package/dist/features/progress-card/reference-adapters.js +59 -54
  155. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +24 -15
  156. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.d.ts +8 -0
  157. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.js +31 -23
  158. package/dist/features/resource-dashboard/react/components/ResourceBoard.js +6 -47
  159. package/dist/features/resource-dashboard/react/components/ResourceBoardView.d.ts +2 -2
  160. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -6
  161. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +15 -0
  162. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +35 -0
  163. package/dist/features/resource-dashboard/react/components/ResourceCard.js +5 -16
  164. package/dist/features/resource-dashboard/react/components/ResourceCardParts.d.ts +34 -0
  165. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +34 -0
  166. package/dist/features/resource-dashboard/react/components/ResourceRowList.js +3 -1
  167. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +2 -2
  168. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.d.ts +19 -0
  169. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +11 -0
  170. package/dist/features/resource-dashboard/react/components/ResourceRowListView.d.ts +1 -1
  171. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +4 -4
  172. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.d.ts +10 -0
  173. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.js +20 -0
  174. package/dist/features/resource-dashboard/react/components/resourceBoardProps.d.ts +48 -0
  175. package/dist/features/resource-dashboard/react/components/resourceBoardProps.js +70 -0
  176. package/dist/features/resource-dashboard/react/components/resourceRowListProps.d.ts +19 -0
  177. package/dist/features/resource-dashboard/react/components/resourceRowListProps.js +14 -0
  178. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.d.ts +16 -0
  179. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.js +13 -0
  180. package/dist/features/resource-dashboard/rules.js +2 -8
  181. package/dist/features/sketch-editor/dom.js +150 -99
  182. package/dist/features/sketch-editor/react/components/SketchEditor.js +26 -15
  183. package/dist/features/skills/react/components/SkillDraftForm.js +13 -19
  184. package/dist/features/skills/react/components/SkillDraftForm.labels.d.ts +31 -0
  185. package/dist/features/skills/react/components/SkillDraftForm.labels.js +54 -0
  186. package/dist/features/skills/react/components/SkillRow.js +38 -18
  187. package/dist/features/skills/react/components/SkillRow.labels.d.ts +7 -0
  188. package/dist/features/skills/react/components/SkillRow.labels.js +18 -0
  189. package/dist/features/skills/react/components/SkillsTab.js +24 -14
  190. package/dist/features/skills/react/components/SkillsTab.labels.d.ts +7 -0
  191. package/dist/features/skills/react/components/SkillsTab.labels.js +11 -0
  192. package/dist/features/skills/rules.js +32 -23
  193. package/dist/features/source-config-list/agent-handles.js +2 -1
  194. package/dist/features/source-config-list/dependencies.js +53 -49
  195. package/dist/features/source-config-list/index.d.ts +3 -3
  196. package/dist/features/source-config-list/react/components/SourceConfigAddForm.d.ts +6 -5
  197. package/dist/features/source-config-list/react/components/SourceConfigAddForm.js +38 -7
  198. package/dist/features/source-config-list/react/components/SourceConfigField.js +69 -25
  199. package/dist/features/source-config-list/react/components/SourceConfigItemCard.d.ts +6 -5
  200. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +76 -20
  201. package/dist/features/source-config-list/react/components/SourceConfigListView.d.ts +5 -3
  202. package/dist/features/source-config-list/react/components/SourceConfigListView.js +33 -3
  203. package/dist/features/source-config-list/react/components/SourceConfigTestControl.js +17 -1
  204. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.d.ts +11 -0
  205. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.js +12 -0
  206. package/dist/features/source-config-list/types.d.ts +14 -0
  207. package/dist/features/tab-launcher-menu/react/components/TabLauncherMenu.js +25 -1
  208. package/dist/features/tab-launcher-menu/react/hooks/useTabLauncherMenu.js +15 -16
  209. package/dist/features/tab-strip/react/components/TabBar.js +8 -1
  210. package/dist/features/tabbed-dialog/react/components/TabbedDialog.js +8 -30
  211. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.d.ts +15 -0
  212. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.js +8 -0
  213. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.d.ts +12 -0
  214. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.js +6 -0
  215. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.d.ts +27 -0
  216. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.js +12 -0
  217. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.d.ts +43 -0
  218. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.js +59 -0
  219. package/dist/features/version-manager/dependencies.d.ts +3 -1
  220. package/dist/features/version-manager/dependencies.js +3 -2
  221. package/dist/features/version-manager/react/components/VersionManagerModal.js +10 -6
  222. package/dist/features/version-manager/react/hooks/useVersionManager.js +7 -4
  223. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +103 -50
  224. package/dist/features/viewer-shell/react/components/MarkdownSplitPane.js +28 -8
  225. package/dist/features/viewer-shell/react/hooks/useCommentReorder.js +22 -16
  226. package/dist/features/viewer-shell/rules.js +19 -23
  227. package/dist/react/components/CustomSelect.d.ts +38 -1
  228. package/dist/react/components/CustomSelect.js +94 -38
  229. package/dist/react/components/OnboardingDropdown.js +20 -8
  230. package/dist/react/components/RemixIcon.stylesheet.js +3 -1
  231. package/dist/react/components/Toast.js +13 -2
  232. package/dist/react/components/TooltipLayer.js +5 -14
  233. package/dist/react/components/WorkingDirPicker.js +78 -50
  234. package/dist/react/components/remixicon-font-recovery.js +38 -30
  235. package/dist/react/components/select-popover.d.ts +8 -6
  236. package/dist/react/components/select-popover.js +33 -29
  237. package/dist/react/mcp-ui/McpUiHost.d.ts +1 -1
  238. package/dist/react/mcp-ui/McpUiHost.js +10 -11
  239. package/dist/react/mcp-ui/host-message-source.d.ts +14 -2
  240. package/dist/react/mcp-ui/host-message-source.js +15 -5
  241. package/dist/react/mcp-ui/mcp-ui-host-frame.d.ts +41 -0
  242. package/dist/react/mcp-ui/mcp-ui-host-frame.js +39 -0
  243. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +97 -69
  244. package/dist/renderers/preview-modal-shell/react/components/PreviewModalShell.js +59 -30
  245. package/dist/renderers/react/components/ArtifactView.js +9 -6
  246. package/dist/renderers/srcdoc/build.d.ts +4 -1
  247. package/dist/renderers/srcdoc/build.js +62 -48
  248. package/dist/styles/admin.css +29 -14
  249. package/dist/styles/lightbox.css +142 -0
  250. package/dist/styles/variables.css +128 -0
  251. package/dist/theme/apply.js +9 -5
  252. package/dist/utils/auto-open-file.d.ts +2 -1
  253. package/dist/utils/auto-open-file.js +69 -85
  254. package/dist/utils/markdown-scroll-sync.js +108 -87
  255. package/dist/utils/polygon-selection.js +25 -26
  256. package/dist/utils/smooth-scroll-to-top.d.ts +1 -1
  257. package/dist/utils/smooth-scroll-to-top.js +2 -32
  258. package/dist/utils/sse.js +26 -26
  259. package/dist/utils/unit-bezier.d.ts +7 -0
  260. package/dist/utils/unit-bezier.js +38 -0
  261. package/package.json +9 -12
  262. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.d.ts +0 -13
  263. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.js +0 -46
@@ -205,6 +205,10 @@ function isUsableRect(r) {
205
205
  function toCaretRect(r) {
206
206
  return { top: r.top, bottom: r.bottom, left: r.left, right: r.right };
207
207
  }
208
+ /** A caret rect pinned to a box's left edge (zero width). */
209
+ function leftEdgeRect(r) {
210
+ return { top: r.top, bottom: r.bottom, left: r.left, right: r.left };
211
+ }
208
212
  /** Reads the caret's viewport rect via the native selection, with ordered
209
213
  * fallbacks for the collapsed-caret 0×0 case: (1) range bounding rect,
210
214
  * (2) range client-rects list, (3) a zero-width probe range cloned at the
@@ -213,99 +217,108 @@ export function readCaretRect(rootEl) {
213
217
  if (typeof window === 'undefined')
214
218
  return null;
215
219
  const winSel = window.getSelection();
216
- if (winSel && winSel.rangeCount > 0) {
217
- const range = winSel.getRangeAt(0);
218
- // jsdom does not implement Range geometry — guard every native call so a
219
- // missing method falls through to the element/root box instead of
220
- // throwing.
221
- if (typeof range.getBoundingClientRect === 'function') {
222
- const r = range.getBoundingClientRect();
223
- if (isUsableRect(r))
224
- return toCaretRect(r);
225
- }
226
- if (typeof range.getClientRects === 'function') {
227
- const rects = range.getClientRects();
228
- if (rects.length > 0 && isUsableRect(rects[0]))
229
- return toCaretRect(rects[0]);
230
- }
231
- const anchorNode = range.startContainer;
232
- if (anchorNode.nodeType === Node.TEXT_NODE) {
233
- try {
234
- const probe = document.createRange();
235
- const len = anchorNode.length;
236
- const off = Math.min(range.startOffset, len);
237
- probe.setStart(anchorNode, off);
238
- probe.setEnd(anchorNode, off);
239
- if (typeof probe.getBoundingClientRect === 'function') {
240
- const pr = probe.getBoundingClientRect();
241
- if (isUsableRect(pr))
242
- return toCaretRect(pr);
243
- }
244
- if (typeof probe.getClientRects === 'function') {
245
- const prList = probe.getClientRects();
246
- if (prList.length > 0 && isUsableRect(prList[0]))
247
- return toCaretRect(prList[0]);
248
- }
249
- }
250
- catch {
251
- /* fall through */
252
- }
253
- }
254
- const el = anchorNode.nodeType === Node.ELEMENT_NODE
255
- ? anchorNode
256
- : anchorNode.parentElement;
257
- if (el) {
258
- const er = el.getBoundingClientRect();
259
- if (isUsableRect(er)) {
260
- return { top: er.top, bottom: er.bottom, left: er.left, right: er.left };
261
- }
262
- }
220
+ const fromSelection = winSel && winSel.rangeCount > 0 ? caretRectFromRange(winSel.getRangeAt(0)) : null;
221
+ if (fromSelection)
222
+ return fromSelection;
223
+ return rootEl ? leftEdgeRect(rootEl.getBoundingClientRect()) : null;
224
+ }
225
+ /** Fallbacks (1)–(4) of `readCaretRect` for one live selection range. */
226
+ function caretRectFromRange(range) {
227
+ const fromRange = rangeGeometryRect(range);
228
+ if (fromRange)
229
+ return fromRange;
230
+ const anchorNode = range.startContainer;
231
+ if (anchorNode.nodeType === Node.TEXT_NODE) {
232
+ const probed = probeTextCaretRect({ text: anchorNode, offset: range.startOffset });
233
+ if (probed)
234
+ return probed;
235
+ }
236
+ return anchorElementRect(anchorNode);
237
+ }
238
+ /** Fallbacks (1) and (2): a range's bounding rect, then its first client rect. */
239
+ function rangeGeometryRect(range) {
240
+ // jsdom does not implement Range geometry — guard every native call so a
241
+ // missing method falls through to the element/root box instead of
242
+ // throwing.
243
+ if (typeof range.getBoundingClientRect === 'function') {
244
+ const r = range.getBoundingClientRect();
245
+ if (isUsableRect(r))
246
+ return toCaretRect(r);
263
247
  }
264
- if (rootEl) {
265
- const rr = rootEl.getBoundingClientRect();
266
- return { top: rr.top, bottom: rr.bottom, left: rr.left, right: rr.left };
248
+ if (typeof range.getClientRects === 'function') {
249
+ const rects = range.getClientRects();
250
+ if (rects.length > 0 && isUsableRect(rects[0]))
251
+ return toCaretRect(rects[0]);
267
252
  }
268
253
  return null;
269
254
  }
255
+ /** Fallback (3): geometry of a zero-width probe range at the anchor text offset. */
256
+ function probeTextCaretRect(at) {
257
+ try {
258
+ const probe = document.createRange();
259
+ const off = Math.min(at.offset, at.text.length);
260
+ probe.setStart(at.text, off);
261
+ probe.setEnd(at.text, off);
262
+ return rangeGeometryRect(probe);
263
+ }
264
+ catch {
265
+ /* fall through */
266
+ return null;
267
+ }
268
+ }
269
+ /** Fallback (4): the anchor element's box (or the text anchor's parent's). */
270
+ function anchorElementRect(anchorNode) {
271
+ const el = anchorNode.nodeType === Node.ELEMENT_NODE ? anchorNode : anchorNode.parentElement;
272
+ if (!el)
273
+ return null;
274
+ const er = el.getBoundingClientRect();
275
+ return isUsableRect(er) ? leftEdgeRect(er) : null;
276
+ }
270
277
  /** Default ABOVE the caret (a composer typically sits at a panel's bottom,
271
278
  * so above never occludes the input line); flip BELOW only when above lacks
272
279
  * room. Clamp horizontally into the viewport (or `boundary`, when given).
273
280
  * Caps height; the caller's popover scrolls internally. */
274
281
  export function computeCaretFloatingLayerPosition(caret, size, boundary, config) {
275
- const { gap, margin, hardMaxHeight, preferredWidth } = config;
282
+ const { margin } = config;
276
283
  const vw = window.innerWidth;
277
- const vh = window.innerHeight;
278
- const viewportAvailableWidth = vw - margin * 2;
279
- const boundaryAvailableWidth = boundary ? boundary.width - margin * 2 : viewportAvailableWidth;
284
+ const width = layerWidth({ vw, boundary, margin, preferredWidth: config.preferredWidth });
285
+ const vertical = verticalPlacement({
286
+ caret,
287
+ vh: window.innerHeight,
288
+ wantedH: size?.height ?? config.hardMaxHeight,
289
+ ...config,
290
+ });
291
+ const left = clampLeft({ caretLeft: caret.left, vw, boundary, margin, width });
292
+ return { left, top: vertical.top, width, maxHeight: vertical.maxHeight, placement: vertical.placement };
293
+ }
294
+ function layerWidth(at) {
295
+ const viewportAvailableWidth = at.vw - at.margin * 2;
296
+ const boundaryAvailableWidth = at.boundary
297
+ ? at.boundary.width - at.margin * 2
298
+ : viewportAvailableWidth;
280
299
  const availableWidth = Math.max(240, Math.min(viewportAvailableWidth, boundaryAvailableWidth));
281
- const width = Math.min(preferredWidth, availableWidth);
300
+ return Math.min(at.preferredWidth, availableWidth);
301
+ }
302
+ function verticalPlacement(at) {
303
+ const { caret, gap, margin, wantedH } = at;
282
304
  const spaceAbove = caret.top - gap - margin;
283
- const spaceBelow = vh - caret.bottom - gap - margin;
284
- const wantedH = size?.height ?? hardMaxHeight;
305
+ const spaceBelow = at.vh - caret.bottom - gap - margin;
285
306
  const aboveFits = spaceAbove >= Math.min(wantedH, 160);
286
307
  const placement = aboveFits || spaceAbove >= spaceBelow ? 'above' : 'below';
287
308
  const space = placement === 'above' ? spaceAbove : spaceBelow;
288
- const maxHeight = Math.max(120, Math.min(hardMaxHeight, space));
289
- let top;
290
- if (placement === 'above') {
291
- const h = Math.min(wantedH, maxHeight);
292
- top = caret.top - gap - h;
293
- if (top < margin)
294
- top = margin;
295
- }
296
- else {
297
- top = caret.bottom + gap;
298
- }
309
+ const maxHeight = Math.max(120, Math.min(at.hardMaxHeight, space));
310
+ if (placement === 'below')
311
+ return { top: caret.bottom + gap, maxHeight, placement };
312
+ const h = Math.min(wantedH, maxHeight);
313
+ return { top: Math.max(margin, caret.top - gap - h), maxHeight, placement };
314
+ }
315
+ function clampLeft(at) {
316
+ const { vw, boundary, margin, width } = at;
299
317
  const minLeft = boundary ? Math.max(margin, boundary.left + margin) : margin;
300
318
  const maxLeft = boundary
301
319
  ? Math.max(minLeft, Math.min(vw - margin - width, boundary.right - margin - width))
302
320
  : vw - margin - width;
303
- let left = caret.left;
304
- if (left > maxLeft)
305
- left = maxLeft;
306
- if (left < minLeft)
307
- left = minLeft;
308
- return { left, top, width, maxHeight, placement };
321
+ return Math.max(minLeft, Math.min(at.caretLeft, maxLeft));
309
322
  }
310
323
  /** Combines detection + caret-rect reading into the shape `RichTextInput`
311
324
  * reports upward on every editor update. */
@@ -9,32 +9,34 @@ import { $isMentionNode } from './mention-node.js';
9
9
  export function serializeRichText(state) {
10
10
  return state.read(() => {
11
11
  const mentions = [];
12
- const blocks = [];
13
- for (const block of $getRoot().getChildren()) {
14
- if (!$isParagraphNode(block)) {
15
- // Stray non-paragraph block (shouldn't happen): fall back to its text.
16
- blocks.push(block.getTextContent());
17
- continue;
18
- }
19
- let line = '';
20
- for (const child of block.getChildren()) {
21
- if ($isMentionNode(child)) {
22
- line += child.getToken();
23
- mentions.push(child.getEntity());
24
- }
25
- else if ($isLineBreakNode(child)) {
26
- line += '\n';
27
- }
28
- else if ($isTextNode(child)) {
29
- line += child.getTextContent();
30
- }
31
- else if ($isElementNode(child)) {
32
- line += child.getTextContent();
33
- }
34
- }
35
- blocks.push(line);
36
- }
12
+ const blocks = $getRoot()
13
+ .getChildren()
14
+ .map((block) => $isParagraphNode(block)
15
+ ? serializeParagraph({ paragraph: block, mentions })
16
+ : // Stray non-paragraph block (shouldn't happen): fall back to its text.
17
+ block.getTextContent());
37
18
  return { text: blocks.join('\n'), mentions };
38
19
  });
39
20
  }
21
+ /**
22
+ * One paragraph's wire text; appends each atomic mention's entity to
23
+ * `mentions` in document order. A non-mention decorator child contributes
24
+ * nothing.
25
+ */
26
+ function serializeParagraph(at) {
27
+ let line = '';
28
+ for (const child of at.paragraph.getChildren()) {
29
+ if ($isMentionNode(child)) {
30
+ line += child.getToken();
31
+ at.mentions.push(child.getEntity());
32
+ }
33
+ else if ($isLineBreakNode(child)) {
34
+ line += '\n';
35
+ }
36
+ else if ($isTextNode(child) || $isElementNode(child)) {
37
+ line += child.getTextContent();
38
+ }
39
+ }
40
+ return line;
41
+ }
40
42
  //# sourceMappingURL=serialize.js.map
@@ -27,51 +27,131 @@ function checkedFields(form) {
27
27
  return form.fields.map(field => ({ ...field }));
28
28
  }
29
29
  /** Classify before persistence; an abort wins over an already-buffered answer.
30
- * Trimming tests blankness only: rewriting a provider secret can corrupt intentional newlines.
31
- * @complexity O(n + b) before save in fields and submitted secret bytes; failure text delegates
32
- * to core's exact-secret redaction scan. Host persistence cost is external. */
33
- async function saveAnswer(required) {
34
- const { answer, fields, prep, ctx, spec, options, exchangeId } = required;
35
- if (ctx.signal.aborted)
36
- return { status: 'abandoned' };
30
+ * @complexity O(1). */
31
+ function answerStep(required) {
32
+ const { answer, signal } = required;
33
+ if (signal.aborted)
34
+ return { run: { status: 'abandoned' } };
37
35
  if (answer.status !== 'received')
38
- return { status: answer.status };
36
+ return { run: { status: answer.status } };
39
37
  if (answer.params[SURFACE_DISMISSED_PARAM] === true)
40
- return { status: 'cancelled' };
41
- const values = Object.fromEntries(fields.map(field => [field.name, answer.params[field.name]]));
38
+ return { run: { status: 'cancelled' } };
39
+ return { params: answer.params };
40
+ }
41
+ /** The exact submitted secret, or undefined when it is blank or malformed.
42
+ * Trimming tests blankness only: rewriting a provider secret can corrupt intentional newlines.
43
+ * @complexity O(b) in the submitted secret bytes. */
44
+ function secretValue(required) {
45
+ const { field, value } = required;
46
+ if (value !== undefined && typeof value !== 'string')
47
+ return undefined;
48
+ const secret = typeof value === 'string' ? value : '';
49
+ if (secret.trim() === '' && (secret !== '' || field.allowBlank !== true))
50
+ return undefined;
51
+ return secret;
52
+ }
53
+ /** Project only declared names; undefined means a required secret was blank or malformed.
54
+ * @complexity O(n + b) in fields and submitted secret bytes. */
55
+ function collectValues(required) {
56
+ const { fields, params } = required;
57
+ const values = Object.fromEntries(fields.map(field => [field.name, params[field.name]]));
42
58
  const secrets = [];
43
59
  for (const field of fields) {
44
60
  if (field.secret !== true)
45
61
  continue;
46
- const value = values[field.name];
47
- if (value !== undefined && typeof value !== 'string')
48
- return { status: 'blank' };
49
- const secret = typeof value === 'string' ? value : '';
50
- if (secret.trim() === '' && (secret !== '' || field.allowBlank !== true))
51
- return { status: 'blank' };
62
+ const secret = secretValue({ field, value: values[field.name] });
63
+ if (secret === undefined)
64
+ return undefined;
52
65
  values[field.name] = secret;
53
66
  secrets.push(secret);
54
67
  }
68
+ return { values, secrets };
69
+ }
70
+ /** Map a save exception to fixed, redacted text and log fixed metadata only.
71
+ * @complexity O(m + b): delegates to core's exact-secret redaction scan. */
72
+ function failedRun(required) {
73
+ const { error, secrets, signal, toolId, options, exchangeId } = required;
74
+ if (signal.aborted)
75
+ return { status: 'abandoned' };
76
+ let safeMessage = options.text?.saveFailure ?? 'Saving failed. Nothing was saved.';
77
+ try {
78
+ safeMessage = options.safeError?.(error) ?? safeMessage;
79
+ }
80
+ catch { /* An error mapper must not disclose a second exception containing the submitted value. */ }
81
+ // Even an accidentally permissive allowlist must not echo the submitted secret.
82
+ safeMessage = redactSecrets({ input: safeMessage }, { exactSecrets: secrets });
83
+ try {
84
+ options.logFailure?.({ toolId, exchangeId, saved: false });
85
+ }
86
+ catch { /* Logging must not replace the safe model result with an opaque logger exception. */ }
87
+ return { status: 'failed', safeMessage };
88
+ }
89
+ /** Classify, then persist; an abort wins over an already-buffered answer.
90
+ * @complexity O(n + b) before save in fields and submitted secret bytes; failure text delegates
91
+ * to core's exact-secret redaction scan. Host persistence cost is external. */
92
+ async function saveAnswer(required) {
93
+ const { answer, fields, prep, ctx, spec, options, exchangeId } = required;
94
+ const step = answerStep({ answer, signal: ctx.signal });
95
+ if ('run' in step)
96
+ return step.run;
97
+ const collected = collectValues({ fields, params: step.params });
98
+ if (collected === undefined)
99
+ return { status: 'blank' };
55
100
  if (ctx.signal.aborted)
56
101
  return { status: 'abandoned' };
102
+ const { values, secrets } = collected;
57
103
  try {
58
104
  return { status: 'saved', saved: await spec.save({ values, prep, signal: ctx.signal }) };
59
105
  }
60
106
  catch (error) {
107
+ return failedRun({ error, secrets, signal: ctx.signal, toolId: spec.toolId, options, exchangeId });
108
+ }
109
+ }
110
+ /** Fail closed without an emitter. @complexity O(1). @throws ToolInputError with fixed text. */
111
+ function requireEmitter(required, optional) {
112
+ if (optional.emitSurface)
113
+ return optional.emitSurface;
114
+ throw new ToolInputError({ message: required.options.text?.noEmitter ?? `${required.toolId}: this execution context has no interactive form channel (no emitSurface), so this form cannot be shown here. Nothing was changed.` });
115
+ }
116
+ /** The form resource: engine-owned routing params and dismissal around the host's copy.
117
+ * @complexity O(n + h) in fields and rendered HTML. */
118
+ function buildCardForm(required) {
119
+ const { form, fields, uri, framing, toolId, exchangeId, options } = required;
120
+ return buildFormSurface({ ...form, fields, uri, ...framing, toolName: toolId,
121
+ baseParams: { [SURFACE_EXCHANGE_ID_PARAM]: exchangeId },
122
+ cancel: { label: form.cancelLabel ?? options.text?.cancelLabel ?? 'Cancel', toolName: toolId,
123
+ params: { [SURFACE_EXCHANGE_ID_PARAM]: exchangeId, [SURFACE_DISMISSED_PARAM]: true } },
124
+ });
125
+ }
126
+ /** Open the exchange, emit the card and hold the call until the real save is reported.
127
+ * @complexity O(n + h + b) locally, plus host I/O. */
128
+ async function runCard(required) {
129
+ const { spec, options, deps, ctx, emit, prep, form, fields } = required;
130
+ const exchange = deps.surfaceExchanges.open({ binding: { toolId: spec.toolId, principalId: ctx.principal.id, channel: 'mcp-ui' }, emit });
131
+ const closeOnAbort = () => exchange.close({});
132
+ ctx.signal.addEventListener('abort', closeOnAbort, { once: true });
133
+ try {
134
+ // Close the open/register race as well as the prepare/abort race.
61
135
  if (ctx.signal.aborted)
62
- return { status: 'abandoned' };
63
- let safeMessage = options.text?.saveFailure ?? 'Saving failed. Nothing was saved.';
64
- try {
65
- safeMessage = options.safeError?.(error) ?? safeMessage;
66
- }
67
- catch { /* An error mapper must not disclose a second exception containing the submitted value. */ }
68
- // Even an accidentally permissive allowlist must not echo the submitted secret.
69
- safeMessage = redactSecrets({ input: safeMessage }, { exactSecrets: secrets });
70
- try {
71
- options.logFailure?.({ toolId: spec.toolId, exchangeId, saved: false });
72
- }
73
- catch { /* Logging must not replace the safe model result with an opaque logger exception. */ }
74
- return { status: 'failed', safeMessage };
136
+ return spec.result({ prep, run: { status: 'abandoned' } });
137
+ const uri = `ui://${options.uriHost ?? 'jini'}/secret-card/${encodeURIComponent(spec.toolId)}/${encodeURIComponent(exchange.id)}`;
138
+ const framing = options.frameSize === undefined ? {} : { preferredFrameSize: options.frameSize };
139
+ const resource = buildCardForm({ form, fields, uri, framing, toolId: spec.toolId, exchangeId: exchange.id, options });
140
+ return await deps.askThenReport({ exchange, confirmationEmission: { channel: 'mcp-ui', payload: { resource } },
141
+ handle: async (answer) => {
142
+ const run = await saveAnswer({ answer, fields, prep, ctx, spec, options, exchangeId: exchange.id });
143
+ const result = spec.result({ prep, run });
144
+ const outcome = spec.outcome({ prep, run });
145
+ if (outcome === undefined)
146
+ return { result };
147
+ return { result, outcome: { channel: 'mcp-ui', payload: { resource: buildOutcomeSurface({ ...outcome, uri, ...framing }) } } };
148
+ },
149
+ });
150
+ }
151
+ finally {
152
+ ctx.signal.removeEventListener('abort', closeOnAbort);
153
+ // Also covers rendering/projection failures before or inside askThenReport; close is idempotent.
154
+ exchange.close({});
75
155
  }
76
156
  }
77
157
  /**
@@ -92,8 +172,7 @@ export function defineSecretCardTool(required, options = {}) {
92
172
  toolId: spec.toolId,
93
173
  secretFieldNames: prep => checkedFields(spec.form({ prep })).filter(field => field.secret === true).map(field => field.name),
94
174
  handler: deps => async (ctx, optional = {}) => {
95
- if (!optional.emitSurface)
96
- throw new ToolInputError({ message: options.text?.noEmitter ?? `${spec.toolId}: this execution context has no interactive form channel (no emitSurface), so this form cannot be shown here. Nothing was changed.` });
175
+ const emit = requireEmitter({ toolId: spec.toolId, options }, optional);
97
176
  const prep = await spec.prepare({ ctx });
98
177
  if (ctx.signal.aborted)
99
178
  return spec.result({ prep, run: { status: 'abandoned' } });
@@ -101,36 +180,7 @@ export function defineSecretCardTool(required, options = {}) {
101
180
  const fields = checkedFields(form);
102
181
  if (ctx.signal.aborted)
103
182
  return spec.result({ prep, run: { status: 'abandoned' } });
104
- const exchange = deps.surfaceExchanges.open({ binding: { toolId: spec.toolId, principalId: ctx.principal.id, channel: 'mcp-ui' }, emit: optional.emitSurface });
105
- const closeOnAbort = () => exchange.close({});
106
- ctx.signal.addEventListener('abort', closeOnAbort, { once: true });
107
- try {
108
- // Close the open/register race as well as the prepare/abort race.
109
- if (ctx.signal.aborted)
110
- return spec.result({ prep, run: { status: 'abandoned' } });
111
- const uri = `ui://${options.uriHost ?? 'jini'}/secret-card/${encodeURIComponent(spec.toolId)}/${encodeURIComponent(exchange.id)}`;
112
- const framing = options.frameSize === undefined ? {} : { preferredFrameSize: options.frameSize };
113
- const resource = buildFormSurface({ ...form, fields, uri, ...framing, toolName: spec.toolId,
114
- baseParams: { [SURFACE_EXCHANGE_ID_PARAM]: exchange.id },
115
- cancel: { label: form.cancelLabel ?? options.text?.cancelLabel ?? 'Cancel', toolName: spec.toolId,
116
- params: { [SURFACE_EXCHANGE_ID_PARAM]: exchange.id, [SURFACE_DISMISSED_PARAM]: true } },
117
- });
118
- return await deps.askThenReport({ exchange, confirmationEmission: { channel: 'mcp-ui', payload: { resource } },
119
- handle: async (answer) => {
120
- const run = await saveAnswer({ answer, fields, prep, ctx, spec, options, exchangeId: exchange.id });
121
- const result = spec.result({ prep, run });
122
- const outcome = spec.outcome({ prep, run });
123
- if (outcome === undefined)
124
- return { result };
125
- return { result, outcome: { channel: 'mcp-ui', payload: { resource: buildOutcomeSurface({ ...outcome, uri, ...framing }) } } };
126
- },
127
- });
128
- }
129
- finally {
130
- ctx.signal.removeEventListener('abort', closeOnAbort);
131
- // Also covers rendering/projection failures before or inside askThenReport; close is idempotent.
132
- exchange.close({});
133
- }
183
+ return runCard({ spec, options, deps, ctx, emit, prep, form, fields });
134
184
  },
135
185
  };
136
186
  }
@@ -110,43 +110,47 @@ function confirmationActions(spec) {
110
110
  actions.push({ id: 'cancel', label: spec.cancel.label, variant: 'neutral' });
111
111
  return actions;
112
112
  }
113
- export function renderConfirmationDocument(spec) {
114
- const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...spec.text };
115
- const choicesParam = spec.choicesParam ?? 'overwrite';
116
- // The ticked ids are merged into confirm.params at click time; a key already there (the token,
117
- // say) would be silently replaced by an array.
118
- if ((spec.choices ?? []).length > 0 && Object.prototype.hasOwnProperty.call(spec.confirm.params, choicesParam)) {
113
+ /** The ticked ids are merged into confirm.params at click time; a key already there (the token,
114
+ * say) would be silently replaced by an array.
115
+ * @complexity O(a) in the alternative count. @throws Error naming the colliding param (and alternative). */
116
+ function assertChoicesParamFree(spec, required) {
117
+ const { choicesParam } = required;
118
+ if ((spec.choices ?? []).length === 0)
119
+ return;
120
+ if (Object.prototype.hasOwnProperty.call(spec.confirm.params, choicesParam)) {
119
121
  throw new Error(`choicesParam ${JSON.stringify(choicesParam)} collides with a key already in confirm.params.`);
120
122
  }
121
- for (const alternative of (spec.choices ?? []).length > 0 ? (spec.alternatives ?? []) : []) {
123
+ for (const alternative of spec.alternatives ?? []) {
122
124
  if (Object.prototype.hasOwnProperty.call(alternative.params, choicesParam)) {
123
125
  throw new Error(`choicesParam ${JSON.stringify(choicesParam)} collides with a key already in the params of alternative ${JSON.stringify(alternative.id)}.`);
124
126
  }
125
127
  }
126
- const alternatives = spec.alternatives ?? [];
127
- assertAlternativeIds(alternatives);
128
- const actions = confirmationActions(spec);
129
- // Every affirmative button starts disabled and waits out the dwell; only cancel is live at once.
130
- const affirmative = new Set(['confirm', ...alternatives.map((alternative) => alternative.id)]);
128
+ }
129
+ /** Header, context, choices, warning, actions and the status region. @complexity O(n) in the rendered length. */
130
+ function confirmationBodyHtml(spec, required) {
131
+ const { affirmative } = required;
131
132
  const warning = spec.warning === undefined ? '' : `<p class="mcpui-warning">${escapeHtml(spec.warning)}</p>`;
132
- const bodyHtml = [
133
+ return [
133
134
  renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
134
135
  renderDetailList(spec.details ?? []),
135
136
  renderChoices(spec.choices ?? []),
136
137
  warning,
137
138
  // Confirm starts disabled; the script enables it once the dwell has run out.
138
- renderActions(actions.map((action) => (affirmative.has(action.id) ? { ...action, disabled: true } : action))),
139
+ renderActions(confirmationActions(spec).map((action) => (affirmative.has(action.id) ? { ...action, disabled: true } : action))),
139
140
  renderStatusRegion(),
140
141
  ]
141
142
  .filter((fragment) => fragment !== '')
142
143
  .join('\n');
143
- // A map from button id to what that button does. `null` means "dismiss locally, call nothing" —
144
- // the shape a cancel-without-a-tool takes, kept as an explicit null rather than an absent key so
145
- // the script can tell "this button dismisses" from "this button is unknown to me".
144
+ }
145
+ /** A map from button id to what that button does. `null` means "dismiss locally, call nothing" —
146
+ * the shape a cancel-without-a-tool takes, kept as an explicit null rather than an absent key so
147
+ * the script can tell "this button dismisses" from "this button is unknown to me".
148
+ * @complexity O(a) in the alternative count. */
149
+ function confirmationPlan(spec, required) {
146
150
  const plan = {
147
151
  confirm: { toolName: spec.confirm.toolName, params: spec.confirm.params },
148
152
  };
149
- for (const alternative of alternatives) {
153
+ for (const alternative of required.alternatives) {
150
154
  plan[alternative.id] = { toolName: alternative.toolName, params: alternative.params };
151
155
  }
152
156
  if (spec.cancel !== undefined) {
@@ -154,7 +158,12 @@ export function renderConfirmationDocument(spec) {
154
158
  ? { toolName: spec.cancel.toolName, params: spec.cancel.params }
155
159
  : null;
156
160
  }
157
- const script = `(function () {
161
+ return plan;
162
+ }
163
+ /** The inline runtime script: dwell, visibility, locking and tool calls. @complexity O(s) in the script length. */
164
+ function confirmationScript(required) {
165
+ const { plan, text, choicesParam, affirmative } = required;
166
+ return `(function () {
158
167
  "use strict";
159
168
  ${SURFACE_SCRIPT_PRELUDE}
160
169
  var PLAN = ${escapeJsValue(plan)};
@@ -303,10 +312,19 @@ ${SURFACE_SCRIPT_PRELUDE}
303
312
  });
304
313
  }
305
314
  }());`;
315
+ }
316
+ export function renderConfirmationDocument(spec) {
317
+ const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...spec.text };
318
+ const choicesParam = spec.choicesParam ?? 'overwrite';
319
+ assertChoicesParamFree(spec, { choicesParam });
320
+ const alternatives = spec.alternatives ?? [];
321
+ assertAlternativeIds(alternatives);
322
+ // Every affirmative button starts disabled and waits out the dwell; only cancel is live at once.
323
+ const affirmative = new Set(['confirm', ...alternatives.map((alternative) => alternative.id)]);
306
324
  return renderSurfaceDocument({
307
325
  title: spec.title,
308
- bodyHtml,
309
- script,
326
+ bodyHtml: confirmationBodyHtml(spec, { affirmative }),
327
+ script: confirmationScript({ plan: confirmationPlan(spec, { alternatives }), text, choicesParam, affirmative }),
310
328
  app: spec.app ?? DEFAULT_APP,
311
329
  ...(spec.lang === undefined ? {} : { lang: spec.lang }),
312
330
  ...(spec.tokens === undefined ? {} : { tokens: spec.tokens }),
@@ -8,7 +8,7 @@ import { type SurfaceTokenName } from './tokens.js';
8
8
  * be a color no caller could reskin. `var(--x, fallback)` appears only where a value is genuinely
9
9
  * optional.
10
10
  */
11
- export declare const SURFACE_BASE_CSS = "*, *::before, *::after { box-sizing: border-box; }\nhtml, body { margin: 0; padding: 0; }\nbody {\n /* Transparent, not var(--jini-mcpui-bg): this document has no page of its own to paint. It is an\n allow-scripts-only sandboxed iframe embedded directly in a host's chat transcript, and html's\n background is left at its (also transparent) initial value, so the host's own pane shows\n straight through to .mcpui-surface below rather than through a second, unrelated rectangle of\n fill color sitting between the two. --jini-mcpui-bg stays declared for a caller styling\n something else via extraCss -- nothing in this stylesheet reads it anymore. */\n background: transparent;\n color: var(--jini-mcpui-text);\n font-family: var(--jini-mcpui-font);\n font-size: 14px;\n line-height: 1.5;\n padding: 16px;\n -webkit-font-smoothing: antialiased;\n}\n.mcpui-surface {\n background: linear-gradient(135deg,\n color-mix(in srgb, var(--jini-mcpui-accent) 5%, var(--jini-mcpui-panel)) 0%,\n var(--jini-mcpui-panel) 60%);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 18%, var(--jini-mcpui-border));\n border-radius: var(--jini-mcpui-radius-md);\n box-shadow: var(--jini-mcpui-surface-shadow);\n padding: 18px 20px 16px;\n max-width: 640px;\n margin: 0 auto;\n}\n.mcpui-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }\n.mcpui-details {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: 4px 14px;\n margin: 14px 0 0;\n}\n.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; }\n.mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }\n.mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }\n.mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }\n.mcpui-outcome-row dt { flex: 1; min-width: 0; overflow-wrap: anywhere; }\n.mcpui-outcome-row dt .mcpui-hint { display: block; font-weight: 400; }\n.mcpui-outcome-row dd { flex: 1; min-width: 0; }\n/* Capped so a long status cannot squeeze the label back to one character per line; the full text\n stays in the cell's title. */\n.mcpui-outcome-row dd.mcpui-detail-status {\n flex: 0 1 auto; max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n/* The iframe has 16px body padding on each side: 352px viewport = 320px card. Stacked, the value\n has the full width, so it wraps between words instead of truncating. */\n@media (max-width: 352px) {\n .mcpui-outcome-row { flex-direction: column; gap: 2px; }\n .mcpui-outcome-row dd.mcpui-detail-status { max-width: 100%; white-space: normal; overflow-wrap: normal; }\n}\n.mcpui-code {\n margin: 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 16em;\n overflow: auto;\n font-family: var(--jini-mcpui-font-mono);\n font-size: 12.5px;\n line-height: 1.45;\n padding: 6px 8px;\n border: 1px solid var(--jini-mcpui-border);\n border-radius: var(--jini-mcpui-radius-sm);\n background: var(--jini-mcpui-bg);\n}\n.mcpui-warning {\n margin: 14px 0 0;\n padding: 9px 11px;\n border-radius: var(--jini-mcpui-radius-md);\n border: 1px solid var(--jini-mcpui-danger);\n background: var(--jini-mcpui-danger-tint);\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-fields { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; border: 0; padding: 0; }\n.mcpui-field { display: flex; flex-direction: column; gap: 4px; }\n.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }\n.mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }\n.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }\n.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }\n.mcpui-input, .mcpui-select, .mcpui-textarea {\n font: inherit;\n color: var(--jini-mcpui-text);\n background: var(--jini-mcpui-panel);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n border-radius: var(--jini-mcpui-radius-sm);\n padding: 7px 9px;\n width: 100%;\n min-width: 0;\n}\n.mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }\n.mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }\n.mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }\n.mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }\n.mcpui-choice-label { color: var(--jini-mcpui-text); font-weight: 400; }\n.mcpui-input:focus-visible, .mcpui-select:focus-visible, .mcpui-textarea:focus-visible,\n.mcpui-checkbox:focus-visible, .mcpui-button:focus-visible {\n outline: 2px solid var(--jini-mcpui-accent-ring);\n outline-offset: 1px;\n border-color: var(--jini-mcpui-accent);\n}\n.mcpui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }\n.mcpui-button {\n font: inherit;\n font-weight: 500;\n padding: 7px 15px;\n border-radius: var(--jini-mcpui-radius-pill);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n background: var(--jini-mcpui-panel);\n color: var(--jini-mcpui-text);\n cursor: pointer;\n}\n.mcpui-button:hover:not([disabled]) { background: var(--jini-mcpui-accent-tint); }\n.mcpui-button-primary {\n background: var(--jini-mcpui-accent);\n border-color: var(--jini-mcpui-accent);\n color: var(--jini-mcpui-accent-ink);\n}\n.mcpui-button-primary:hover:not([disabled]) { background: var(--jini-mcpui-accent-hover); border-color: var(--jini-mcpui-accent-hover); }\n.mcpui-button-danger {\n background: var(--jini-mcpui-danger);\n border-color: var(--jini-mcpui-danger);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }\n.mcpui-button[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }\n.mcpui-status[data-state=\"failed\"], .mcpui-status[data-state=\"invalid\"], .mcpui-status[data-state=\"expired\"] { color: var(--jini-mcpui-danger); }\n.mcpui-status[data-state=\"done\"] { color: var(--jini-mcpui-text-strong); }\n/* Neither a plain success nor a failure \u2014 e.g. an outcome surface reporting \"uploaded, not yet\n reachable\" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would\n wrongly read as \"this failed\") and --jini-mcpui-text-strong (would wrongly read as \"this is\n fully done\"); the accent token reads as \"worth a second look\" without asserting either. */\n.mcpui-status[data-state=\"partial\"] { color: var(--jini-mcpui-accent); }\n.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }";
11
+ export declare const SURFACE_BASE_CSS = "*, *::before, *::after { box-sizing: border-box; }\nhtml, body { margin: 0; padding: 0; }\nbody {\n /* Transparent, not var(--jini-mcpui-bg): this document has no page of its own to paint. It is an\n allow-scripts-only sandboxed iframe embedded directly in a host's chat transcript, and html's\n background is left at its (also transparent) initial value, so the host's own pane shows\n straight through to .mcpui-surface below rather than through a second, unrelated rectangle of\n fill color sitting between the two. --jini-mcpui-bg stays declared for a caller styling\n something else via extraCss -- nothing in this stylesheet reads it anymore. */\n background: transparent;\n color: var(--jini-mcpui-text);\n font-family: var(--jini-mcpui-font);\n font-size: 14px;\n line-height: 1.5;\n padding: 16px;\n -webkit-font-smoothing: antialiased;\n}\n.mcpui-surface {\n background: linear-gradient(135deg,\n color-mix(in srgb, var(--jini-mcpui-accent) 5%, var(--jini-mcpui-panel)) 0%,\n var(--jini-mcpui-panel) 60%);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 18%, var(--jini-mcpui-border));\n border-radius: var(--jini-mcpui-radius-md);\n box-shadow: var(--jini-mcpui-surface-shadow);\n padding: 18px 20px 16px;\n max-width: 640px;\n margin: 0 auto;\n}\n.mcpui-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }\n/* fit-content(40%), not max-content: one long label used to take the whole row and squeeze every\n value to one character per line. Labels wrap at 40% instead. */\n.mcpui-details {\n display: grid;\n grid-template-columns: fit-content(40%) minmax(0, 1fr);\n gap: 4px 14px;\n margin: 14px 0 0;\n}\n.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; overflow-wrap: anywhere; }\n.mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }\n.mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }\n.mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }\n.mcpui-outcome-row dt { flex: 1; min-width: 0; overflow-wrap: anywhere; }\n.mcpui-outcome-row dt .mcpui-hint { display: block; font-weight: 400; }\n.mcpui-outcome-row dd { flex: 1; min-width: 0; }\n/* Capped so a long status cannot squeeze the label back to one character per line; the full text\n stays in the cell's title. */\n.mcpui-outcome-row dd.mcpui-detail-status {\n flex: 0 1 auto; max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n/* Frames up to 480px: stack each label above its value so the value gets the full width.\n Outcome lists keep their own row layout. */\n@media (max-width: 480px) {\n .mcpui-details:not(.mcpui-details-outcome) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }\n .mcpui-details:not(.mcpui-details-outcome) dd { margin-bottom: 8px; }\n}\n/* The iframe has 16px body padding on each side: 352px viewport = 320px card. Stacked, the value\n has the full width, so it wraps between words instead of truncating. */\n@media (max-width: 352px) {\n .mcpui-outcome-row { flex-direction: column; gap: 2px; }\n .mcpui-outcome-row dd.mcpui-detail-status { max-width: 100%; white-space: normal; overflow-wrap: normal; }\n}\n.mcpui-code {\n margin: 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 16em;\n overflow: auto;\n font-family: var(--jini-mcpui-font-mono);\n font-size: 12.5px;\n line-height: 1.45;\n padding: 6px 8px;\n border: 1px solid var(--jini-mcpui-border);\n border-radius: var(--jini-mcpui-radius-sm);\n background: var(--jini-mcpui-bg);\n}\n.mcpui-warning {\n margin: 14px 0 0;\n padding: 9px 11px;\n border-radius: var(--jini-mcpui-radius-md);\n border: 1px solid var(--jini-mcpui-danger);\n background: var(--jini-mcpui-danger-tint);\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-fields { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; border: 0; padding: 0; }\n.mcpui-field { display: flex; flex-direction: column; gap: 4px; }\n.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }\n.mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }\n.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }\n.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }\n.mcpui-input, .mcpui-select, .mcpui-textarea {\n font: inherit;\n color: var(--jini-mcpui-text);\n background: var(--jini-mcpui-panel);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n border-radius: var(--jini-mcpui-radius-sm);\n padding: 7px 9px;\n width: 100%;\n min-width: 0;\n}\n.mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }\n.mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }\n.mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }\n.mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }\n.mcpui-choice-label { color: var(--jini-mcpui-text); font-weight: 400; }\n.mcpui-input:focus-visible, .mcpui-select:focus-visible, .mcpui-textarea:focus-visible,\n.mcpui-checkbox:focus-visible, .mcpui-button:focus-visible {\n outline: 2px solid var(--jini-mcpui-accent-ring);\n outline-offset: 1px;\n border-color: var(--jini-mcpui-accent);\n}\n.mcpui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }\n.mcpui-button {\n font: inherit;\n font-weight: 500;\n padding: 7px 15px;\n border-radius: var(--jini-mcpui-radius-pill);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n background: var(--jini-mcpui-panel);\n color: var(--jini-mcpui-text);\n cursor: pointer;\n}\n.mcpui-button:hover:not([disabled]) { background: var(--jini-mcpui-accent-tint); }\n.mcpui-button-primary {\n background: var(--jini-mcpui-accent);\n border-color: var(--jini-mcpui-accent);\n color: var(--jini-mcpui-accent-ink);\n}\n.mcpui-button-primary:hover:not([disabled]) { background: var(--jini-mcpui-accent-hover); border-color: var(--jini-mcpui-accent-hover); }\n.mcpui-button-danger {\n background: var(--jini-mcpui-danger);\n border-color: var(--jini-mcpui-danger);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }\n.mcpui-button[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }\n.mcpui-status[data-state=\"failed\"], .mcpui-status[data-state=\"invalid\"], .mcpui-status[data-state=\"expired\"] { color: var(--jini-mcpui-danger); }\n.mcpui-status[data-state=\"done\"] { color: var(--jini-mcpui-text-strong); }\n/* Neither a plain success nor a failure \u2014 e.g. an outcome surface reporting \"uploaded, not yet\n reachable\" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would\n wrongly read as \"this failed\") and --jini-mcpui-text-strong (would wrongly read as \"this is\n fully done\"); the accent token reads as \"worth a second look\" without asserting either. */\n.mcpui-status[data-state=\"partial\"] { color: var(--jini-mcpui-accent); }\n.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }";
12
12
  /**
13
13
  * The Content-Security-Policy every generated surface declares in its own `<head>`.
14
14
  *
@@ -59,13 +59,15 @@ body {
59
59
  color: var(--jini-mcpui-text-strong);
60
60
  }
61
61
  .mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }
62
+ /* fit-content(40%), not max-content: one long label used to take the whole row and squeeze every
63
+ value to one character per line. Labels wrap at 40% instead. */
62
64
  .mcpui-details {
63
65
  display: grid;
64
- grid-template-columns: max-content minmax(0, 1fr);
66
+ grid-template-columns: fit-content(40%) minmax(0, 1fr);
65
67
  gap: 4px 14px;
66
68
  margin: 14px 0 0;
67
69
  }
68
- .mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; }
70
+ .mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; overflow-wrap: anywhere; }
69
71
  .mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }
70
72
  .mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }
71
73
  .mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }
@@ -77,6 +79,12 @@ body {
77
79
  .mcpui-outcome-row dd.mcpui-detail-status {
78
80
  flex: 0 1 auto; max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
79
81
  }
82
+ /* Frames up to 480px: stack each label above its value so the value gets the full width.
83
+ Outcome lists keep their own row layout. */
84
+ @media (max-width: 480px) {
85
+ .mcpui-details:not(.mcpui-details-outcome) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
86
+ .mcpui-details:not(.mcpui-details-outcome) dd { margin-bottom: 8px; }
87
+ }
80
88
  /* The iframe has 16px body padding on each side: 352px viewport = 320px card. Stacked, the value
81
89
  has the full width, so it wraps between words instead of truncating. */
82
90
  @media (max-width: 352px) {