@jini-ai/ui 0.6.15-rc.2 → 0.6.15

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 (183) hide show
  1. package/dist/browser/useDismissOnOutsideOrEscape.js +1 -1
  2. package/dist/browser/useGlobalKeydown.js +2 -2
  3. package/dist/browser/useModalWindowDragGuard.js +1 -1
  4. package/dist/core/theme/css-name.d.ts +3 -0
  5. package/dist/core/theme/css-name.js +5 -0
  6. package/dist/core/theme/stylesheet.js +2 -1
  7. package/dist/core/theme/validation.js +15 -4
  8. package/dist/features/about/rules.js +2 -1
  9. package/dist/features/admin-widgets/components/InfoTip.hooks.js +22 -12
  10. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +1 -1
  11. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +1 -1
  12. package/dist/features/admin-widgets/components/SeeMore/SeeMore.hooks.js +2 -2
  13. package/dist/features/admin-widgets/components/SeeMore/SeeMore.js +4 -2
  14. package/dist/features/admin-widgets/components/Select/Select.hooks.js +2 -1
  15. package/dist/features/admin-widgets/components/Select/Select.js +2 -1
  16. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +4 -2
  17. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +3 -2
  18. package/dist/features/asset-grid/react/components/AssetGrid.js +8 -3
  19. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +7 -6
  20. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +8 -10
  21. package/dist/features/asset-grid/react/hooks/useRubberBandDrag.js +1 -1
  22. package/dist/features/asset-tree-browser/react/components/AssetTreeFileRow.js +6 -8
  23. package/dist/features/asset-tree-browser/react/hooks/useAssetTreeRowMenu.js +1 -1
  24. package/dist/features/asset-tree-browser/rules.js +1 -3
  25. package/dist/features/browser-chrome/rules.js +10 -8
  26. package/dist/features/charts/browser.js +2 -1
  27. package/dist/features/charts/react.js +3 -1
  28. package/dist/features/connectors/ConnectorsBrowser.js +3 -1
  29. package/dist/features/connectors/hooks/useConnectorAuthorization.js +1 -1
  30. package/dist/features/connectors/hooks/useConnectorCatalog.js +2 -2
  31. package/dist/features/connectors/rules.js +10 -14
  32. package/dist/features/execution/react/components/AgentDiagnosticRow.js +2 -0
  33. package/dist/features/execution/react/components/ByokProviderForm.js +1 -3
  34. package/dist/features/execution/react/components/LocalCliAgentSettings.js +2 -1
  35. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +6 -2
  36. package/dist/features/execution/react/hooks/useExecutionTab.js +9 -5
  37. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +3 -6
  38. package/dist/features/execution/react/hooks/useReasoningControl.js +3 -3
  39. package/dist/features/execution/rules.js +23 -18
  40. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +10 -1
  41. package/dist/features/file-dropzone/rules.js +3 -2
  42. package/dist/features/html-editor/canvas-embed-placeholders.js +2 -3
  43. package/dist/features/html-editor/css.js +7 -5
  44. package/dist/features/html-editor/editor-blocks.js +60 -6
  45. package/dist/features/html-editor/protected-elements.js +5 -3
  46. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +3 -3
  47. package/dist/features/html-editor/source-splice.js +6 -4
  48. package/dist/features/iframe-pool/react/components/IframeKeepAliveProvider.js +5 -5
  49. package/dist/features/iframe-pool/react/dom-sync.js +2 -2
  50. package/dist/features/integrations/react/components/IntegrationsTab.js +3 -1
  51. package/dist/features/integrations/rules.js +6 -4
  52. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -5
  53. package/dist/features/interactive-ui/table-data.js +1 -1
  54. package/dist/features/lexical-rich-text-editor/deserialize.js +2 -2
  55. package/dist/features/lexical-rich-text-editor/mention-parser.js +4 -4
  56. package/dist/features/lexical-rich-text-editor/react/components/RichTextInput.js +2 -1
  57. package/dist/features/lexical-rich-text-editor/react/hooks/useCaretFloatingLayerPosition.js +1 -1
  58. package/dist/features/lexical-rich-text-editor/react/hooks/useMentionColorStamping.js +1 -1
  59. package/dist/features/lexical-rich-text-editor/serialize.js +13 -10
  60. package/dist/features/list-detail-panel/react/components/ListDetailPanel.js +1 -1
  61. package/dist/features/mcp-ui/confirmation-store.js +3 -6
  62. package/dist/features/mcp-ui/surfaces/confirmation.d.ts +5 -0
  63. package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
  64. package/dist/features/mcp-ui/surfaces/document.d.ts +3 -15
  65. package/dist/features/mcp-ui/surfaces/document.js +37 -6
  66. package/dist/features/mcp-ui/surfaces/fields.js +2 -0
  67. package/dist/features/mcp-ui/surfaces/host-theme.js +2 -1
  68. package/dist/features/mcp-ui/surfaces/index.d.ts +1 -0
  69. package/dist/features/mcp-ui/surfaces/index.js +1 -0
  70. package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
  71. package/dist/features/mcp-ui/surfaces/select.js +2 -1
  72. package/dist/features/mcp-ui/surfaces/structured-details.d.ts +40 -0
  73. package/dist/features/mcp-ui/surfaces/structured-details.js +159 -0
  74. package/dist/features/mcp-ui/surfaces/text-input.js +1 -1
  75. package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
  76. package/dist/features/mcp-ui/surfaces/tool-approval.js +41 -0
  77. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +2 -1
  78. package/dist/features/memory/async-commit-guard.js +4 -1
  79. package/dist/features/memory/formatters.js +5 -1
  80. package/dist/features/memory/react/components/MemoryConnectedPanel.js +3 -1
  81. package/dist/features/memory/react/components/MemoryEntryCard.js +1 -1
  82. package/dist/features/memory/react/components/MemorySettingsPanel.js +3 -1
  83. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +12 -10
  84. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +2 -5
  85. package/dist/features/memory/react/hooks/useMemoryConnectors.rules.js +10 -9
  86. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -2
  87. package/dist/features/memory/react/hooks/useMemoryFlash.hooks.js +1 -1
  88. package/dist/features/memory/rules.js +2 -1
  89. package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -2
  90. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +3 -2
  91. package/dist/features/panel-kit/fetch-query/binding.react.js +2 -1
  92. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +13 -4
  93. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -1
  94. package/dist/features/panel-kit/hooks/use-focus-trap.hooks.js +1 -1
  95. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +14 -10
  96. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +2 -2
  97. package/dist/features/progress-card/components/ProgressCard.d.ts +1 -1
  98. package/dist/features/progress-card/reference-adapters.js +23 -13
  99. package/dist/features/progress-card/rules.d.ts +0 -1
  100. package/dist/features/progress-card/rules.js +14 -20
  101. package/dist/features/progress-card/types.d.ts +1 -1
  102. package/dist/features/project-locations/dependencies.js +6 -1
  103. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +2 -1
  104. package/dist/features/project-locations/rules.js +2 -1
  105. package/dist/features/resource-dashboard/react/components/ResourceBoardToolbar.js +8 -1
  106. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +2 -1
  107. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +3 -1
  108. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +2 -1
  109. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +1 -1
  110. package/dist/features/resource-dashboard/react/hooks/useResourceBoard.js +1 -1
  111. package/dist/features/schedule-picker/rules.js +13 -7
  112. package/dist/features/sketch-editor/constants.d.ts +0 -6
  113. package/dist/features/sketch-editor/constants.js +17 -15
  114. package/dist/features/sketch-editor/dom.js +4 -7
  115. package/dist/features/sketch-editor/react/components/SketchMainMenu.js +2 -1
  116. package/dist/features/sketch-editor/react/components/SketchSaveStateBadge.js +2 -1
  117. package/dist/features/sketch-editor/react/hooks/useSketchDomEnhancements.js +1 -1
  118. package/dist/features/sketch-editor/rules.js +13 -12
  119. package/dist/features/skills/dependencies.js +9 -4
  120. package/dist/features/skills/react/components/SkillRow.js +2 -1
  121. package/dist/features/skills/react/components/SkillsTab.js +2 -1
  122. package/dist/features/skills/rules.js +3 -3
  123. package/dist/features/source-config-list/agent-handles.js +4 -3
  124. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +3 -1
  125. package/dist/features/tab-strip/react/components/TabBar.hooks.js +4 -2
  126. package/dist/features/version-manager/react/components/VersionPreviewFrame.js +4 -1
  127. package/dist/features/version-manager/react/components/VersionSidebar.js +8 -6
  128. package/dist/features/version-manager/react/hooks/usePreviewCanvasSize.js +1 -1
  129. package/dist/features/version-manager/react/hooks/useVersionManager.js +5 -7
  130. package/dist/features/version-manager/rules.js +3 -5
  131. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +2 -1
  132. package/dist/features/viewer-shell/react/components/SegmentedToggle.js +2 -1
  133. package/dist/features/viewer-shell/react/components/ViewerShell.js +2 -1
  134. package/dist/features/viewer-shell/rules.js +8 -7
  135. package/dist/react/components/BrandLogo.js +10 -2
  136. package/dist/react/components/CustomSelect.js +1 -1
  137. package/dist/react/components/EditorIcon.js +17 -14
  138. package/dist/react/components/Loading.js +2 -1
  139. package/dist/react/components/OnboardingChipField.js +5 -1
  140. package/dist/react/components/OnboardingDropdown.js +6 -4
  141. package/dist/react/components/PaletteTweaks.js +7 -6
  142. package/dist/react/components/PillButton.js +2 -1
  143. package/dist/react/components/PopoverMenu.js +2 -1
  144. package/dist/react/components/RemixIcon.js +2 -1
  145. package/dist/react/components/SecretInput.hooks.d.ts +2 -25
  146. package/dist/react/components/SecretInput.hooks.js +2 -24
  147. package/dist/react/components/SecretInput.js +5 -4
  148. package/dist/react/components/Toast.js +4 -2
  149. package/dist/react/components/TooltipLayer.js +4 -1
  150. package/dist/react/components/WorkingDirPicker.js +5 -4
  151. package/dist/react/components/remixicon-font-recovery.js +9 -1
  152. package/dist/react/components/select-popover.js +2 -2
  153. package/dist/react/hooks/useBrandFonts.js +1 -1
  154. package/dist/react/hooks/useEdgeAutoScroll.js +1 -1
  155. package/dist/react/hooks/useInView.js +9 -9
  156. package/dist/react/hooks/useResizableSplitPane.js +12 -20
  157. package/dist/react/mcp-ui/useHostSurfaceTokens.js +1 -1
  158. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +7 -1
  159. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.controller.js +4 -2
  160. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.js +9 -5
  161. package/dist/renderers/annotation-canvas/rules.js +9 -13
  162. package/dist/renderers/annotation-canvas/theme.js +5 -2
  163. package/dist/renderers/renderers/index.js +1 -1
  164. package/dist/renderers/renderers/markdown.js +4 -6
  165. package/dist/theme/apply.js +7 -3
  166. package/dist/utils/class-names.d.ts +3 -0
  167. package/dist/utils/class-names.js +5 -0
  168. package/dist/utils/design-md.js +3 -3
  169. package/dist/utils/ime-composing.d.ts +1 -1
  170. package/dist/utils/ime-composing.js +1 -2
  171. package/dist/utils/localized-url.js +2 -11
  172. package/dist/utils/markdown-scroll-sync.js +1 -1
  173. package/dist/utils/omit-keys.d.ts +3 -0
  174. package/dist/utils/omit-keys.js +8 -0
  175. package/dist/utils/polygon-selection.js +1 -1
  176. package/dist/utils/sse.js +3 -0
  177. package/dist/utils/trim-chars.d.ts +13 -0
  178. package/dist/utils/trim-chars.js +30 -0
  179. package/dist/utils/unhandled-case.d.ts +10 -0
  180. package/dist/utils/unhandled-case.js +12 -0
  181. package/dist/utils/unit-bezier.js +1 -3
  182. package/dist/utils/zip.js +6 -10
  183. package/package.json +10 -9
@@ -1,3 +1,4 @@
1
+ import { unhandledCase } from '../../utils/unhandled-case.js';
1
2
  /** Path hint per OS — a Windows user shouldn't see a `~/...` path their
2
3
  * shell won't expand, and vice versa. */
3
4
  export function homeConfigPath(platform, posix, windows) {
@@ -36,7 +37,8 @@ export function buildCodexEnvToml(serverName, info) {
36
37
  const entries = Object.entries(info.env ?? {});
37
38
  if (entries.length === 0)
38
39
  return '';
39
- return `\n\n[mcp_servers.${serverName}.env]\n${entries.map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join('\n')}`;
40
+ const lines = entries.map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join('\n');
41
+ return `\n\n[mcp_servers.${serverName}.env]\n${lines}`;
40
42
  }
41
43
  /** The `{"mcpServers": {"<serverName>": {...}}}` shape shared by Cursor,
42
44
  * Antigravity, and Windsurf. */
@@ -186,8 +188,8 @@ export function snippetForClient(clientId, serverName, info) {
186
188
  // refactored away rather than kept behind a coverage-suppression comment.
187
189
  // TypeScript still catches a missing case if `McpClientId` grows a member
188
190
  // and only `methodLabelForClient` gets updated: with every current case
189
- // returning, `tsc` requires this switch to be exhaustive too, or the
190
- // function's declared `McpClientSnippet` return type is violated on the
191
- // "falls through" path.
191
+ // returning, `unhandledCase`'s `never` parameter makes `tsc` require this
192
+ // switch to be exhaustive too.
193
+ return unhandledCase(clientId);
192
194
  }
193
195
  //# sourceMappingURL=rules.js.map
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
3
  import { getNumericColumnKeys, sortTableRows } from '../../table-data.js';
4
4
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table.js';
5
+ /** Header glyph per column sort state; `none` marks a sortable but unsorted column. */
6
+ const SORT_GLYPHS = { ascending: '↑', descending: '↓', none: '↕' };
5
7
  function resolveTablePage({ pageSize, rowCount, requestedPage }, _options = {}) {
6
8
  const requestedSize = pageSize !== undefined && Number.isInteger(pageSize) && pageSize > 0 ? pageSize : null;
7
9
  const paging = requestedSize !== null;
@@ -20,11 +22,14 @@ export function DataTable({ columns, rows, onRowClick, sortable = true, pageSize
20
22
  const sortedRows = useMemo(() => sortTableRows({ rows, sort: activeSort, numericKeys }), [rows, activeSort, numericKeys]);
21
23
  const { paging, size, pageCount, page, start } = resolveTablePage({ pageSize, rowCount: rows.length, requestedPage });
22
24
  const visibleRows = sortedRows.slice(start, start + size);
23
- return (_jsxs("div", { className: "jini-data-table-frame", "data-table-provider": "shadcn", children: [_jsxs(Table, { className: "jini-data-table", children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: columns.map((column) => (_jsx(TableHead, { scope: "col", "data-numeric": numericKeys.has(column.key), "aria-sort": sortable ? (activeSort?.key === column.key ? activeSort.direction : 'none') : undefined, children: sortable ? (_jsxs("button", { type: "button", className: "jini-data-table-sort", onClick: () => {
24
- setSort({ key: column.key, direction: activeSort?.key === column.key && activeSort.direction === 'ascending'
25
- ? 'descending' : 'ascending' });
26
- setPage(0);
27
- }, children: [_jsx("span", { children: column.label }), _jsx("span", { "aria-hidden": "true", children: activeSort?.key === column.key ? (activeSort.direction === 'ascending' ? '↑' : '↓') : '↕' })] })) : column.label }, column.key))) }) }), _jsxs(TableBody, { children: [rows.length === 0 && _jsx(TableRow, { children: _jsx(TableCell, { className: "jini-data-table-empty", colSpan: columns.length, children: "No results." }) }), visibleRows.map(({ row, index }) => (_jsx(TableRow, { onClick: onRowClick ? () => onRowClick(row, index) : undefined, role: onRowClick ? 'button' : undefined, tabIndex: onRowClick ? 0 : undefined, onKeyDown: onRowClick ? (event) => {
25
+ return (_jsxs("div", { className: "jini-data-table-frame", "data-table-provider": "shadcn", children: [_jsxs(Table, { className: "jini-data-table", children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: columns.map((column) => {
26
+ const columnSort = activeSort?.key === column.key ? activeSort.direction : undefined;
27
+ return (_jsx(TableHead, { scope: "col", "data-numeric": numericKeys.has(column.key), "aria-sort": sortable ? (columnSort ?? 'none') : undefined, children: sortable ? (_jsxs("button", { type: "button", className: "jini-data-table-sort", onClick: () => {
28
+ setSort({ key: column.key, direction: activeSort?.key === column.key && activeSort.direction === 'ascending'
29
+ ? 'descending' : 'ascending' });
30
+ setPage(0);
31
+ }, children: [_jsx("span", { children: column.label }), _jsx("span", { "aria-hidden": "true", children: SORT_GLYPHS[columnSort ?? 'none'] })] })) : column.label }, column.key));
32
+ }) }) }), _jsxs(TableBody, { children: [rows.length === 0 && _jsx(TableRow, { children: _jsx(TableCell, { className: "jini-data-table-empty", colSpan: columns.length, children: "No results." }) }), visibleRows.map(({ row, index }) => (_jsx(TableRow, { onClick: onRowClick ? () => onRowClick(row, index) : undefined, role: onRowClick ? 'button' : undefined, tabIndex: onRowClick ? 0 : undefined, onKeyDown: onRowClick ? (event) => {
28
33
  if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) {
29
34
  event.preventDefault();
30
35
  onRowClick(row, index);
@@ -1,7 +1,7 @@
1
1
  function isNumericValue(value) {
2
2
  if (typeof value === 'number')
3
3
  return Number.isFinite(value);
4
- return typeof value === 'string' && /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim())
4
+ return typeof value === 'string' && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim())
5
5
  && Number.isFinite(Number(value));
6
6
  }
7
7
  /** Infer numeric columns once for both providers; empty cells do not turn a numeric column into text.
@@ -37,10 +37,10 @@ export function setRichTextFromPlainText(editor, text, knownMentions) {
37
37
  label: part.entity.label,
38
38
  title: part.entity.title,
39
39
  }));
40
+ continue;
40
41
  }
41
- else if (part.text) {
42
+ if (part.text)
42
43
  p.append($createTextNode(part.text));
43
- }
44
44
  }
45
45
  });
46
46
  root.append(p);
@@ -232,10 +232,10 @@ export function foldPresentMentions(text, present, known) {
232
232
  if (part.kind !== 'mention' || part.entity.kind === 'unknown')
233
233
  continue;
234
234
  const key = `${part.entity.kind}:${part.entity.id}`;
235
- if (seen.has(key))
236
- continue;
237
- seen.add(key);
238
- result.push(part.entity);
235
+ if (!seen.has(key)) {
236
+ seen.add(key);
237
+ result.push(part.entity);
238
+ }
239
239
  }
240
240
  return result;
241
241
  }
@@ -68,6 +68,7 @@ function EditorSurface(props, ref) {
68
68
  }, { discrete: true });
69
69
  },
70
70
  insertMention(insert) {
71
+ const isActiveTrigger = (trigger) => trigger.id === mentionTriggerIdRef.current;
71
72
  editor.update(() => {
72
73
  let sel = $getSelection();
73
74
  if (!$isRangeSelection(sel)) {
@@ -80,7 +81,7 @@ function EditorSurface(props, ref) {
80
81
  // The cast documents that instead of an unreachable
81
82
  // early-return branch no real document state can trigger.
82
83
  const rangeSel = sel;
83
- const mentionTrigger = triggersRef.current.find((t) => t.id === mentionTriggerIdRef.current) ??
84
+ const mentionTrigger = triggersRef.current.find(isActiveTrigger) ??
84
85
  triggersRef.current[0];
85
86
  if (mentionTrigger) {
86
87
  deleteActiveTrigger(rangeSel, buildTriggerDeletionRegex(mentionTrigger));
@@ -30,7 +30,7 @@ export function useCaretFloatingLayerPosition(caret, open, boundaryRef) {
30
30
  }, [open, caret, reposition]);
31
31
  useEffect(() => {
32
32
  if (!open)
33
- return;
33
+ return undefined;
34
34
  let raf = 0;
35
35
  const onMove = () => {
36
36
  if (raf)
@@ -23,7 +23,7 @@ export function useMentionColorStamping(resolveMentionColor) {
23
23
  const [editor] = useLexicalComposerContext();
24
24
  useEffect(() => {
25
25
  if (!resolveMentionColor)
26
- return;
26
+ return undefined;
27
27
  const stampByKey = (key) => {
28
28
  const dom = editor.getElementByKey(key);
29
29
  if (!dom)
@@ -26,17 +26,20 @@ export function serializeRichText(state) {
26
26
  function serializeParagraph(at) {
27
27
  let line = '';
28
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
- }
29
+ line += serializedChildText(child, at.mentions);
39
30
  }
40
31
  return line;
41
32
  }
33
+ /** One paragraph child's plain-text contribution; a mention also records its entity. Other node kinds add nothing. */
34
+ function serializedChildText(child, mentions) {
35
+ if ($isMentionNode(child)) {
36
+ mentions.push(child.getEntity());
37
+ return child.getToken();
38
+ }
39
+ if ($isLineBreakNode(child))
40
+ return '\n';
41
+ if ($isTextNode(child) || $isElementNode(child))
42
+ return child.getTextContent();
43
+ return '';
44
+ }
42
45
  //# sourceMappingURL=serialize.js.map
@@ -18,6 +18,6 @@ export function ListDetailPanel({ items, selectedId, onSelect, renderItem, rende
18
18
  const state = { active };
19
19
  const resolvedItemClassName = typeof itemClassName === 'function' ? itemClassName(item, state) : itemClassName;
20
20
  return (_jsx("button", { type: "button", className: join('jini-list-detail-panel-item', active && 'jini-list-detail-panel-item-active', resolvedItemClassName), "aria-pressed": active, "aria-label": getItemAriaLabel?.(item), "data-active": active, "data-testid": `${testId}-item-${item.id}`, onClick: () => onSelect(item.id), children: renderItem(item, state) }, item.id));
21
- }) }))] }), _jsx("section", { className: join('jini-list-detail-panel-detail', detailClassName), "data-testid": `${testId}-detail`, children: loading ? (_jsx("div", { className: "jini-list-detail-panel-loading-detail", role: "status", "aria-busy": "true", "aria-label": t('Loading'), "data-testid": `${testId}-loading-detail`, children: loadingDetailContent })) : selectedItem ? (renderDetail(selectedItem)) : (emptyDetailContent) })] }));
21
+ }) }))] }), _jsxs("section", { className: join('jini-list-detail-panel-detail', detailClassName), "data-testid": `${testId}-detail`, children: [loading ? (_jsx("div", { className: "jini-list-detail-panel-loading-detail", role: "status", "aria-busy": "true", "aria-label": t('Loading'), "data-testid": `${testId}-loading-detail`, children: loadingDetailContent })) : null, !loading && selectedItem ? renderDetail(selectedItem) : null, !loading && !selectedItem ? emptyDetailContent : null] })] }));
22
22
  }
23
23
  //# sourceMappingURL=ListDetailPanel.js.map
@@ -1,3 +1,4 @@
1
+ import { trimTrailingChars } from '../../utils/trim-chars.js';
1
2
  /**
2
3
  * @module features/mcp-ui/confirmation-store
3
4
  *
@@ -66,11 +67,7 @@ function bindingsMatch(minted, presented) {
66
67
  // does not — the type check below is what keeps those apart, and it is exact on purpose: a
67
68
  // binding that silently coerces is a binding that can be satisfied by a value the minter never
68
69
  // agreed to.
69
- if (typeof left !== typeof right) {
70
- matched = false;
71
- continue;
72
- }
73
- if (!constantTimeEquals(String(left), String(right)))
70
+ if (typeof left !== typeof right || !constantTimeEquals(String(left), String(right)))
74
71
  matched = false;
75
72
  }
76
73
  return matched;
@@ -89,7 +86,7 @@ function defaultRandomToken() {
89
86
  let binary = '';
90
87
  for (const byte of bytes)
91
88
  binary += String.fromCharCode(byte);
92
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
89
+ return trimTrailingChars(btoa(binary).replace(/\+/g, '-').replace(/\//g, '_'), '=');
93
90
  }
94
91
  /**
95
92
  * Creates an in-process confirmation store.
@@ -67,6 +67,11 @@ export interface ConfirmationSurfaceSpec {
67
67
  readonly description?: string;
68
68
  /** The facts being agreed about. A dialog that says "delete this?" without naming *this* is not consent. */
69
69
  readonly details?: readonly SurfaceDetail[];
70
+ /** Technical identifiers and input stay available without crowding out the consent wording. */
71
+ readonly detailDisclosure?: {
72
+ readonly label: string;
73
+ readonly details: readonly SurfaceDetail[];
74
+ };
70
75
  /**
71
76
  * Optional per-row checkboxes, rendered unchecked between the details and the warning. Ticking one
72
77
  * never confirms anything by itself — the same property the module doc opens with, extended to a
@@ -133,6 +133,7 @@ function confirmationBodyHtml(spec, required) {
133
133
  return [
134
134
  renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
135
135
  renderDetailList(spec.details ?? []),
136
+ spec.detailDisclosure === undefined ? '' : `<details class="mcpui-detail-disclosure mcpui-technical-details"><summary aria-expanded="false">${escapeHtml(spec.detailDisclosure.label)}</summary>${renderDetailList(spec.detailDisclosure.details)}</details>`,
136
137
  renderChoices(spec.choices ?? []),
137
138
  warning,
138
139
  // Confirm starts disabled; the script enables it once the dwell has run out.
@@ -1,5 +1,7 @@
1
1
  import { type BridgeScriptSpec } from './bridge.js';
2
2
  import { type SurfaceTokenName } from './tokens.js';
3
+ import { type SurfaceDetail } from './structured-details.js';
4
+ export type { SurfaceDetail } from './structured-details.js';
3
5
  /**
4
6
  * The base stylesheet, applied after the token block.
5
7
  *
@@ -62,27 +64,13 @@ export declare function renderSurfaceHeader(spec: {
62
64
  title: string;
63
65
  description?: string;
64
66
  }): string;
65
- /** One row of a surface's detail list. */
66
- export interface SurfaceDetail {
67
- readonly label: string;
68
- readonly value: string;
69
- /** Optional secondary context beneath the label, using the shared muted hint style. */
70
- readonly description?: string;
71
- /**
72
- * `code` renders the value in a monospace `<pre>` that keeps its line breaks and scrolls inside
73
- * its own box past a fixed height, so a long SQL statement or JSON argument stays readable without
74
- * stretching the dialog past its frame. `status` reserves one line for a short outcome value.
75
- * Omitted (or `text`) keeps the plain inline value.
76
- */
77
- readonly format?: 'text' | 'code' | 'status';
78
- }
79
67
  /**
80
68
  * Renders a `label: value` description list.
81
69
  *
82
70
  * @param options.layout - Outcome rows size independently and stack in narrow cards.
83
71
  * @returns The `<dl>`, or `''` for an empty list — an empty `<dl>` is announced by screen readers
84
72
  * as a list with no items, which is worse than no list.
85
- * @complexity O(n) in the rendered length; no effects or input mutation.
73
+ * @complexity O(n * depth) with previous snapshots, otherwise O(n) rendered bytes; no effects or input mutation.
86
74
  */
87
75
  export declare function renderDetailList(details: readonly SurfaceDetail[], options?: {
88
76
  layout?: 'outcome';
@@ -15,6 +15,7 @@ import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE }
15
15
  import { escapeHtml } from '../escape.js';
16
16
  import { SURFACE_BRIDGE_GLOBAL, renderBridgeScript } from './bridge.js';
17
17
  import { renderTokenBlock } from './tokens.js';
18
+ import { DETAIL_DISCLOSURE_SCRIPT, renderStructuredDetail } from './structured-details.js';
18
19
  /**
19
20
  * The base stylesheet, applied after the token block.
20
21
  *
@@ -202,6 +203,34 @@ body {
202
203
  fully done"); the accent token reads as "worth a second look" without asserting either. */
203
204
  .mcpui-status[data-state="partial"] { color: var(--jini-mcpui-accent); }
204
205
  .mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }`;
206
+ /** Structured details extend the same token system; legacy short-string documents keep their layout. */
207
+ const STRUCTURED_DETAILS_CSS = `
208
+ .mcpui-details { gap: 8px 16px; margin-top: 16px; }
209
+ .mcpui-details dt { font-weight: 400; }
210
+ .mcpui-details dd { min-width: 0; font-weight: 400; }
211
+ .mcpui-detail-group { margin: 0; padding: 0; padding-inline-start: 8px; border-inline-start: 1px solid var(--jini-mcpui-border); }
212
+ .mcpui-detail-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 8px; padding: 8px 0; }
213
+ .mcpui-detail-row:first-child { padding-top: 0; }
214
+ .mcpui-detail-row:last-child { padding-bottom: 0; }
215
+ .mcpui-detail-row:has(> dd > .mcpui-detail-group) { grid-template-columns: minmax(0, 1fr); }
216
+ .mcpui-detail-array { margin: 0; padding-inline-start: 16px; }
217
+ .mcpui-detail-array li + li { margin-top: 8px; }
218
+ .mcpui-detail-disclosure { min-width: 0; margin-top: 8px; }
219
+ .mcpui-detail-disclosure:first-child { margin-top: 0; }
220
+ .mcpui-detail-disclosure > summary { color: var(--jini-mcpui-text-muted); cursor: pointer; overflow-wrap: anywhere; }
221
+ .mcpui-detail-disclosure > summary:focus-visible { outline: 2px solid var(--jini-mcpui-accent-ring); outline-offset: 2px; }
222
+ .mcpui-detail-disclosure > .mcpui-code, .mcpui-detail-disclosure > .mcpui-detail-group,
223
+ .mcpui-detail-full { margin-top: 8px; }
224
+ .mcpui-detail-preview { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; color: var(--jini-mcpui-text); }
225
+ .mcpui-detail-long > summary { list-style: none; }
226
+ .mcpui-detail-long > summary::-webkit-details-marker { display: none; }
227
+ .mcpui-detail-less, .mcpui-detail-long[open] .mcpui-detail-more,
228
+ .mcpui-detail-long[open] .mcpui-detail-preview { display: none; }
229
+ .mcpui-detail-long[open] .mcpui-detail-less { display: inline; }
230
+ .mcpui-detail-change { display: flex; flex-direction: column; gap: 8px; }
231
+ .mcpui-detail-change > div > .mcpui-hint { display: block; }
232
+ .mcpui-detail-raw { font-size: 12px; }
233
+ .mcpui-detail-disclosure .mcpui-code { color: var(--jini-mcpui-text); }`;
205
234
  /**
206
235
  * The Content-Security-Policy every generated surface declares in its own `<head>`.
207
236
  *
@@ -229,6 +258,9 @@ export const SURFACE_CSP = "default-src 'none'; script-src 'unsafe-inline'; styl
229
258
  */
230
259
  export function renderSurfaceDocument(spec) {
231
260
  const extraCss = spec.extraCss === undefined ? '' : `\n${spec.extraCss}`;
261
+ const hasDisclosures = spec.bodyHtml.includes('<details class="mcpui-detail-disclosure');
262
+ const detailCss = hasDisclosures ? STRUCTURED_DETAILS_CSS : '';
263
+ const detailScript = hasDisclosures ? `\n${DETAIL_DISCLOSURE_SCRIPT}` : '';
232
264
  return `<!doctype html>
233
265
  <html lang="${escapeHtml(spec.lang ?? 'en')}">
234
266
  <head>
@@ -238,7 +270,7 @@ export function renderSurfaceDocument(spec) {
238
270
  <title>${escapeHtml(spec.title)}</title>
239
271
  <style>
240
272
  ${renderTokenBlock(spec.tokens)}
241
- ${SURFACE_BASE_CSS}${extraCss}
273
+ ${SURFACE_BASE_CSS}${detailCss}${extraCss}
242
274
  </style>
243
275
  </head>
244
276
  <body>
@@ -249,7 +281,7 @@ ${spec.bodyHtml}
249
281
  ${renderBridgeScript(spec.app)}
250
282
  </script>
251
283
  <script>
252
- ${spec.script}
284
+ ${spec.script}${detailScript}
253
285
  </script>
254
286
  </body>
255
287
  </html>`;
@@ -271,7 +303,7 @@ export function renderSurfaceHeader(spec) {
271
303
  * @param options.layout - Outcome rows size independently and stack in narrow cards.
272
304
  * @returns The `<dl>`, or `''` for an empty list — an empty `<dl>` is announced by screen readers
273
305
  * as a list with no items, which is worse than no list.
274
- * @complexity O(n) in the rendered length; no effects or input mutation.
306
+ * @complexity O(n * depth) with previous snapshots, otherwise O(n) rendered bytes; no effects or input mutation.
275
307
  */
276
308
  export function renderDetailList(details, options = {}) {
277
309
  if (details.length === 0)
@@ -279,10 +311,9 @@ export function renderDetailList(details, options = {}) {
279
311
  const outcome = options.layout === 'outcome';
280
312
  const rows = details
281
313
  .map((detail) => {
282
- const value = escapeHtml(detail.value);
283
- const body = detail.format === 'code' ? `<pre class="mcpui-code">${value}</pre>` : value;
314
+ const body = renderStructuredDetail({ detail });
284
315
  const description = detail.description === undefined ? '' : `<span class="mcpui-hint">${escapeHtml(detail.description)}</span>`;
285
- const valueClass = detail.format === 'status' ? ` class="mcpui-detail-status" title="${value}"` : '';
316
+ const valueClass = detail.format === 'status' && typeof detail.value === 'string' ? ` class="mcpui-detail-status" title="${escapeHtml(detail.value)}"` : '';
286
317
  const pair = `<dt>${escapeHtml(detail.label)}${description}</dt><dd${valueClass}>${body}</dd>`;
287
318
  return outcome ? ` <div class="mcpui-outcome-row">${pair}</div>` : ` ${pair}`;
288
319
  })
@@ -14,6 +14,7 @@
14
14
  * return type — a compile error at the switch itself. A `default` that threw would instead move
15
15
  * that failure to runtime, and add a branch no test could reach.
16
16
  */
17
+ import { unhandledCase } from '../../../utils/unhandled-case.js';
17
18
  import { renderCheckbox } from './checkbox.js';
18
19
  import { renderChoiceGroup } from './choice-group.js';
19
20
  import { renderSelect } from './select.js';
@@ -66,6 +67,7 @@ export function renderFieldControl(field, text) {
66
67
  case 'multi-enum':
67
68
  return renderChoiceGroup({ ...base(field), selection: 'multiple', options: field.options, ...present(field, ['value']) });
68
69
  }
70
+ return unhandledCase(field);
69
71
  }
70
72
  /** Projects fields to runtime validation data; conditional requirements also carry their guidance. */
71
73
  export function toFieldReadSpecs(fields) {
@@ -79,7 +79,8 @@ export function applyHostSurfaceTokens(html, tokens) {
79
79
  if (close === -1)
80
80
  return html;
81
81
  const at = close + '</style>'.length;
82
- const rule = `\n<style ${HOST_THEME_STYLE_ATTRIBUTE}>\n:root {\n${entries.map(([name, value]) => ` ${name}: ${value};`).join('\n')}\n}\n</style>`;
82
+ const declarations = entries.map(([name, value]) => ` ${name}: ${value};`).join('\n');
83
+ const rule = `\n<style ${HOST_THEME_STYLE_ATTRIBUTE}>\n:root {\n${declarations}\n}\n</style>`;
83
84
  return `${html.slice(0, at)}${rule}${html.slice(at)}`;
84
85
  }
85
86
  //# sourceMappingURL=host-theme.js.map
@@ -10,4 +10,5 @@ export * from './outcome.js';
10
10
  export * from './select.js';
11
11
  export * from './text-input.js';
12
12
  export * from './tokens.js';
13
+ export * from './tool-approval.js';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -10,4 +10,5 @@ export * from './outcome.js';
10
10
  export * from './select.js';
11
11
  export * from './text-input.js';
12
12
  export * from './tokens.js';
13
+ export * from './tool-approval.js';
13
14
  //# sourceMappingURL=index.js.map
@@ -44,7 +44,7 @@
44
44
  */
45
45
  import { createUIResource } from '../resource.js';
46
46
  import { escapeHtml, escapeJsValue } from '../escape.js';
47
- import { renderActions, renderDetailList, renderStatusRegion, renderSurfaceDocument, renderSurfaceHeader, SURFACE_SCRIPT_PRELUDE, SURFACE_STATUS_ELEMENT_ID, } from './document.js';
47
+ import { renderActions, renderDetailList, renderSurfaceDocument, renderSurfaceHeader, SURFACE_SCRIPT_PRELUDE, SURFACE_STATUS_ELEMENT_ID, } from './document.js';
48
48
  const DEFAULT_APP = { appName: 'jini-mcp-ui-outcome', appVersion: '1' };
49
49
  const DEFAULT_OPEN_LINK_LABEL = 'Open site';
50
50
  const OPEN_LINK_ACTION_ID = 'open-link';
@@ -17,8 +17,9 @@ import { fieldDescribedBy, fieldElementId, renderFieldLabel } from './document.j
17
17
  */
18
18
  export function renderSelect(props) {
19
19
  const id = fieldElementId(props.name);
20
+ const placeholderSelected = props.value === undefined ? 'selected ' : '';
20
21
  const placeholderOption = props.required === true
21
- ? `\n <option value="" ${props.value === undefined ? 'selected ' : ''}disabled>${escapeHtml(props.placeholder ?? '—')}</option>`
22
+ ? `\n <option value="" ${placeholderSelected}disabled>${escapeHtml(props.placeholder ?? '—')}</option>`
22
23
  : '';
23
24
  const options = props.options
24
25
  .map((option) => {
@@ -0,0 +1,40 @@
1
+ /** One fact. Values and previous snapshots must be JSON-serializable; strings remain supported. */
2
+ export interface SurfaceDetail {
3
+ readonly label: string;
4
+ readonly value: unknown;
5
+ /** Presence enables a diff, including an explicitly absent previous override. */
6
+ readonly previous?: unknown;
7
+ /** Explicit, optionally translated labels keyed by dotted object path. */
8
+ readonly labels?: Readonly<Record<string, string>>;
9
+ /** Optional secondary context beneath the label, using the shared muted hint style. */
10
+ readonly description?: string;
11
+ /**
12
+ * `code` discloses a monospace `<pre>` that keeps line breaks and scrolls inside its own box
13
+ * past a fixed height, so long SQL or JSON remains readable without stretching the dialog past
14
+ * its frame. `status` reserves one line for a short outcome value. Omitted (or `text`) permits
15
+ * value-sensitive presentation, preserving plain inline strings for short prose.
16
+ */
17
+ readonly format?: 'text' | 'code' | 'status';
18
+ }
19
+ type ValueContext = {
20
+ value: unknown;
21
+ path: string;
22
+ labels: Readonly<Record<string, string>>;
23
+ format?: SurfaceDetail['format'];
24
+ };
25
+ /** Native details owns keyboard interaction; mirror its state for explicit ARIA consumers. */
26
+ export declare const DETAIL_DISCLOSURE_SCRIPT = "document.addEventListener(\"toggle\", function (event) {\n var disclosure = event.target;\n if (!disclosure.matches || !disclosure.matches(\"details.mcpui-detail-disclosure\")) return;\n disclosure.querySelector(\"summary\").setAttribute(\"aria-expanded\", disclosure.open ? \"true\" : \"false\");\n}, true);";
27
+ /** Humanized English fallback; caller labels remain verbatim. @complexity O(k) key length. */
28
+ export declare function surfaceDetailLabel({ key, path, labels }: {
29
+ key: string;
30
+ path: string;
31
+ labels: ValueContext['labels'];
32
+ }, _options?: {}): string;
33
+ /** One reusable detail body. Raw JSON is escaped text, never a script or HTML fragment.
34
+ * @returns HTML for JSON-compatible caller data; JSON.stringify errors propagate for invalid data.
35
+ * @complexity O(n * depth) with a previous snapshot, otherwise O(n), in serialized bytes. */
36
+ export declare function renderStructuredDetail({ detail }: {
37
+ detail: SurfaceDetail;
38
+ }, _options?: {}): string;
39
+ export {};
40
+ //# sourceMappingURL=structured-details.d.ts.map
@@ -0,0 +1,159 @@
1
+ /** Shared value presentation for confirmation, form and outcome details. Never interprets HTML. */
2
+ import { escapeHtml } from '../escape.js';
3
+ /** Native details owns keyboard interaction; mirror its state for explicit ARIA consumers. */
4
+ export const DETAIL_DISCLOSURE_SCRIPT = `document.addEventListener("toggle", function (event) {
5
+ var disclosure = event.target;
6
+ if (!disclosure.matches || !disclosure.matches("details.mcpui-detail-disclosure")) return;
7
+ disclosure.querySelector("summary").setAttribute("aria-expanded", disclosure.open ? "true" : "false");
8
+ }, true);`;
9
+ /** Humanized English fallback; caller labels remain verbatim. @complexity O(k) key length. */
10
+ export function surfaceDetailLabel({ key, path, labels }, _options = {}) {
11
+ if (Object.hasOwn(labels, path))
12
+ return labels[path];
13
+ const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z])([A-Z][a-z])/g, '$1 $2').replace(/[_-]+/g, ' ').toLowerCase();
14
+ const acronyms = words.replace(/\b(url|css|html|json|api|id)\b/g, word => word.toUpperCase());
15
+ return acronyms.charAt(0).toUpperCase() + acronyms.slice(1);
16
+ }
17
+ function isRecord(value) {
18
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+ function disclosure({ summary, body, kind = '' }, _options = {}) {
21
+ return `<details class="mcpui-detail-disclosure ${kind}"><summary aria-expanded="false">${escapeHtml(summary)}</summary>${body}</details>`;
22
+ }
23
+ /** JSON scalar display; missing override values use the same empty marker as null. */
24
+ function scalarText({ value }, _options = {}) {
25
+ if (value === undefined || value === null || value === '')
26
+ return '—';
27
+ if (typeof value === 'boolean')
28
+ return value ? 'On' : 'Off';
29
+ return String(value);
30
+ }
31
+ /** Classifies content for a disclosure, preserving the original text bytes in the body. */
32
+ function codeKind({ value, path, format }, _options = {}) {
33
+ const hasDeclaration = value.split('}').slice(0, -1).some(part => {
34
+ const open = part.indexOf('{');
35
+ return open >= 0 && part.indexOf(':', open + 1) >= 0;
36
+ });
37
+ if (/css$/i.test(path) || (hasDeclaration && !/^\s*[\[{]/.test(value)))
38
+ return 'CSS';
39
+ if (/^\s*</.test(value))
40
+ return 'HTML';
41
+ if (/^\s*[\[{]/.test(value))
42
+ return 'JSON';
43
+ if (format === 'code')
44
+ return 'Code';
45
+ return /[\r\n]/.test(value) ? 'Text' : undefined;
46
+ }
47
+ /** Escapes all text once at the HTML sink. Long prose uses a three-line summary. */
48
+ function renderText({ value, path, format }, _options = {}) {
49
+ const text = escapeHtml(value);
50
+ const kind = codeKind({ value, path, format });
51
+ if (kind) {
52
+ const lines = value.split(/\r\n|\r|\n/).length;
53
+ return disclosure({ summary: `${kind} · ${lines} ${lines === 1 ? 'line' : 'lines'}`, body: `<pre class="mcpui-code">${text}</pre>`, kind: 'mcpui-detail-code' });
54
+ }
55
+ if (value.length <= 80)
56
+ return text;
57
+ return `<details class="mcpui-detail-disclosure mcpui-detail-long"><summary aria-expanded="false"><span class="mcpui-detail-preview">${text}</span><span class="mcpui-detail-more">Show more</span><span class="mcpui-detail-less">Show less</span></summary><div class="mcpui-detail-full">${text}</div></details>`;
58
+ }
59
+ /** Arrays keep scalar items compact; nested or long items use readable bullet rows. */
60
+ function renderArray({ value, ...context }, _options = {}) {
61
+ if (value.length === 0)
62
+ return '<span class="mcpui-detail-value">—</span>';
63
+ const compact = value.every(item => item === null || ['boolean', 'number', 'string'].includes(typeof item))
64
+ && value.every(item => String(item).length <= 80 && !/[\r\n<{[]/.test(String(item)));
65
+ if (compact)
66
+ return renderText({ ...context, value: value.map(item => scalarText({ value: item })).join(', ') });
67
+ const items = value.map((item, index) => {
68
+ const path = context.path ? `${context.path}.${index}` : String(index);
69
+ return `<li>${renderValue({ ...context, value: item, path })}</li>`;
70
+ });
71
+ const rest = items.length <= 5 ? '' : disclosure({ summary: `Show more (${items.length - 5} items)`, body: `<ul class="mcpui-detail-array">${items.slice(5).join('')}</ul>` });
72
+ return `<ul class="mcpui-detail-array">${items.slice(0, 5).join('')}</ul>${rest}`;
73
+ }
74
+ function objectRow({ key, context, body }, _options = {}) {
75
+ return `<div class="mcpui-detail-row"><dt>${escapeHtml(surfaceDetailLabel({ key, path: context.path, labels: context.labels }))}</dt><dd>${body}</dd></div>`;
76
+ }
77
+ /** Nested objects stay definition lists, indented once per actual group. @complexity O(n) rendered bytes. */
78
+ function renderObject({ value, ...context }, _options = {}) {
79
+ const entries = Object.entries(value);
80
+ if (entries.length === 0)
81
+ return '<span class="mcpui-detail-value">—</span>';
82
+ const rows = entries.map(([key, item]) => {
83
+ const child = { ...context, value: item, path: context.path ? `${context.path}.${key}` : key };
84
+ return objectRow({ key, context: child, body: renderValue(child) });
85
+ }).join('');
86
+ return `<dl class="mcpui-detail-group">${rows}</dl>`;
87
+ }
88
+ /** Pure recursive rendering, O(n) in output bytes and O(depth) traversal stack; no external I/O. */
89
+ function renderValue(context, _options = {}) {
90
+ const { value } = context;
91
+ if (Array.isArray(value))
92
+ return renderArray({ ...context, value });
93
+ if (isRecord(value))
94
+ return renderObject({ ...context, value });
95
+ if (typeof value === 'string' && value !== '')
96
+ return renderText({ ...context, value });
97
+ return escapeHtml(scalarText({ value }));
98
+ }
99
+ /** Object order is immaterial; array order and missing keys matter to an approval diff. */
100
+ function equalValue({ before, after }, _options = {}) {
101
+ if (Object.is(before, after))
102
+ return true;
103
+ if (Array.isArray(before) && Array.isArray(after)) {
104
+ return before.length === after.length && before.every((item, index) => equalValue({ before: item, after: after[index] }));
105
+ }
106
+ if (!isRecord(before) || !isRecord(after))
107
+ return false;
108
+ const keys = Object.keys(before);
109
+ return keys.length === Object.keys(after).length && keys.every(key => Object.hasOwn(after, key) && equalValue({ before: before[key], after: after[key] }));
110
+ }
111
+ /** A missing JSON key must never read an inherited property such as __proto__ or constructor. */
112
+ function ownValue({ record, key }, _options = {}) {
113
+ return Object.hasOwn(record, key) ? record[key] : undefined;
114
+ }
115
+ /** Presence needs its own label: null and missing otherwise both display an empty marker. */
116
+ function renderFieldDiff({ child, previous, value, key }, _options = {}) {
117
+ const body = renderDiff(child);
118
+ if (Object.hasOwn(previous, key) && Object.hasOwn(value, key))
119
+ return body;
120
+ const label = Object.hasOwn(value, key) ? 'Added' : 'Removed';
121
+ return `<span class="mcpui-hint">${label}</span>${body}`;
122
+ }
123
+ /** Partition by equality before rendering so unchanged fields never crowd out changes.
124
+ * @complexity O(n * depth) worst-case nested comparison work; O(n) markup space. No reads or mutation. */
125
+ function renderObjectDiff(context, _options = {}) {
126
+ const { previous, value } = context;
127
+ const keys = [...new Set([...Object.keys(value), ...Object.keys(previous)])];
128
+ const changed = [];
129
+ const unchanged = [];
130
+ for (const key of keys) {
131
+ const child = { ...context, previous: ownValue({ record: previous, key }), value: ownValue({ record: value, key }), path: context.path ? `${context.path}.${key}` : key };
132
+ const same = Object.hasOwn(previous, key) === Object.hasOwn(value, key) && equalValue({ before: child.previous, after: child.value });
133
+ const body = same ? renderValue(child) : renderFieldDiff({ child, previous, value, key });
134
+ (same ? unchanged : changed).push(objectRow({ key, context: child, body }));
135
+ }
136
+ const rest = unchanged.length === 0 ? '' : disclosure({ summary: `Unchanged fields (${unchanged.length})`, body: `<dl class="mcpui-detail-group">${unchanged.join('')}</dl>`, kind: 'mcpui-detail-unchanged' });
137
+ const changes = changed.length === 0 ? '' : `<dl class="mcpui-detail-group">${changed.join('')}</dl>`;
138
+ return changes + rest || '<span class="mcpui-detail-value">—</span>';
139
+ }
140
+ /** Recurse only through object pairs; atomic and array updates show explicit before/after values. */
141
+ function renderDiff(context, _options = {}) {
142
+ if (isRecord(context.previous) && isRecord(context.value))
143
+ return renderObjectDiff({ ...context, previous: context.previous, value: context.value });
144
+ return `<div class="mcpui-detail-change"><div><span class="mcpui-hint">Before</span>${renderValue({ ...context, value: context.previous })}</div><div><span class="mcpui-hint">After</span>${renderValue(context)}</div></div>`;
145
+ }
146
+ /** One reusable detail body. Raw JSON is escaped text, never a script or HTML fragment.
147
+ * @returns HTML for JSON-compatible caller data; JSON.stringify errors propagate for invalid data.
148
+ * @complexity O(n * depth) with a previous snapshot, otherwise O(n), in serialized bytes. */
149
+ export function renderStructuredDetail({ detail }, _options = {}) {
150
+ const context = { value: detail.value, path: '', labels: detail.labels ?? {}, format: detail.format };
151
+ const hasPrevious = Object.hasOwn(detail, 'previous');
152
+ const body = hasPrevious ? renderDiff({ ...context, previous: detail.previous }) : renderValue(context);
153
+ const raw = hasPrevious ? { previous: detail.previous, value: detail.value } : detail.value;
154
+ const showRaw = hasPrevious || (typeof detail.value === 'object' && detail.value !== null) || body.startsWith('<details ');
155
+ if (!showRaw)
156
+ return body;
157
+ return body + disclosure({ summary: 'View raw', body: `<pre class="mcpui-code">${escapeHtml(JSON.stringify(raw, null, 2))}</pre>`, kind: 'mcpui-detail-raw' });
158
+ }
159
+ //# sourceMappingURL=structured-details.js.map