@jini-ai/ui 0.6.15-rc.1 → 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 (195) 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/interactive-ui.css +1 -1
  53. package/dist/features/interactive-ui/providers/shadcn/button.d.ts +8 -6
  54. package/dist/features/interactive-ui/providers/shadcn/button.js +5 -6
  55. package/dist/features/interactive-ui/providers/shadcn/card.d.ts +3 -3
  56. package/dist/features/interactive-ui/providers/shadcn/card.js +3 -4
  57. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -5
  58. package/dist/features/interactive-ui/providers/shadcn/select.d.ts +3 -5
  59. package/dist/features/interactive-ui/providers/shadcn/select.js +3 -7
  60. package/dist/features/interactive-ui/providers/shadcn/table.d.ts +4 -5
  61. package/dist/features/interactive-ui/providers/shadcn/table.js +4 -9
  62. package/dist/features/interactive-ui/table-data.js +1 -1
  63. package/dist/features/lexical-rich-text-editor/deserialize.js +2 -2
  64. package/dist/features/lexical-rich-text-editor/mention-parser.js +4 -4
  65. package/dist/features/lexical-rich-text-editor/react/components/RichTextInput.js +2 -1
  66. package/dist/features/lexical-rich-text-editor/react/hooks/useCaretFloatingLayerPosition.js +1 -1
  67. package/dist/features/lexical-rich-text-editor/react/hooks/useMentionColorStamping.js +1 -1
  68. package/dist/features/lexical-rich-text-editor/serialize.js +13 -10
  69. package/dist/features/list-detail-panel/react/components/ListDetailPanel.js +1 -1
  70. package/dist/features/mcp-ui/confirmation-store.js +3 -6
  71. package/dist/features/mcp-ui/surfaces/confirmation.d.ts +5 -0
  72. package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
  73. package/dist/features/mcp-ui/surfaces/document.d.ts +3 -15
  74. package/dist/features/mcp-ui/surfaces/document.js +37 -6
  75. package/dist/features/mcp-ui/surfaces/fields.js +2 -0
  76. package/dist/features/mcp-ui/surfaces/host-theme.js +2 -1
  77. package/dist/features/mcp-ui/surfaces/index.d.ts +1 -0
  78. package/dist/features/mcp-ui/surfaces/index.js +1 -0
  79. package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
  80. package/dist/features/mcp-ui/surfaces/select.js +2 -1
  81. package/dist/features/mcp-ui/surfaces/structured-details.d.ts +40 -0
  82. package/dist/features/mcp-ui/surfaces/structured-details.js +159 -0
  83. package/dist/features/mcp-ui/surfaces/text-input.js +1 -1
  84. package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
  85. package/dist/features/mcp-ui/surfaces/tool-approval.js +41 -0
  86. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +2 -1
  87. package/dist/features/memory/async-commit-guard.js +4 -1
  88. package/dist/features/memory/formatters.js +5 -1
  89. package/dist/features/memory/react/components/MemoryConnectedPanel.js +3 -1
  90. package/dist/features/memory/react/components/MemoryEntryCard.js +1 -1
  91. package/dist/features/memory/react/components/MemorySettingsPanel.js +3 -1
  92. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +12 -10
  93. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +2 -5
  94. package/dist/features/memory/react/hooks/useMemoryConnectors.rules.js +10 -9
  95. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -2
  96. package/dist/features/memory/react/hooks/useMemoryFlash.hooks.js +1 -1
  97. package/dist/features/memory/rules.js +2 -1
  98. package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -2
  99. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +3 -2
  100. package/dist/features/panel-kit/fetch-query/binding.react.js +2 -1
  101. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +13 -4
  102. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -1
  103. package/dist/features/panel-kit/hooks/use-focus-trap.hooks.js +1 -1
  104. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +14 -10
  105. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +2 -2
  106. package/dist/features/progress-card/components/ProgressCard.d.ts +1 -1
  107. package/dist/features/progress-card/reference-adapters.js +23 -13
  108. package/dist/features/progress-card/rules.d.ts +0 -1
  109. package/dist/features/progress-card/rules.js +14 -20
  110. package/dist/features/progress-card/types.d.ts +1 -1
  111. package/dist/features/project-locations/dependencies.js +6 -1
  112. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +2 -1
  113. package/dist/features/project-locations/rules.js +2 -1
  114. package/dist/features/resource-dashboard/react/components/ResourceBoardToolbar.js +8 -1
  115. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -2
  116. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +0 -2
  117. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +0 -6
  118. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +2 -1
  119. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +3 -1
  120. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +2 -1
  121. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +1 -1
  122. package/dist/features/resource-dashboard/react/hooks/useResourceBoard.js +1 -1
  123. package/dist/features/schedule-picker/rules.js +13 -7
  124. package/dist/features/sketch-editor/constants.d.ts +0 -6
  125. package/dist/features/sketch-editor/constants.js +17 -15
  126. package/dist/features/sketch-editor/dom.js +4 -7
  127. package/dist/features/sketch-editor/react/components/SketchMainMenu.js +2 -1
  128. package/dist/features/sketch-editor/react/components/SketchSaveStateBadge.js +2 -1
  129. package/dist/features/sketch-editor/react/hooks/useSketchDomEnhancements.js +1 -1
  130. package/dist/features/sketch-editor/rules.js +13 -12
  131. package/dist/features/skills/dependencies.js +9 -4
  132. package/dist/features/skills/react/components/SkillRow.js +2 -1
  133. package/dist/features/skills/react/components/SkillsTab.js +2 -1
  134. package/dist/features/skills/rules.js +3 -3
  135. package/dist/features/source-config-list/agent-handles.js +4 -3
  136. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +3 -1
  137. package/dist/features/tab-strip/react/components/TabBar.hooks.js +4 -2
  138. package/dist/features/version-manager/react/components/VersionPreviewFrame.js +4 -1
  139. package/dist/features/version-manager/react/components/VersionSidebar.js +8 -6
  140. package/dist/features/version-manager/react/hooks/usePreviewCanvasSize.js +1 -1
  141. package/dist/features/version-manager/react/hooks/useVersionManager.js +5 -7
  142. package/dist/features/version-manager/rules.js +3 -5
  143. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +2 -1
  144. package/dist/features/viewer-shell/react/components/SegmentedToggle.js +2 -1
  145. package/dist/features/viewer-shell/react/components/ViewerShell.js +2 -1
  146. package/dist/features/viewer-shell/rules.js +8 -7
  147. package/dist/react/components/BrandLogo.js +10 -2
  148. package/dist/react/components/CustomSelect.js +1 -1
  149. package/dist/react/components/EditorIcon.js +17 -14
  150. package/dist/react/components/Loading.js +2 -1
  151. package/dist/react/components/OnboardingChipField.js +5 -1
  152. package/dist/react/components/OnboardingDropdown.js +6 -4
  153. package/dist/react/components/PaletteTweaks.js +7 -6
  154. package/dist/react/components/PillButton.js +2 -1
  155. package/dist/react/components/PopoverMenu.js +2 -1
  156. package/dist/react/components/RemixIcon.js +2 -1
  157. package/dist/react/components/SecretInput.hooks.d.ts +2 -25
  158. package/dist/react/components/SecretInput.hooks.js +2 -24
  159. package/dist/react/components/SecretInput.js +5 -4
  160. package/dist/react/components/Toast.js +4 -2
  161. package/dist/react/components/TooltipLayer.js +4 -1
  162. package/dist/react/components/WorkingDirPicker.js +5 -4
  163. package/dist/react/components/remixicon-font-recovery.js +9 -1
  164. package/dist/react/components/select-popover.js +2 -2
  165. package/dist/react/hooks/useBrandFonts.js +1 -1
  166. package/dist/react/hooks/useEdgeAutoScroll.js +1 -1
  167. package/dist/react/hooks/useInView.js +9 -9
  168. package/dist/react/hooks/useResizableSplitPane.js +12 -20
  169. package/dist/react/mcp-ui/useHostSurfaceTokens.js +1 -1
  170. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +7 -1
  171. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.controller.js +4 -2
  172. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.js +9 -5
  173. package/dist/renderers/annotation-canvas/rules.js +9 -13
  174. package/dist/renderers/annotation-canvas/theme.js +5 -2
  175. package/dist/renderers/renderers/index.js +1 -1
  176. package/dist/renderers/renderers/markdown.js +4 -6
  177. package/dist/theme/apply.js +7 -3
  178. package/dist/utils/class-names.d.ts +3 -0
  179. package/dist/utils/class-names.js +5 -0
  180. package/dist/utils/design-md.js +3 -3
  181. package/dist/utils/ime-composing.d.ts +1 -1
  182. package/dist/utils/ime-composing.js +1 -2
  183. package/dist/utils/localized-url.js +2 -11
  184. package/dist/utils/markdown-scroll-sync.js +1 -1
  185. package/dist/utils/omit-keys.d.ts +3 -0
  186. package/dist/utils/omit-keys.js +8 -0
  187. package/dist/utils/polygon-selection.js +1 -1
  188. package/dist/utils/sse.js +3 -0
  189. package/dist/utils/trim-chars.d.ts +13 -0
  190. package/dist/utils/trim-chars.js +30 -0
  191. package/dist/utils/unhandled-case.d.ts +10 -0
  192. package/dist/utils/unhandled-case.js +12 -0
  193. package/dist/utils/unit-bezier.js +1 -3
  194. package/dist/utils/zip.js +6 -10
  195. package/package.json +10 -9
@@ -1,3 +1,4 @@
1
+ import { omitKeys } from '../../utils/omit-keys.js';
1
2
  /** Restricts the catalog to a single provider tab's `match` predicate, defaulting to the first tab. */
2
3
  export function scopeConnectorsToProvider(connectors, providerTabs, selectedProviderId) {
3
4
  const tab = providerTabs.find((p) => p.id === selectedProviderId) ?? providerTabs[0];
@@ -176,8 +177,7 @@ export function applyConnectorStatuses(current, statuses) {
176
177
  const next = statuses[connector.id];
177
178
  if (!next)
178
179
  return connector;
179
- const { accountLabel: _accountLabel, lastError: _lastError, ...base } = connector;
180
- return { ...base, ...next };
180
+ return { ...omitKeys(connector, ['accountLabel', 'lastError']), ...next };
181
181
  });
182
182
  }
183
183
  export function getConnectorDisplayToolCount(connector) {
@@ -214,8 +214,7 @@ export function mergeConnectorToolPreview(current, next, append) {
214
214
  };
215
215
  if (next.toolsNextCursor !== undefined)
216
216
  return { ...merged, toolsNextCursor: next.toolsNextCursor };
217
- const { toolsNextCursor: _toolsNextCursor, ...withoutCursor } = merged;
218
- return withoutCursor;
217
+ return omitKeys(merged, ['toolsNextCursor']);
219
218
  }
220
219
  export function mergeConnectorActionResult(current, next) {
221
220
  const toolCount = next.toolCount ?? current.toolCount;
@@ -355,17 +354,14 @@ export function toolsBadgeTranslation(count) {
355
354
  return { key: '1 tool' };
356
355
  return { key: '{count} tools', vars: { count } };
357
356
  }
357
+ const STATUS_LABELS = {
358
+ available: 'Available',
359
+ connected: 'Connected',
360
+ error: 'Error',
361
+ disabled: 'Disabled',
362
+ };
358
363
  export function statusLabel(status) {
359
- switch (status) {
360
- case 'available':
361
- return 'Available';
362
- case 'connected':
363
- return 'Connected';
364
- case 'error':
365
- return 'Error';
366
- case 'disabled':
367
- return 'Disabled';
368
- }
364
+ return STATUS_LABELS[status];
369
365
  }
370
366
  export function defaultCategoryLabel(category) {
371
367
  return category;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useT } from '../../../i18n/index.js';
3
3
  import { agentDiagnosticTooltip } from '../../rules.js';
4
4
  import { Icon } from '../../../../react/components/Icon.js';
5
+ import { unhandledCase } from '../../../../utils/unhandled-case.js';
5
6
  /**
6
7
  * Maps one typed fix intent to a concrete icon button. `'setEnv'`/
7
8
  * `'clearEnv'`/`'launchOAuth'` intentionally resolve to no button — same as
@@ -31,6 +32,7 @@ function resolveAction(intent, handlers, labels) {
31
32
  case 'launchOAuth':
32
33
  return null;
33
34
  }
35
+ return unhandledCase(intent);
34
36
  }
35
37
  /**
36
38
  * Presents a single agent diagnostic as "one-line reason + fix button(s)".
@@ -133,9 +133,7 @@ function ByokConnectionResult({ connectionTest, canTestConnection }) {
133
133
  return null;
134
134
  if (connectionTest.status !== 'ok' && connectionTest.status !== 'error')
135
135
  return null;
136
- return (_jsx("div", { className: "jini-byok-test-row", children: _jsx("span", { className: `jini-byok-test-status is-${connectionTest.status}`, role: connectionTest.status === 'error' ? 'alert' : 'status', children: connectionTest.status === 'ok'
137
- ? (connectionTest.message !== undefined ? t(connectionTest.message) : t('Connection succeeded'))
138
- : t(connectionTest.message) }) }));
136
+ return (_jsx("div", { className: "jini-byok-test-row", children: _jsx("span", { className: `jini-byok-test-status is-${connectionTest.status}`, role: connectionTest.status === 'error' ? 'alert' : 'status', children: t(connectionTest.message ?? 'Connection succeeded') }) }));
139
137
  }
140
138
  /** Discovery failure, as an inline hint below the Model row. Renders nothing otherwise. */
141
139
  function ByokDiscoveryError({ modelDiscovery }) {
@@ -20,7 +20,8 @@ function LocalCliAgentModelControls({ agent, card, onModelChange, agentHandle })
20
20
  const { reasoning, source, sourceLabel, sourceHint } = card;
21
21
  const { hasModels, hasReasoning, reasoningOptions, reasoningValue, reasoningDisabled, allowCustomModel, customActive, modelChoices, selectValue, customModelInputValue, } = reasoning;
22
22
  const additionalOptions = allowCustomModel ? [{ value: CUSTOM_MODEL_SENTINEL, label: t('Custom…') }] : undefined;
23
- return (_jsxs("div", { className: "jini-agent-settings-row", children: [hasModels ? (_jsxs("div", { className: "jini-agent-settings-field jini-agent-settings-model", children: [_jsxs("div", { className: "jini-agent-settings-label", children: [_jsx("span", { className: "jini-field-label", children: t('Model') }), sourceLabel ? (_jsx("span", { className: `jini-agent-model-source ${source}`, children: sourceLabel })) : null] }), _jsx(SearchableModelSelect, { className: "jini-input jini-agent-model-select", ariaLabel: t('Model'), searchPlaceholder: t('Search models'), testId: `jini-agent-model-${agent.id}`, searchInputTestId: `jini-agent-model-search-${agent.id}`, ...(agentHandle ? { agentHandle: agentSubHandle({ base: agentHandle, action: 'model' }) } : {}), value: selectValue, models: modelChoices, additionalOptions: additionalOptions, onChange: card.selectModel }), sourceHint ? _jsx("p", { className: "jini-field-hint", children: sourceHint }) : null] })) : null, customActive ? (_jsxs("label", { className: "jini-agent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Custom model id') }), _jsx("input", { className: "jini-input jini-agent-settings-custom", type: "text", "data-testid": `jini-agent-model-custom-${agent.id}`, value: customModelInputValue, placeholder: t('e.g. my-fine-tuned-model'), spellCheck: false, onChange: (event) => onModelChange(agent.id, event.target.value.trim()), ...agentHandleProps({}, { base: agentHandle, action: 'custom-model', role: 'field', label: t('Custom model id') }) })] })) : null, hasReasoning ? (_jsxs("label", { className: "jini-agent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Reasoning effort') }), _jsx("select", { className: "jini-input jini-agent-settings-reasoning", "data-testid": `jini-agent-reasoning-${agent.id}`, value: reasoningValue, disabled: reasoningDisabled, onChange: (event) => card.selectReasoning(event.target.value), ...agentHandleProps({}, { base: agentHandle, action: 'reasoning', role: 'field', label: t('Reasoning effort') }), children: reasoningOptions.map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id))) })] })) : null] }));
23
+ const modelHandleProps = agentHandle ? { agentHandle: agentSubHandle({ base: agentHandle, action: 'model' }) } : {};
24
+ return (_jsxs("div", { className: "jini-agent-settings-row", children: [hasModels ? (_jsxs("div", { className: "jini-agent-settings-field jini-agent-settings-model", children: [_jsxs("div", { className: "jini-agent-settings-label", children: [_jsx("span", { className: "jini-field-label", children: t('Model') }), sourceLabel ? (_jsx("span", { className: `jini-agent-model-source ${source}`, children: sourceLabel })) : null] }), _jsx(SearchableModelSelect, { className: "jini-input jini-agent-model-select", ariaLabel: t('Model'), searchPlaceholder: t('Search models'), testId: `jini-agent-model-${agent.id}`, searchInputTestId: `jini-agent-model-search-${agent.id}`, ...modelHandleProps, value: selectValue, models: modelChoices, additionalOptions: additionalOptions, onChange: card.selectModel }), sourceHint ? _jsx("p", { className: "jini-field-hint", children: sourceHint }) : null] })) : null, customActive ? (_jsxs("label", { className: "jini-agent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Custom model id') }), _jsx("input", { className: "jini-input jini-agent-settings-custom", type: "text", "data-testid": `jini-agent-model-custom-${agent.id}`, value: customModelInputValue, placeholder: t('e.g. my-fine-tuned-model'), spellCheck: false, onChange: (event) => onModelChange(agent.id, event.target.value.trim()), ...agentHandleProps({}, { base: agentHandle, action: 'custom-model', role: 'field', label: t('Custom model id') }) })] })) : null, hasReasoning ? (_jsxs("label", { className: "jini-agent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Reasoning effort') }), _jsx("select", { className: "jini-input jini-agent-settings-reasoning", "data-testid": `jini-agent-reasoning-${agent.id}`, value: reasoningValue, disabled: reasoningDisabled, onChange: (event) => card.selectReasoning(event.target.value), ...agentHandleProps({}, { base: agentHandle, action: 'reasoning', role: 'field', label: t('Reasoning effort') }), children: reasoningOptions.map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id))) })] })) : null] }));
24
25
  }
25
26
  /** Probe evidence and repair actions stay together beneath the model controls. */
26
27
  function LocalCliAgentFeedback({ agent, card, onEnvChange }) {
@@ -14,8 +14,12 @@ export function useByokFieldValidation(required, optional = {}) {
14
14
  return {
15
15
  missing,
16
16
  baseUrlInvalid,
17
- isMissing: ({ field }, _options = {}) => (missing.has(field) || (field === 'baseUrl' && baseUrlInvalid)) &&
18
- (!optional.deferMissingFeedback || (touched.providerId === config.providerId && touched.fields.has(field))),
17
+ isMissing: ({ field }, _options = {}) => {
18
+ const invalid = missing.has(field) || (field === 'baseUrl' && baseUrlInvalid);
19
+ if (!invalid || !optional.deferMissingFeedback)
20
+ return invalid;
21
+ return touched.providerId === config.providerId && touched.fields.has(field);
22
+ },
19
23
  markTouched: ({ field }, _options = {}) => {
20
24
  setTouched(current => ({
21
25
  providerId: config.providerId,
@@ -48,7 +48,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
48
48
  }, []);
49
49
  const runDetection = useCallback((detect) => {
50
50
  clearTimeout(detectionTimer.current);
51
- const ticket = ++detectionTicket.current;
51
+ detectionTicket.current += 1;
52
+ const ticket = detectionTicket.current;
52
53
  const isCurrent = () => alive.current && detectionTicket.current === ticket;
53
54
  setScan({ status: 'scanning' });
54
55
  detect().then((found) => {
@@ -60,7 +61,7 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
60
61
  if (found.some(agent => agent.discoveryPending)) {
61
62
  detectionTimer.current = setTimeout(() => {
62
63
  if (isCurrent())
63
- runDetection(() => port.detectLocalAgents());
64
+ runDetection(port.detectLocalAgents.bind(port));
64
65
  }, 1000);
65
66
  }
66
67
  }, (error) => {
@@ -81,7 +82,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
81
82
  runDetection(() => rescanFn.call(port));
82
83
  }, [port, runDetection]);
83
84
  const testConnection = useCallback((config) => {
84
- const ticket = ++connectionTestTicket.current;
85
+ connectionTestTicket.current += 1;
86
+ const ticket = connectionTestTicket.current;
85
87
  const isCurrent = () => alive.current && connectionTestTicket.current === ticket;
86
88
  setConnectionTest({ status: 'testing' });
87
89
  port.testConnection(config).then((result) => {
@@ -100,7 +102,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
100
102
  const run = port.testAgent;
101
103
  if (!run)
102
104
  return;
103
- const ticket = ++agentTestTicket.current;
105
+ agentTestTicket.current += 1;
106
+ const ticket = agentTestTicket.current;
104
107
  const isCurrent = () => alive.current && agentTestTicket.current === ticket;
105
108
  setAgentTest({ status: 'testing', agentId });
106
109
  run.call(port, agentId, model).then((result) => {
@@ -128,7 +131,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
128
131
  const listModels = port.listModels;
129
132
  if (!listModels)
130
133
  return;
131
- const ticket = ++modelDiscoveryTicket.current;
134
+ modelDiscoveryTicket.current += 1;
135
+ const ticket = modelDiscoveryTicket.current;
132
136
  const isCurrent = () => alive.current && modelDiscoveryTicket.current === ticket;
133
137
  setModelDiscovery({ status: 'loading' });
134
138
  listModels.call(port, config).then((found) => {
@@ -53,14 +53,11 @@ export function useLocalCliAgentCard({ agent, config, agentTest, cliEnvFields, o
53
53
  }
54
54
  onReasoningChange(agent.id, nextValue);
55
55
  };
56
- const hasSettings = reasoning.hasModels ||
57
- reasoning.hasReasoning ||
58
- Boolean(onTest) ||
59
- result !== null ||
60
- cliEnvFieldsForAgent(cliEnvFields, agent.id).length > 0;
56
+ const hasSettings = reasoning.hasModels || reasoning.hasReasoning || Boolean(onTest) || result !== null;
57
+ const hasEnvironmentSettings = cliEnvFieldsForAgent(cliEnvFields, agent.id).length > 0;
61
58
  return {
62
59
  reasoning,
63
- hasSettings,
60
+ hasSettings: hasSettings || hasEnvironmentSettings,
64
61
  source,
65
62
  sourceLabel,
66
63
  sourceHint,
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect } from 'react';
2
2
  import { CUSTOM_MODEL_SENTINEL } from '../../constants.js';
3
3
  import { modelIdForReasoningLevel, pickerModelOptions, reasoningModelGroupFor, reasoningModelGroups, selectedAgentModel, selectedAgentReasoning, shouldShowCustomModelInput, splitReasoningModelId, } from '../../rules.js';
4
- export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCustomMode) {
4
+ export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCustomMode = false) {
5
5
  const { agent, config, explicitCustomMode = false, onReasoningChange } = reasoningControlOptions(optionsOrAgent, maybeConfig, maybeExplicitCustomMode);
6
6
  const models = agent.models ?? [];
7
7
  const hasModels = models.length > 0;
@@ -23,7 +23,7 @@ export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCu
23
23
  onReasoningChange('');
24
24
  // `reasoningOptionIdsKey` stands in for `reasoningOptions` (a fresh array every render) so this
25
25
  // only re-runs when the narrowed vocabulary actually changes, not on every unrelated re-render.
26
- // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ // Dependency list intentionally follows the lifecycle rationale above.
27
27
  }, [flatReasoningOptions.length, onReasoningChange, config.reasoningByAgentId, agent.id, reasoningOptionIdsKey]);
28
28
  const { allowCustomModel, knownModelIds, customActive } = customModelState({ agent, modelGroups, models, resolvedModel, explicitCustomMode });
29
29
  // A model-suffix agent picks a BASE model here; the effort control below supplies the rest of the
@@ -83,7 +83,7 @@ export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCu
83
83
  }
84
84
  /** The two call shapes (options object, or the older positional `agent, config, explicitCustomMode`)
85
85
  * as one options object. */
86
- function reasoningControlOptions(optionsOrAgent, maybeConfig, maybeExplicitCustomMode) {
86
+ function reasoningControlOptions(optionsOrAgent, maybeConfig, maybeExplicitCustomMode = false) {
87
87
  if ('agent' in optionsOrAgent && 'config' in optionsOrAgent)
88
88
  return optionsOrAgent;
89
89
  return {
@@ -118,10 +118,10 @@ function foreignKeyOwner(key, preset, presets) {
118
118
  const prefix = candidate.apiKeyPrefix;
119
119
  if (!prefix || candidate.custom || candidate.id === preset.id)
120
120
  continue;
121
- if (prefix.length <= bestClaim || !key.startsWith(prefix))
122
- continue;
123
- owner = candidate;
124
- bestClaim = prefix.length;
121
+ if (prefix.length > bestClaim && key.startsWith(prefix)) {
122
+ owner = candidate;
123
+ bestClaim = prefix.length;
124
+ }
125
125
  }
126
126
  return owner;
127
127
  }
@@ -328,9 +328,9 @@ export function cleanAgentVersionLabel(label, version) {
328
328
  * binary path, so the card stays one line without discarding either.
329
329
  */
330
330
  export function agentMetaLabel(agent, labels) {
331
- const title = (agent.authStatus === 'missing' || agent.authStatus === 'unknown'
332
- ? (agent.authMessage ?? agent.path ?? '')
333
- : (agent.path ?? ''));
331
+ const authUnresolved = agent.authStatus === 'missing' || agent.authStatus === 'unknown';
332
+ const pathTitle = agent.path ?? '';
333
+ const title = authUnresolved ? (agent.authMessage ?? pathTitle) : pathTitle;
334
334
  if (!agent.installed)
335
335
  return { text: labels.notInstalled, title };
336
336
  if (agent.authStatus === 'missing')
@@ -505,10 +505,11 @@ export function pickerModelOptions({ models }, _optional = {}) {
505
505
  const existing = choices.get(id);
506
506
  if (!existing) {
507
507
  choices.set(id, option);
508
+ continue;
508
509
  }
509
- else if (existing.label === id && label !== id) {
510
+ // A later duplicate only upgrades an entry whose label was just its id.
511
+ if (existing.label === id && label !== id)
510
512
  choices.set(id, { ...existing, label });
511
- }
512
513
  }
513
514
  return [...choices.values()];
514
515
  }
@@ -578,21 +579,25 @@ export function splitReasoningModelId(modelId, levelIds) {
578
579
  * has no level, so nothing is stripped, and a vendor label that happens to end in an unrelated
579
580
  * parenthetical keeps it.
580
581
  */
581
- /** `Head (Inner)` with optional surrounding whitespace; group 1 is the head, group 2 the qualifier. */
582
- const TRAILING_PARENTHETICAL = /^(.*?)\s*\(([^()]*)\)\s*$/;
583
582
  function baseLabelForLevel(label, level) {
584
583
  if (!level)
585
584
  return label;
586
- if (!TRAILING_PARENTHETICAL.test(label))
585
+ const trimmed = label.trimEnd();
586
+ if (!trimmed.endsWith(')'))
587
+ return label;
588
+ const open = trimmed.lastIndexOf('(');
589
+ if (open < 0)
590
+ return label;
591
+ const head = trimmed.slice(0, open).trimEnd();
592
+ const qualifier = trimmed.slice(open + 1, -1);
593
+ // A matched terminal group always supplies both parts; the old capture fallbacks were unreachable.
594
+ // The original head's dot did not span line breaks, and the qualifier excluded parentheses.
595
+ if (/[\r\n\u2028\u2029]/.test(head) || qualifier.includes(')'))
587
596
  return label;
588
- // Both capture groups always participate once the pattern matches, so reading them through
589
- // `replace` (which yields a plain string) replaces the old `match[n] ?? ''` fallbacks, which no
590
- // matching label could ever reach.
591
- const inner = label.replace(TRAILING_PARENTHETICAL, '$2').trim().toLowerCase();
597
+ const inner = qualifier.trim().toLowerCase();
592
598
  if (inner !== level.id.toLowerCase() && inner !== level.label.toLowerCase())
593
599
  return label;
594
- const stripped = label.replace(TRAILING_PARENTHETICAL, '$1').trim();
595
- return stripped || label;
600
+ return head.trim() || label;
596
601
  }
597
602
  /**
598
603
  * Groups an agent's model list by base model, recording for each base the effort levels it
@@ -48,7 +48,16 @@ export function ExternalMcpTab({ dependencies, fieldSpecs = MCP_SOURCE_FIELD_SPE
48
48
  // A real load failure always wins over the host's static/decorative banner
49
49
  // text — showing both would just be two banners saying overlapping things.
50
50
  const banner = list.error ?? connectionError;
51
- return (_jsxs("section", { className: "external-mcp-tab", children: [_jsxs("div", { className: "external-mcp-head", children: [_jsxs("div", { children: [_jsx("h3", { children: t('External MCP servers') }), _jsx("p", { className: "external-mcp-subtitle", children: t('Third-party tools for your coding agent.') })] }), _jsxs("button", { type: "button", className: "external-mcp-add-button", onClick: () => setFormOpen((open) => !open), "aria-expanded": formOpen, children: [_jsx(Icon, { name: "plus", size: 14 }), _jsx("span", { children: t('Add server') })] })] }), banner ? (_jsx("div", { className: "external-mcp-banner", role: "alert", children: t(banner) })) : null, formOpen ? (_jsx(SourceConfigAddForm, { fieldSpecs: fieldSpecs, values: addForm.values, validation: addForm.validation, submitAttempted: addForm.submitAttempted, submitting: addForm.submitting, submitError: addForm.submitError, onFieldChange: addForm.setField, onTrustChange: addForm.setTrust, onSubmit: () => void addForm.submit(), canTest: list.capabilities.canTest, testing: list.isPending(DRAFT_TEST_SCOPE, 'test'), ...(list.testResults[DRAFT_TEST_SCOPE] ? { testResult: list.testResults[DRAFT_TEST_SCOPE] } : {}), onTest: () => void list.test(undefined, addForm.values), addLabel: "Add server" })) : null, list.loading ? (_jsx("div", { className: "source-config-list-loading", role: "status", children: t('Loading…') })) : list.sources.length === 0 ? (_jsxs("div", { className: "external-mcp-empty", children: [_jsx("p", { className: "external-mcp-empty-title", children: t('No MCP servers configured.') }), _jsx("p", { className: "external-mcp-empty-hint", children: t('Click "Add server" to get started — pick a template or set up a custom stdio / HTTP server.') })] })) : (_jsx("div", { className: "source-config-list-items", children: list.sources.map((source) => (_jsx(SourceConfigItemCard, { source: source, fieldSpecs: fieldSpecs, ...(trustOptions ? { trustOptions } : {}), capabilities: list.capabilities, removing: list.isPending(source.id, 'remove'), refreshing: list.isPending(source.id, 'refresh'), settingTrust: list.isPending(source.id, 'trust'), testing: list.isPending(source.id, 'test'), updating: list.isPending(source.id, 'update'), onRefresh: () => void list.refresh(source.id), onRemove: () => void list.remove(source.id), onTrustChange: (trust) => void list.setTrust(source.id, trust), onTest: () => void list.test(source.id), onUpdate: (patch) => void list.update(source.id, patch), ...(list.testResults[source.id] ? { testResult: list.testResults[source.id] } : {}) }, source.id))) })), _jsx(ExternalMcpFooter, { saveStatusLabel: saveStatusLabel, configPath: configPath })] }));
51
+ const draftTestResultProps = list.testResults[DRAFT_TEST_SCOPE] ? { testResult: list.testResults[DRAFT_TEST_SCOPE] } : {};
52
+ return (_jsxs("section", { className: "external-mcp-tab", children: [_jsxs("div", { className: "external-mcp-head", children: [_jsxs("div", { children: [_jsx("h3", { children: t('External MCP servers') }), _jsx("p", { className: "external-mcp-subtitle", children: t('Third-party tools for your coding agent.') })] }), _jsxs("button", { type: "button", className: "external-mcp-add-button", onClick: () => setFormOpen((open) => !open), "aria-expanded": formOpen, children: [_jsx(Icon, { name: "plus", size: 14 }), _jsx("span", { children: t('Add server') })] })] }), banner ? (_jsx("div", { className: "external-mcp-banner", role: "alert", children: t(banner) })) : null, formOpen ? (_jsx(SourceConfigAddForm, { fieldSpecs: fieldSpecs, values: addForm.values, validation: addForm.validation, submitAttempted: addForm.submitAttempted, submitting: addForm.submitting, submitError: addForm.submitError, onFieldChange: addForm.setField, onTrustChange: addForm.setTrust, onSubmit: () => void addForm.submit(), canTest: list.capabilities.canTest, testing: list.isPending(DRAFT_TEST_SCOPE, 'test'), ...draftTestResultProps, onTest: () => void list.test(undefined, addForm.values), addLabel: "Add server" })) : null, list.loading ? (_jsx("div", { className: "source-config-list-loading", role: "status", children: t('Loading…') })) : (_jsx(ExternalMcpSources, { list: list, fieldSpecs: fieldSpecs, trustOptions: trustOptions })), _jsx(ExternalMcpFooter, { saveStatusLabel: saveStatusLabel, configPath: configPath })] }));
53
+ }
54
+ /** The configured servers, or the empty-state hint when there are none yet. */
55
+ function ExternalMcpSources({ list, fieldSpecs, trustOptions, }) {
56
+ const t = useT();
57
+ if (list.sources.length === 0) {
58
+ return (_jsxs("div", { className: "external-mcp-empty", children: [_jsx("p", { className: "external-mcp-empty-title", children: t('No MCP servers configured.') }), _jsx("p", { className: "external-mcp-empty-hint", children: t('Click "Add server" to get started — pick a template or set up a custom stdio / HTTP server.') })] }));
59
+ }
60
+ return (_jsx("div", { className: "source-config-list-items", children: list.sources.map((source) => (_jsx(SourceConfigItemCard, { source: source, fieldSpecs: fieldSpecs, ...(trustOptions ? { trustOptions } : {}), capabilities: list.capabilities, removing: list.isPending(source.id, 'remove'), refreshing: list.isPending(source.id, 'refresh'), settingTrust: list.isPending(source.id, 'trust'), testing: list.isPending(source.id, 'test'), updating: list.isPending(source.id, 'update'), onRefresh: () => void list.refresh(source.id), onRemove: () => void list.remove(source.id), onTrustChange: (trust) => void list.setTrust(source.id, trust), onTest: () => void list.test(source.id), onUpdate: (patch) => void list.update(source.id, patch), ...(list.testResults[source.id] ? { testResult: list.testResults[source.id] } : {}) }, source.id))) }));
52
61
  }
53
62
  /** Renders the save-status footer as one unit; a path alone does not show it. */
54
63
  function ExternalMcpFooter({ saveStatusLabel, configPath }) {
@@ -3,6 +3,7 @@
3
3
  * DOM-shaped value (`File`, event targets) arrives as a parameter.
4
4
  */
5
5
  import { FILE_DROPZONE_KIND_BY_EXTENSION } from './constants.js';
6
+ import { trimChars } from '../../utils/trim-chars.js';
6
7
  /** Lowercased extension (no dot), or `''` for an extensionless name. */
7
8
  export function fileDropzoneExtension(file) {
8
9
  return (/\.([a-z0-9]+)$/i.exec(file.name)?.[1] ?? '').toLowerCase();
@@ -29,7 +30,7 @@ export function fileDropzoneKind(file) {
29
30
  }
30
31
  /** Kinds renderable straight from an object URL (`blob:`). Fonts also need one (`FontFace` loads from a URL); text is read via `FileReader`/`Blob.text()` instead. */
31
32
  export function fileDropzoneNeedsObjectUrl(kind) {
32
- return kind === 'image' || kind === 'video' || kind === 'audio' || kind === 'pdf' || kind === 'html' || kind === 'font';
33
+ return ['image', 'video', 'audio', 'pdf', 'html', 'font'].includes(kind);
33
34
  }
34
35
  /** Uppercased, 4-char-capped extension for the glyph badge (e.g. `"a/b.jpeg"` -> `"JPEG"`, extensionless -> `"FILE"`). */
35
36
  export function fileDropzoneExtensionLabel(file) {
@@ -50,7 +51,7 @@ export function fileDropzoneStagingKey(file) {
50
51
  }
51
52
  /** CSS-safe, collision-resistant `@font-face` family name for a staged font at `index`. */
52
53
  export function fileDropzoneFontFamilyName(file, index) {
53
- const slug = file.name.replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 24);
54
+ const slug = trimChars(file.name.replace(/[^a-z0-9]+/gi, '-'), '-').slice(0, 24);
54
55
  return `jini-file-dropzone-font-${index}-${slug || 'font'}`;
55
56
  }
56
57
  /** Whether a selection is large enough (by count or total bytes) to warrant a loading affordance while it's staged. */
@@ -227,9 +227,8 @@ export function applyCanvasEmbedPlaceholders(body, describe) {
227
227
  if (existingCard && !canRefreshRuntimeCard({ el, card: existingCard }, {}))
228
228
  continue;
229
229
  const descriptor = describe(el);
230
- if (!descriptor)
231
- continue;
232
- updatePlaceholder({ el, doc, descriptor, existingCard }, {});
230
+ if (descriptor)
231
+ updatePlaceholder({ el, doc, descriptor, existingCard }, {});
233
232
  }
234
233
  }
235
234
  function canRefreshRuntimeCard({ el, card }, _options = {}) {
@@ -1,3 +1,4 @@
1
+ import { trimTrailingChars } from '../../utils/trim-chars.js';
1
2
  /**
2
3
  * `editor.getCss()` always returns single-line, semicolon-packed CSS — confirmed directly in
3
4
  * GrapesJS's own bundled source (`CssRule.prototype.toCSS`/`getDeclaration`): there is no
@@ -65,7 +66,7 @@ export function prettifyCss(css) {
65
66
  return false;
66
67
  };
67
68
  const openRule = (_input, _options = {}) => {
68
- out = out.replace(/[ \t]+$/, '');
69
+ out = trimTrailingChars(out, ' \t');
69
70
  out += ' {';
70
71
  depth++;
71
72
  atDeclStart = true;
@@ -77,7 +78,7 @@ export function prettifyCss(css) {
77
78
  };
78
79
  const closeRule = (_input, _options = {}) => {
79
80
  depth = Math.max(0, depth - 1);
80
- out = out.replace(/[ \t]+$/, '');
81
+ out = trimTrailingChars(out, ' \t');
81
82
  // A `}` immediately after `{` (an empty rule) needs no newline before it; otherwise close on
82
83
  // its own indented line.
83
84
  if (!out.endsWith('{'))
@@ -137,10 +138,11 @@ export function prettifyCss(css) {
137
138
  out += ' ';
138
139
  i++;
139
140
  skipWhitespace({}, {});
140
- continue;
141
141
  }
142
- out += ch;
143
- i++;
142
+ else {
143
+ out += ch;
144
+ i++;
145
+ }
144
146
  }
145
147
  return out.trim();
146
148
  }
@@ -34,11 +34,54 @@ const RUNTIME_EFFECTS = new Set([
34
34
  const TEXT_EFFECT_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'dt', 'dd', 'blockquote', 'figcaption', 'label']);
35
35
  const PHRASING_TAGS = new Set([...INLINE_TAGS, 'abbr', 'br', 'cite', 'code', 'data', 'dfn', 'img', 'kbd', 'q', 'samp', 'time', 'var', 'wbr']);
36
36
  const isPhrasingOnly = (el) => el.children.every(child => PHRASING_TAGS.has(child.tagName) && isPhrasingOnly(child));
37
+ /** Closing quotes for each suffix, honoring the same backslash escapes as effect string literals.
38
+ * A failed quoted token can contain another valid token; suffix lookup preserves that recovery
39
+ * without repeatedly scanning an unterminated string. O(n) time and temporary memory. */
40
+ function effectQuoteEnds({ text, quote }, _options = {}) {
41
+ const ends = new Int32Array(text.length + 2).fill(-1);
42
+ for (let i = text.length - 1; i >= 0; i--) {
43
+ const char = text[i];
44
+ if (char === quote)
45
+ ends[i] = i;
46
+ else if (char === '\\' && /[\r\n\u2028\u2029]/.test(text[i + 1] ?? ''))
47
+ ends[i] = -1;
48
+ else
49
+ ends[i] = ends[i + (char === '\\' ? 2 : 1)];
50
+ }
51
+ return ends;
52
+ }
53
+ /** Remove complete quoted strings and parenthesized arguments before splitting effect names.
54
+ * The first closing parenthesis wins, as in the existing effect grammar. O(n) time and memory. */
55
+ function withoutEffectArguments({ text }, _options = {}) {
56
+ const double = effectQuoteEnds({ text, quote: '"' });
57
+ const single = effectQuoteEnds({ text, quote: "'" });
58
+ const lastClose = text.lastIndexOf(')');
59
+ const parts = [];
60
+ let start = 0;
61
+ let i = 0;
62
+ while (i < text.length) {
63
+ let end = -1;
64
+ if (text[i] === '"')
65
+ end = double[i + 1];
66
+ if (text[i] === "'")
67
+ end = single[i + 1];
68
+ if (text[i] === '(' && i < lastClose)
69
+ end = text.indexOf(')', i + 1);
70
+ if (end >= 0) {
71
+ parts.push(text.slice(start, i));
72
+ i = end;
73
+ start = end + 1;
74
+ }
75
+ i++;
76
+ }
77
+ parts.push(text.slice(start));
78
+ return parts.join('');
79
+ }
37
80
  const isEditableEffect = (el) => {
38
81
  if (el.attributes['data-editor'] === 'block')
39
82
  return false;
40
83
  // Quoted selectors and timing/color functions may contain commas, which are not effect separators.
41
- const effects = (el.attributes['data-kui'] ?? '').replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\([^)]*\)/g, '')
84
+ const effects = withoutEffectArguments({ text: el.attributes['data-kui'] ?? '' })
42
85
  .split(',').map(segment => segment.trim().split(/\s+/)[0]).filter(Boolean);
43
86
  if (effects.some(effect => RUNTIME_EFFECTS.has(effect)))
44
87
  return false;
@@ -72,10 +115,16 @@ function describeBlock({ el, isLive, hasLiveDescendants }, options = {}) {
72
115
  reason = 'embed';
73
116
  return { reason, host };
74
117
  }
118
+ /** `.first.second` from an element's class attribute (first two classes), or undefined when it has none. */
119
+ function classIdentity(className) {
120
+ if (!className)
121
+ return undefined;
122
+ return `.${className.trim().split(/\s+/).slice(0, 2).join('.')}`;
123
+ }
75
124
  function createEditorBlock({ el, reason, host }, options = {}) {
76
125
  const t = options.translate ?? ((key) => key);
77
126
  const id = el.attributes.id;
78
- const identity = id ? `#${id}` : el.attributes.class ? `.${el.attributes.class.trim().split(/\s+/).slice(0, 2).join('.')}` : el.tagName;
127
+ const identity = id ? `#${id}` : classIdentity(el.attributes.class) ?? el.tagName;
79
128
  return {
80
129
  ...el.outerRange, tagName: el.tagName, ...(id ? { id } : {}), reason, element: el,
81
130
  descriptor: reason === 'embed' && host ? host : {
@@ -155,8 +204,10 @@ export function prepareEditorSource({ html }, options = {}) {
155
204
  instances.sort((a, b) => a.outerStart - b.outerStart);
156
205
  let markerAttribute = 'data-jini-raw-source-0';
157
206
  let namespace = 0;
158
- while (html.toLowerCase().includes(markerAttribute))
159
- markerAttribute = `data-jini-raw-source-${++namespace}`;
207
+ while (html.toLowerCase().includes(markerAttribute)) {
208
+ namespace += 1;
209
+ markerAttribute = `data-jini-raw-source-${namespace}`;
210
+ }
160
211
  const prepared = renderSourceShells({ html, instances, blocks: detected.blocks, markerAttribute }, {});
161
212
  return { ok: true, html: prepared, markerAttribute, instances, canvasCss: css.join('\n') };
162
213
  }
@@ -175,7 +226,8 @@ function renderSourceShells({ html, instances, blocks, markerAttribute }, _optio
175
226
  .map(name => ` ${name}="${escapeAttribute(attrs[name])}"`).join('');
176
227
  const tag = instance.nonvisual ? 'span' : instance.tagName;
177
228
  const voidTag = instance.openingEnd === instance.outerEnd;
178
- const shell = `<${tag} ${markerAttribute}="${i}"${layout}>${voidTag && !instance.nonvisual ? '' : `</${tag}>`}`;
229
+ const closingTag = voidTag && !instance.nonvisual ? '' : `</${tag}>`;
230
+ const shell = `<${tag} ${markerAttribute}="${i}"${layout}>${closingTag}`;
179
231
  prepared = prepared.slice(0, instance.outerStart) + shell + prepared.slice(instance.outerEnd);
180
232
  }
181
233
  return prepared;
@@ -186,7 +238,9 @@ export function findEditorBlockOpening({ html, target }, _options = {}) {
186
238
  let result;
187
239
  const walk = (el) => {
188
240
  if (el.tagName === target.tagName.toLowerCase() && (target.id === undefined || el.attributes.id === target.id)) {
189
- if (occurrence++ === target.occurrenceIndex && el.outerRange) {
241
+ const index = occurrence;
242
+ occurrence += 1;
243
+ if (index === target.occurrenceIndex && el.outerRange) {
190
244
  const { outerStart: start, openingEnd: end } = el.outerRange;
191
245
  result = { start, end, openingTag: html.slice(start, end) };
192
246
  }
@@ -77,10 +77,12 @@ function selectedSourceStart({ wrapper, selected, original, html }, _options = {
77
77
  let copiesBefore = 0;
78
78
  let reached = false;
79
79
  visit(wrapper, component => {
80
- if (component === selected)
80
+ if (component === selected) {
81
81
  reached = true;
82
- else if (!reached && component.is(PROTECTED_ELEMENT_TYPE) && protectedElementHtml({ component }, {}) === original)
83
- copiesBefore++;
82
+ return;
83
+ }
84
+ if (!reached && component.is(PROTECTED_ELEMENT_TYPE) && protectedElementHtml({ component }, {}) === original)
85
+ copiesBefore += 1;
84
86
  });
85
87
  let start = -1;
86
88
  for (let i = 0; i <= copiesBefore; i++)
@@ -227,7 +227,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
227
227
  useEffect(() => {
228
228
  const wrapper = wrapperRef.current;
229
229
  if (!wrapper)
230
- return;
230
+ return undefined;
231
231
  const sourcePlan = prepareEditorSource({ html }, { isProtectedElement, describeHostElement: describeEmbedPlaceholder, translate: options.translate });
232
232
  let integrityFailure;
233
233
  const reportSourceError = (error) => {
@@ -237,7 +237,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
237
237
  };
238
238
  if (!sourcePlan.ok) {
239
239
  reportSourceError(new EditorSourceError({ message: sourcePlan.problem }, {}));
240
- return;
240
+ return undefined;
241
241
  }
242
242
  const mountEl = document.createElement('div');
243
243
  mountEl.style.height = '100%';
@@ -249,7 +249,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
249
249
  catch {
250
250
  reportSourceError(new EditorSourceError());
251
251
  mountEl.remove();
252
- return;
252
+ return undefined;
253
253
  }
254
254
  const decorate = () => {
255
255
  protectTextAncestors({ editor }, {});
@@ -34,9 +34,7 @@ function recordInnerRange({ node, currentPath, out }, _options = {}) {
34
34
  function indexChildren(children, parentPath, out) {
35
35
  let elementIndex = 0;
36
36
  for (const node of children) {
37
- if (!isElement(node))
38
- continue;
39
- if (UNINDEXED_TAGS.has(node.tagName))
37
+ if (!isElement(node) || UNINDEXED_TAGS.has(node.tagName))
40
38
  continue;
41
39
  const currentPath = [...parentPath, elementIndex];
42
40
  recordInnerRange({ node, currentPath, out }, {});
@@ -134,7 +132,11 @@ export function indexSourceElements({ html }, _options = {}) {
134
132
  let slot = 0;
135
133
  const elements = nodes.filter(isElement).map(node => {
136
134
  const atom = UNINDEXED_TAGS.has(node.tagName);
137
- const path = atom ? null : [...parentPath, slot++];
135
+ let path = null;
136
+ if (!atom) {
137
+ path = [...parentPath, slot];
138
+ slot += 1;
139
+ }
138
140
  const loc = node.sourceCodeLocation;
139
141
  const children = walk({ nodes: node.childNodes, parentPath: path ?? parentPath, parentStart: loc?.startTag?.endOffset ?? parentStart, parentEnd: loc?.endTag?.startOffset ?? parentEnd }, {});
140
142
  const outerRange = sourceOuterRange({ node, html, parentStart, parentEnd }, {});
@@ -35,15 +35,15 @@ export function IframeKeepAliveProvider({ children, maxMounted = DEFAULT_MAX_MOU
35
35
  const pool = useMemo(() => ({
36
36
  attach(key, host, create) {
37
37
  let entry = entriesRef.current.get(key);
38
- if (!entry) {
39
- entry = { key, element: create(), lastUsedAt: Date.now() };
40
- entriesRef.current.set(key, entry);
41
- }
42
- else if (!activeKeysRef.current.has(key)) {
38
+ if (entry && !activeKeysRef.current.has(key)) {
43
39
  // Reattaching a parked entry — undo the hidden/inert markers
44
40
  // `release()` set, or the reused iframe would stay invisible.
45
41
  unparkIframeElement(entry.element);
46
42
  }
43
+ if (!entry) {
44
+ entry = { key, element: create(), lastUsedAt: Date.now() };
45
+ entriesRef.current.set(key, entry);
46
+ }
47
47
  entry.lastUsedAt = Date.now();
48
48
  activeKeysRef.current.add(key);
49
49
  host.appendChild(entry.element);
@@ -1,10 +1,10 @@
1
1
  export function setForwardedRef(ref, value) {
2
2
  if (typeof ref === 'function') {
3
3
  ref(value);
4
+ return;
4
5
  }
5
- else if (ref) {
6
+ if (ref)
6
7
  ref.current = value;
7
- }
8
8
  }
9
9
  export function propNameToAttributeName(name) {
10
10
  if (name === 'className')
@@ -96,7 +96,9 @@ function DeeplinkInstallRow({ deeplink, info, agentHandle }) {
96
96
  /** The generated snippet, or a loading / could-not-resolve placeholder while there is none. */
97
97
  function InstallSnippet({ resolved, loading, error }) {
98
98
  const t = useT();
99
- return (_jsx(SnippetBlock, { snippet: resolved?.snippet ?? '', language: resolved?.language ?? 'json', placeholder: loading ? t('Loading install paths…') : error ? t('Could not resolve install paths.') : '' }));
99
+ const errorPlaceholder = error ? t('Could not resolve install paths.') : '';
100
+ const placeholder = loading ? t('Loading install paths…') : errorPlaceholder;
101
+ return (_jsx(SnippetBlock, { snippet: resolved?.snippet ?? '', language: resolved?.language ?? 'json', placeholder: placeholder }));
100
102
  }
101
103
  /** Build/Node warning, shown only once install info has loaded and reports a missing prerequisite. */
102
104
  function PrerequisiteWarning({ info }) {