@jini-ai/ui 0.5.5 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +91 -2
  2. package/dist/core/theme/validation.js +40 -38
  3. package/dist/features/a2ui/renderer.js +15 -13
  4. package/dist/features/about/react/components/AboutTab.js +64 -34
  5. package/dist/features/about/rules.d.ts +2 -2
  6. package/dist/features/about/rules.js +156 -129
  7. package/dist/features/admin-widgets/components/ImagePreviewModal.d.ts +4 -3
  8. package/dist/features/admin-widgets/components/ImagePreviewModal.js +6 -10
  9. package/dist/features/admin-widgets/components/InfoTip.d.ts +3 -2
  10. package/dist/features/admin-widgets/components/InfoTip.hooks.js +2 -0
  11. package/dist/features/admin-widgets/components/InfoTip.js +34 -7
  12. package/dist/features/admin-widgets/components/InfoTip.rules.d.ts +13 -0
  13. package/dist/features/admin-widgets/components/InfoTip.rules.js +43 -10
  14. package/dist/features/admin-widgets/components/Lightbox/Lightbox.d.ts +30 -0
  15. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.d.ts +40 -0
  16. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +128 -0
  17. package/dist/features/admin-widgets/components/Lightbox/Lightbox.js +18 -0
  18. package/dist/features/admin-widgets/components/MiddleTruncate.d.ts +14 -0
  19. package/dist/features/admin-widgets/components/MiddleTruncate.js +12 -0
  20. package/dist/features/admin-widgets/components/MiddleTruncate.rules.d.ts +20 -0
  21. package/dist/features/admin-widgets/components/MiddleTruncate.rules.js +21 -0
  22. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.d.ts +18 -0
  23. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.d.ts +16 -0
  24. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +45 -0
  25. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.js +18 -0
  26. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.d.ts +32 -0
  27. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.js +26 -0
  28. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +29 -14
  29. package/dist/features/admin-widgets/index.d.ts +14 -2
  30. package/dist/features/admin-widgets/index.js +7 -1
  31. package/dist/features/admin-widgets/styles/admin-widgets.css +1 -0
  32. package/dist/features/asset-grid/react/components/AssetCard.js +7 -1
  33. package/dist/features/asset-grid/react/components/AssetGrid.d.ts +1 -1
  34. package/dist/features/asset-grid/react/components/AssetGrid.js +38 -18
  35. package/dist/features/asset-grid/react/components/useAssetGridFilters.d.ts +21 -0
  36. package/dist/features/asset-grid/react/components/useAssetGridFilters.js +19 -0
  37. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +16 -11
  38. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.d.ts +2 -0
  39. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +35 -11
  40. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.d.ts +6 -6
  41. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.js +10 -129
  42. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.d.ts +50 -0
  43. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.js +95 -0
  44. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.d.ts +56 -0
  45. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.js +80 -0
  46. package/dist/features/asset-tree-browser/rules.js +40 -22
  47. package/dist/features/browser-chrome/rules.js +74 -70
  48. package/dist/features/charts/browser.js +11 -5
  49. package/dist/features/charts/react.js +2 -2
  50. package/dist/features/charts/table.js +2 -2
  51. package/dist/features/command-palette/react/hooks/useCommandPalette.js +3 -9
  52. package/dist/features/connectors/ConnectorsBrowser.d.ts +4 -4
  53. package/dist/features/connectors/ConnectorsBrowser.js +26 -14
  54. package/dist/features/connectors/components/ConnectorCard.js +67 -38
  55. package/dist/features/connectors/components/ConnectorDetailDrawer.js +62 -31
  56. package/dist/features/connectors/components/ConnectorGrid.js +11 -6
  57. package/dist/features/connectors/hooks/useConnectorAuthorization.js +6 -0
  58. package/dist/features/connectors/hooks/useConnectorCatalog.d.ts +2 -0
  59. package/dist/features/connectors/hooks/useConnectorCatalog.js +32 -13
  60. package/dist/features/connectors/hooks/useWiredConnectorsBrowser.d.ts +3 -3
  61. package/dist/features/connectors/rules.d.ts +8 -1
  62. package/dist/features/connectors/rules.js +42 -20
  63. package/dist/features/connectors/types.d.ts +11 -0
  64. package/dist/features/execution/constants.d.ts +4 -10
  65. package/dist/features/execution/constants.js +9 -94
  66. package/dist/features/execution/react/components/ByokProviderForm.d.ts +2 -4
  67. package/dist/features/execution/react/components/ByokProviderForm.js +140 -43
  68. package/dist/features/execution/react/components/ExecutionTab.d.ts +1 -2
  69. package/dist/features/execution/react/components/ExecutionTab.js +52 -30
  70. package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -3
  71. package/dist/features/execution/react/components/LocalCliAgentList.js +14 -3
  72. package/dist/features/execution/react/components/LocalCliAgentSettings.js +14 -3
  73. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +54 -36
  74. package/dist/features/execution/react/hooks/useReasoningControl.js +114 -78
  75. package/dist/features/execution/rules.d.ts +2 -2
  76. package/dist/features/execution/rules.js +31 -15
  77. package/dist/features/execution/types.d.ts +4 -42
  78. package/dist/features/execution/types.js +0 -17
  79. package/dist/features/external-mcp/react/components/ExternalMcpTab.d.ts +6 -2
  80. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +9 -2
  81. package/dist/features/file-dropzone/react/components/FileDropzone.js +24 -25
  82. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.d.ts +2 -2
  83. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.js +35 -30
  84. package/dist/features/file-dropzone/react/components/FileDropzoneThumbnailGrid.js +30 -24
  85. package/dist/features/file-dropzone/react/components/FileDropzoneZone.d.ts +14 -0
  86. package/dist/features/file-dropzone/react/components/FileDropzoneZone.js +26 -0
  87. package/dist/features/html-editor/canvas-content-wrapper.js +13 -13
  88. package/dist/features/html-editor/canvas-embed-placeholders.js +46 -37
  89. package/dist/features/html-editor/css.js +76 -69
  90. package/dist/features/html-editor/editor-blocks.js +47 -33
  91. package/dist/features/html-editor/editor-serialization.js +6 -5
  92. package/dist/features/html-editor/protected-elements.js +39 -28
  93. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +4 -4
  94. package/dist/features/html-editor/source-splice.js +51 -30
  95. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +1 -0
  96. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +1 -0
  97. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +1 -0
  98. package/dist/features/i18n/locale.js +20 -16
  99. package/dist/features/iframe-pool/react/dom-sync.js +8 -6
  100. package/dist/features/integrations/react/components/CodexInstallToggleButton.js +19 -4
  101. package/dist/features/integrations/react/components/IntegrationsTab.js +60 -14
  102. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  103. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -7
  104. package/dist/features/lexical-rich-text-editor/mention-parser.js +60 -57
  105. package/dist/features/lexical-rich-text-editor/react/hooks/useKeyboardCommands.js +33 -38
  106. package/dist/features/lexical-rich-text-editor/rules.js +87 -74
  107. package/dist/features/lexical-rich-text-editor/serialize.js +27 -25
  108. package/dist/features/mcp-ui/secret-card.js +111 -61
  109. package/dist/features/mcp-ui/surfaces/confirmation.js +39 -21
  110. package/dist/features/mcp-ui/surfaces/document.d.ts +1 -1
  111. package/dist/features/mcp-ui/surfaces/document.js +10 -2
  112. package/dist/features/mcp-ui/surfaces/fields.js +30 -50
  113. package/dist/features/mcp-ui/surfaces/form.d.ts +6 -0
  114. package/dist/features/mcp-ui/surfaces/form.js +43 -21
  115. package/dist/features/mcp-ui/surfaces/outcome.js +30 -20
  116. package/dist/features/mcp-ui/surfaces/text-input.js +33 -21
  117. package/dist/features/mcp-ui/surfaces/tokens.d.ts +6 -1
  118. package/dist/features/mcp-ui/surfaces/tokens.js +5 -2
  119. package/dist/features/media-providers/constants.d.ts +1 -0
  120. package/dist/features/media-providers/constants.js +2 -81
  121. package/dist/features/media-providers/index.d.ts +1 -1
  122. package/dist/features/media-providers/ports.d.ts +2 -0
  123. package/dist/features/media-providers/react/components/MediaProviderCard.d.ts +22 -0
  124. package/dist/features/media-providers/react/components/MediaProviderCard.js +50 -0
  125. package/dist/features/media-providers/react/components/MediaProvidersTab.d.ts +5 -6
  126. package/dist/features/media-providers/react/components/MediaProvidersTab.js +40 -72
  127. package/dist/features/media-providers/react/components/media-providers-tab-labels.d.ts +11 -0
  128. package/dist/features/media-providers/react/components/media-providers-tab-labels.js +57 -0
  129. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.d.ts +4 -2
  130. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +37 -23
  131. package/dist/features/media-providers/rules.js +17 -10
  132. package/dist/features/media-providers/types.d.ts +4 -22
  133. package/dist/features/media-providers/types.js +3 -8
  134. package/dist/features/memory/formatters.js +127 -92
  135. package/dist/features/memory/react/components/MemoryConnectedPanel.js +92 -36
  136. package/dist/features/memory/react/components/MemoryList.js +37 -18
  137. package/dist/features/memory/react/hooks/useMemoryExtractions.store.js +42 -36
  138. package/dist/features/notifications/react/components/NotificationsTab.js +28 -28
  139. package/dist/features/observability/long-task.d.ts +0 -12
  140. package/dist/features/observability/long-task.js +15 -19
  141. package/dist/features/observability/resource-error.d.ts +0 -10
  142. package/dist/features/observability/resource-error.js +18 -13
  143. package/dist/features/observability/stuck-run.js +10 -6
  144. package/dist/features/observability/telemetry-url.d.ts +6 -0
  145. package/dist/features/observability/telemetry-url.js +16 -0
  146. package/dist/features/observability/white-screen.js +12 -24
  147. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
  148. package/dist/features/panel-kit/fetch-query/adapter.react.js +4 -2
  149. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +46 -33
  150. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +3 -1
  151. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +3 -1
  152. package/dist/features/privacy/react/components/PrivacyTab.js +24 -13
  153. package/dist/features/progress-card/components/ProgressCard.js +7 -3
  154. package/dist/features/progress-card/reference-adapters.js +59 -54
  155. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +24 -15
  156. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.d.ts +8 -0
  157. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.js +31 -23
  158. package/dist/features/resource-dashboard/react/components/ResourceBoard.js +6 -47
  159. package/dist/features/resource-dashboard/react/components/ResourceBoardView.d.ts +2 -2
  160. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -6
  161. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +15 -0
  162. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +35 -0
  163. package/dist/features/resource-dashboard/react/components/ResourceCard.js +5 -16
  164. package/dist/features/resource-dashboard/react/components/ResourceCardParts.d.ts +34 -0
  165. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +34 -0
  166. package/dist/features/resource-dashboard/react/components/ResourceRowList.js +3 -1
  167. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +2 -2
  168. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.d.ts +19 -0
  169. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +11 -0
  170. package/dist/features/resource-dashboard/react/components/ResourceRowListView.d.ts +1 -1
  171. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +4 -4
  172. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.d.ts +10 -0
  173. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.js +20 -0
  174. package/dist/features/resource-dashboard/react/components/resourceBoardProps.d.ts +48 -0
  175. package/dist/features/resource-dashboard/react/components/resourceBoardProps.js +70 -0
  176. package/dist/features/resource-dashboard/react/components/resourceRowListProps.d.ts +19 -0
  177. package/dist/features/resource-dashboard/react/components/resourceRowListProps.js +14 -0
  178. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.d.ts +16 -0
  179. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.js +13 -0
  180. package/dist/features/resource-dashboard/rules.js +2 -8
  181. package/dist/features/sketch-editor/dom.js +150 -99
  182. package/dist/features/sketch-editor/react/components/SketchEditor.js +26 -15
  183. package/dist/features/skills/react/components/SkillDraftForm.js +13 -19
  184. package/dist/features/skills/react/components/SkillDraftForm.labels.d.ts +31 -0
  185. package/dist/features/skills/react/components/SkillDraftForm.labels.js +54 -0
  186. package/dist/features/skills/react/components/SkillRow.js +38 -18
  187. package/dist/features/skills/react/components/SkillRow.labels.d.ts +7 -0
  188. package/dist/features/skills/react/components/SkillRow.labels.js +18 -0
  189. package/dist/features/skills/react/components/SkillsTab.js +24 -14
  190. package/dist/features/skills/react/components/SkillsTab.labels.d.ts +7 -0
  191. package/dist/features/skills/react/components/SkillsTab.labels.js +11 -0
  192. package/dist/features/skills/rules.js +32 -23
  193. package/dist/features/source-config-list/agent-handles.js +2 -1
  194. package/dist/features/source-config-list/dependencies.js +53 -49
  195. package/dist/features/source-config-list/index.d.ts +3 -3
  196. package/dist/features/source-config-list/react/components/SourceConfigAddForm.d.ts +6 -5
  197. package/dist/features/source-config-list/react/components/SourceConfigAddForm.js +38 -7
  198. package/dist/features/source-config-list/react/components/SourceConfigField.js +69 -25
  199. package/dist/features/source-config-list/react/components/SourceConfigItemCard.d.ts +6 -5
  200. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +76 -20
  201. package/dist/features/source-config-list/react/components/SourceConfigListView.d.ts +5 -3
  202. package/dist/features/source-config-list/react/components/SourceConfigListView.js +33 -3
  203. package/dist/features/source-config-list/react/components/SourceConfigTestControl.js +17 -1
  204. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.d.ts +11 -0
  205. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.js +12 -0
  206. package/dist/features/source-config-list/types.d.ts +14 -0
  207. package/dist/features/tab-launcher-menu/react/components/TabLauncherMenu.js +25 -1
  208. package/dist/features/tab-launcher-menu/react/hooks/useTabLauncherMenu.js +15 -16
  209. package/dist/features/tab-strip/react/components/TabBar.js +8 -1
  210. package/dist/features/tabbed-dialog/react/components/TabbedDialog.js +8 -30
  211. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.d.ts +15 -0
  212. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.js +8 -0
  213. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.d.ts +12 -0
  214. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.js +6 -0
  215. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.d.ts +27 -0
  216. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.js +12 -0
  217. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.d.ts +43 -0
  218. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.js +59 -0
  219. package/dist/features/version-manager/dependencies.d.ts +3 -1
  220. package/dist/features/version-manager/dependencies.js +3 -2
  221. package/dist/features/version-manager/react/components/VersionManagerModal.js +10 -6
  222. package/dist/features/version-manager/react/hooks/useVersionManager.js +7 -4
  223. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +103 -50
  224. package/dist/features/viewer-shell/react/components/MarkdownSplitPane.js +28 -8
  225. package/dist/features/viewer-shell/react/hooks/useCommentReorder.js +22 -16
  226. package/dist/features/viewer-shell/rules.js +19 -23
  227. package/dist/react/components/CustomSelect.d.ts +38 -1
  228. package/dist/react/components/CustomSelect.js +94 -38
  229. package/dist/react/components/OnboardingDropdown.js +20 -8
  230. package/dist/react/components/RemixIcon.stylesheet.js +3 -1
  231. package/dist/react/components/Toast.js +13 -2
  232. package/dist/react/components/TooltipLayer.js +5 -14
  233. package/dist/react/components/WorkingDirPicker.js +78 -50
  234. package/dist/react/components/remixicon-font-recovery.js +38 -30
  235. package/dist/react/components/select-popover.d.ts +8 -6
  236. package/dist/react/components/select-popover.js +33 -29
  237. package/dist/react/mcp-ui/McpUiHost.d.ts +1 -1
  238. package/dist/react/mcp-ui/McpUiHost.js +10 -11
  239. package/dist/react/mcp-ui/host-message-source.d.ts +14 -2
  240. package/dist/react/mcp-ui/host-message-source.js +15 -5
  241. package/dist/react/mcp-ui/mcp-ui-host-frame.d.ts +41 -0
  242. package/dist/react/mcp-ui/mcp-ui-host-frame.js +39 -0
  243. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +97 -69
  244. package/dist/renderers/preview-modal-shell/react/components/PreviewModalShell.js +59 -30
  245. package/dist/renderers/react/components/ArtifactView.js +9 -6
  246. package/dist/renderers/srcdoc/build.d.ts +4 -1
  247. package/dist/renderers/srcdoc/build.js +62 -48
  248. package/dist/styles/admin.css +29 -14
  249. package/dist/styles/lightbox.css +142 -0
  250. package/dist/styles/variables.css +128 -0
  251. package/dist/theme/apply.js +9 -5
  252. package/dist/utils/auto-open-file.d.ts +2 -1
  253. package/dist/utils/auto-open-file.js +69 -85
  254. package/dist/utils/markdown-scroll-sync.js +108 -87
  255. package/dist/utils/polygon-selection.js +25 -26
  256. package/dist/utils/smooth-scroll-to-top.d.ts +1 -1
  257. package/dist/utils/smooth-scroll-to-top.js +2 -32
  258. package/dist/utils/sse.js +26 -26
  259. package/dist/utils/unit-bezier.d.ts +7 -0
  260. package/dist/utils/unit-bezier.js +38 -0
  261. package/package.json +9 -12
  262. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.d.ts +0 -13
  263. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.js +0 -46
@@ -1,4 +1,4 @@
1
- import type { MediaProvidersPort } from '../../ports.js';
1
+ import type { MediaProviderCatalogPolicy, MediaProvidersPort } from '../../ports.js';
2
2
  import type { MediaProviderMap, MediaProviderOption } from '../../types.js';
3
3
  export interface MediaProvidersTabLabels {
4
4
  title?: string;
@@ -32,10 +32,9 @@ export interface MediaProvidersTabLabels {
32
32
  }
33
33
  export interface MediaProvidersTabProps {
34
34
  port: MediaProvidersPort;
35
- /** The provider catalog to render. Optional — defaults to
36
- * `DEFAULT_MEDIA_PROVIDER_CATALOG`; see `MediaProviderOption`'s doc for
37
- * the host-override convention. */
38
- catalog?: readonly MediaProviderOption[];
35
+ /** Host-owned provider catalog and its matching identity policy. */
36
+ catalog: readonly MediaProviderOption[];
37
+ catalogPolicy: MediaProviderCatalogPolicy;
39
38
  /** Host-persisted local edits from before this tab mounted. See
40
39
  * `useMediaProvidersTab`'s doc for how this feeds the first-load merge. */
41
40
  initialProviders?: MediaProviderMap;
@@ -58,5 +57,5 @@ export interface MediaProvidersTabProps {
58
57
  * decorations outside this package's boundary; see `MediaProviderOption`'s
59
58
  * doc and this tab's `ports.ts` header for the full provenance note.
60
59
  */
61
- export declare function MediaProvidersTab({ port, catalog, initialProviders, pinnedProviderIds, labels, agentHandle, }: MediaProvidersTabProps): import("react").JSX.Element;
60
+ export declare function MediaProvidersTab({ port, catalog, catalogPolicy, initialProviders, pinnedProviderIds, labels, agentHandle, }: MediaProvidersTabProps): import("react").JSX.Element;
62
61
  //# sourceMappingURL=MediaProvidersTab.d.ts.map
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { canonicalMediaProviderId } from '@jini-ai/integrations/media-providers/catalog';
3
- import { Fragment, useState } from 'react';
2
+ import { useState } from 'react';
4
3
  import { agentHandleProps, buildAgentListHandles } from '@jini-ai/agentic';
5
4
  import { useT } from '../../../i18n/index.js';
6
5
  import { Icon } from '../../../../react/components/Icon.js';
7
- import { invalidBaseUrlProviderIds, isEntryPresent, isMarkerOnlyEntry, isProviderBaseUrlInvalid, maskedKeyLabel, resolveProviderBaseUrl, sortProvidersByConfigured, } from '../../rules.js';
8
- import { DEFAULT_MEDIA_PROVIDER_CATALOG } from '../../constants.js';
6
+ import { invalidBaseUrlProviderIds, sortProvidersByConfigured } from '../../rules.js';
9
7
  import { useMediaProvidersTab } from '../hooks/useMediaProvidersTab.js';
8
+ import { MediaProviderCard } from './MediaProviderCard.js';
9
+ import { resolveMediaProvidersTabLabels } from './media-providers-tab-labels.js';
10
10
  /**
11
11
  * Per-provider credential cards: API key, base URL, and (when the catalog
12
12
  * entry advertises any) a model picker, each independently editable and
@@ -16,47 +16,17 @@ import { useMediaProvidersTab } from '../hooks/useMediaProvidersTab.js';
16
16
  * decorations outside this package's boundary; see `MediaProviderOption`'s
17
17
  * doc and this tab's `ports.ts` header for the full provenance note.
18
18
  */
19
- export function MediaProvidersTab({ port, catalog = DEFAULT_MEDIA_PROVIDER_CATALOG, initialProviders, pinnedProviderIds, labels, agentHandle, }) {
19
+ export function MediaProvidersTab({ port, catalog, catalogPolicy, initialProviders, pinnedProviderIds, labels, agentHandle, }) {
20
20
  const t = useT();
21
- const { providers, load, save, hasAnyConfigured, pendingProviderIds, updateProvider, clearProvider, saveChanges, reload } = useMediaProvidersTab({ port, initialProviders });
21
+ const { providers, load, save, hasAnyConfigured, pendingProviderIds, updateProvider, clearProvider, saveChanges, reload } = useMediaProvidersTab({ port, catalogPolicy, initialProviders });
22
+ const { canonicalMediaProviderId } = catalogPolicy;
22
23
  const [visibleApiKeys, setVisibleApiKeys] = useState(() => new Set());
23
- const title = labels?.title ?? t('Media providers');
24
- const description = labels?.description ?? t('Credentials this host uses to generate images and video.');
25
- const reloadLabel = labels?.reloadLabel ?? t('Reload');
26
- const reloadingLabel = labels?.reloadingLabel ?? t('Reloading…');
27
- const unreachableLabel = labels?.unreachableLabel ?? t('Could not reach the server. Showing local changes only.');
28
- const emptyStateLabel = labels?.emptyStateLabel ?? t('No media providers configured yet.');
29
- const apiKeyLabel = labels?.apiKeyLabel ?? t('API key');
30
- const apiKeyPlaceholder = labels?.apiKeyPlaceholder ?? t('Paste your API key');
31
- const showKeyLabel = labels?.showKeyLabel ?? t('Show');
32
- const hideKeyLabel = labels?.hideKeyLabel ?? t('Hide');
33
- const baseUrlLabel = labels?.baseUrlLabel ?? t('Base URL');
34
- const baseUrlPlaceholder = labels?.baseUrlPlaceholder ?? t('https://api.example.com');
35
- const baseUrlDefaultHintTemplate = labels?.baseUrlDefaultHintTemplate ?? t('Uses {url} by default.');
36
- const baseUrlInvalidLabel = labels?.baseUrlInvalidLabel ??
37
- t('Enter an absolute http:// or https:// URL that is not a private or internal address.');
38
- const modelLabel = labels?.modelLabel ?? t('Model');
39
- const modelPlaceholder = labels?.modelPlaceholder ?? t('Default model');
40
- const savedWithMaskTemplate = labels?.savedWithMaskTemplate ?? t('Saved ({mask})');
41
- const unsavedLabel = labels?.unsavedLabel ?? t('Unsaved');
42
- const clearLabel = labels?.clearLabel ?? t('Clear');
43
- const saveChangesLabel = labels?.saveChangesLabel ?? t('Save changes');
44
- const savingLabel = labels?.savingLabel ?? t('Saving…');
45
- const savedNoticeLabel = labels?.savedNoticeLabel ?? t('Saved.');
46
- const saveErrorLabel = labels?.saveErrorLabel ?? t('Could not save media providers.');
24
+ const resolved = resolveMediaProvidersTabLabels({ t }, { labels });
47
25
  const toggleKeyVisible = (providerId) => {
48
- setVisibleApiKeys((current) => {
49
- const next = new Set(current);
50
- if (next.has(providerId)) {
51
- next.delete(providerId);
52
- }
53
- else {
54
- next.add(providerId);
55
- }
56
- return next;
57
- });
26
+ setVisibleApiKeys((current) => toggledMember({ set: current, member: providerId }));
58
27
  };
59
- const orderedCatalog = sortProvidersByConfigured(catalog.map((entry) => ({ ...entry, id: canonicalMediaProviderId({ id: entry.id }) })), providers, pinnedProviderIds?.map((id) => canonicalMediaProviderId({ id })));
28
+ const canonicalPinnedIds = pinnedProviderIds?.map((id) => canonicalMediaProviderId({ id }));
29
+ const orderedCatalog = sortProvidersByConfigured(catalog.map((entry) => ({ ...entry, id: canonicalMediaProviderId({ id: entry.id }) })), providers, canonicalPinnedIds);
60
30
  const providerHandles = agentHandle
61
31
  ? buildAgentListHandles({ prefix: agentHandle, ids: orderedCatalog.map((option) => option.id) })
62
32
  : undefined;
@@ -64,40 +34,38 @@ export function MediaProvidersTab({ port, catalog = DEFAULT_MEDIA_PROVIDER_CATAL
64
34
  // pinned id absent from `catalog` never appears in `orderedCatalog` either (see
65
35
  // `sortProvidersByConfigured`'s own "silently skipped" doc), so this stays correct without
66
36
  // re-deriving which pinned ids actually resolved to a real card.
67
- const pinnedIdSet = new Set(pinnedProviderIds?.map((id) => canonicalMediaProviderId({ id })) ?? []);
37
+ const pinnedIdSet = new Set(canonicalPinnedIds ?? []);
68
38
  const hasPendingChanges = pendingProviderIds.size > 0;
69
39
  // Save writes every provider at once, so ONE unacceptable endpoint blocks the
70
40
  // whole button rather than being silently persisted alongside the good ones.
71
41
  const blockedByInvalidBaseUrl = invalidBaseUrlProviderIds(providers).length > 0;
72
- return (_jsxs("section", { className: "jini-settings-section jini-settings-media-providers", children: [_jsxs("div", { className: "jini-section-head", children: [_jsxs("div", { children: [_jsx("h4", { children: title }), _jsx("p", { className: "jini-hint", children: description })] }), _jsxs("button", { type: "button", className: "jini-button jini-button-ghost", onClick: reload, disabled: load.status === 'loading', ...agentHandleProps({}, { base: agentHandle, ...({ action: 'reload', role: 'button', label: reloadLabel }) }), children: [_jsx(Icon, { name: "refresh", size: 13 }), _jsx("span", { children: load.status === 'loading' ? reloadingLabel : reloadLabel })] })] }), load.status === 'unreachable' ? (_jsx("p", { className: "jini-hint jini-hint-error", role: "alert", children: unreachableLabel })) : null, !hasAnyConfigured ? _jsx("p", { className: "jini-hint", children: emptyStateLabel }) : null, _jsx("div", { className: "jini-media-provider-list", children: orderedCatalog.map((option, index) => {
73
- const entry = providers[option.id] ?? {};
74
- const clearable = isEntryPresent(entry);
75
- const saved = isMarkerOnlyEntry(entry);
76
- // Guaranteed non-null: `isMarkerOnlyEntry` only holds when a server
77
- // marker is present, and `maskedKeyLabel` always resolves a string
78
- // for exactly that case — see both functions' doc comments.
79
- const maskedLabel = saved ? maskedKeyLabel(entry) : null;
80
- const keyVisible = visibleApiKeys.has(option.id);
81
- const rawBaseUrl = entry.baseUrl ?? '';
82
- const effectiveBaseUrl = resolveProviderBaseUrl(entry, option.defaultBaseUrl);
83
- const baseUrlInvalid = isProviderBaseUrlInvalid(entry);
84
- const modelListId = `jini-media-provider-models-${option.id}`;
85
- // A divider renders once, directly above the first UNpinned card — i.e. exactly at the
86
- // pinned/rest boundary `sortProvidersByConfigured` produced. Never renders above index 0
87
- // (nothing pinned, or the pinned card IS index 0 with no boundary yet) and never when
88
- // every card is pinned (no "rest" to separate from).
89
- const previousOption = index > 0 ? orderedCatalog[index - 1] : undefined;
90
- const showDividerBefore = !pinnedIdSet.has(option.id) && previousOption !== undefined && pinnedIdSet.has(previousOption.id);
91
- const providerHandle = providerHandles?.[index];
92
- return (_jsxs(Fragment, { children: [showDividerBefore ? _jsx("hr", { className: "jini-media-provider-divider" }) : null, _jsxs("div", { className: "jini-media-provider-card", children: [_jsxs("div", { className: "jini-media-provider-card-head", children: [_jsx("strong", { children: option.label }), saved ? (
93
- // `maskedLabel` is guaranteed non-null here — see its
94
- // definition above — so no fallback is reachable to test.
95
- _jsx("span", { className: "jini-field-status-badge jini-field-status-badge-success", children: t(savedWithMaskTemplate, { mask: maskedLabel }) })) : null, !saved && pendingProviderIds.has(option.id) ? _jsx("span", { className: "jini-field-status-badge", children: unsavedLabel }) : null] }), _jsxs("div", { className: "jini-media-provider-fields", children: [_jsxs("label", { className: "jini-media-provider-field jini-media-provider-field--secret", children: [_jsx("span", { className: "jini-field-label jini-sr-only", children: apiKeyLabel }), _jsx("input", { className: "jini-input", type: keyVisible ? 'text' : 'password',
96
- /* `new-password`, NOT `"off"` — Chrome deliberately ignores `off` on
97
- credential-shaped fields, which is how a saved password gets autofilled into
98
- an API-key box. Same defect, same fix, same reasoning as
99
- `ByokProviderForm`'s own API-key input; see its comment. */
100
- autoComplete: "new-password", spellCheck: false, placeholder: saved ? maskedLabel : apiKeyPlaceholder, "aria-label": `${option.label} ${apiKeyLabel}`, value: entry.apiKey ?? '', onChange: (event) => updateProvider(option.id, { apiKey: event.target.value }), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'api-key', role: 'field', label: `${option.label} ${apiKeyLabel}` }) }) }), _jsx("button", { type: "button", className: "jini-input-affix-btn", "aria-pressed": keyVisible, "aria-label": `${option.label} ${keyVisible ? hideKeyLabel : showKeyLabel}`, onClick: () => toggleKeyVisible(option.id), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'reveal-key', role: 'button', label: `${option.label} ${showKeyLabel}` }) }), children: _jsx(Icon, { name: keyVisible ? 'eye-off' : 'eye', size: 14 }) })] }), _jsxs("label", { className: "jini-media-provider-field", children: [_jsx("span", { className: "jini-field-label jini-sr-only", children: baseUrlLabel }), _jsx("input", { className: "jini-input", type: "url", inputMode: "url", spellCheck: false, placeholder: option.defaultBaseUrl || baseUrlPlaceholder, "aria-label": `${option.label} ${baseUrlLabel}`, value: rawBaseUrl, "aria-invalid": baseUrlInvalid || undefined, onChange: (event) => updateProvider(option.id, { baseUrl: event.target.value }), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'base-url', role: 'field', label: `${option.label} ${baseUrlLabel}` }) }) })] }), option.models && option.models.length > 0 ? (_jsxs("label", { className: "jini-media-provider-field", children: [_jsx("span", { className: "jini-field-label jini-sr-only", children: modelLabel }), _jsx("input", { className: "jini-input", list: modelListId, spellCheck: false, placeholder: modelPlaceholder, "aria-label": `${option.label} ${modelLabel}`, value: entry.model ?? '', onChange: (event) => updateProvider(option.id, { model: event.target.value }), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'model', role: 'field', label: `${option.label} ${modelLabel}` }) }) }), _jsx("datalist", { id: modelListId, children: option.models.map((model) => (_jsx("option", { value: model }, model))) })] })) : null, _jsx("button", { type: "button", className: "jini-button jini-button-ghost", disabled: !clearable, "aria-label": `${option.label} ${clearLabel}`, onClick: () => clearProvider(option.id), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'clear', role: 'button', label: `${option.label} ${clearLabel}` }) }), children: clearLabel })] }), baseUrlInvalid ? (_jsx("span", { className: "jini-field-hint jini-hint-error", role: "alert", children: baseUrlInvalidLabel })) : !rawBaseUrl.trim() && effectiveBaseUrl ? (_jsx("span", { className: "jini-field-hint", children: t(baseUrlDefaultHintTemplate, { url: effectiveBaseUrl }) })) : null] })] }, option.id));
101
- }) }), _jsxs("div", { className: "jini-media-provider-save-row", children: [_jsx("button", { type: "button", className: "jini-button", onClick: saveChanges, disabled: save.status === 'saving' || !hasPendingChanges || blockedByInvalidBaseUrl, ...agentHandleProps({}, { base: agentHandle, ...({ action: 'save', role: 'button', label: saveChangesLabel }) }), children: save.status === 'saving' ? savingLabel : saveChangesLabel }), save.status === 'saved' ? (_jsx("span", { className: "jini-hint", role: "status", children: savedNoticeLabel })) : null, save.status === 'save-error' ? (_jsx("span", { className: "jini-hint jini-hint-error", role: "alert", children: saveErrorLabel })) : null] })] }));
42
+ return (_jsxs("section", { className: "jini-settings-section jini-settings-media-providers", children: [_jsx(TabHead, { labels: resolved, loading: load.status === 'loading', onReload: reload, agentHandle: agentHandle }), load.status === 'unreachable' ? (_jsx("p", { className: "jini-hint jini-hint-error", role: "alert", children: resolved.unreachableLabel })) : null, !hasAnyConfigured ? _jsx("p", { className: "jini-hint", children: resolved.emptyStateLabel }) : null, _jsx("div", { className: "jini-media-provider-list", children: orderedCatalog.map((option, index) => (_jsx(MediaProviderCard, { option: option, entry: providers[option.id] ?? {}, labels: resolved, t: t, pending: pendingProviderIds.has(option.id), keyVisible: visibleApiKeys.has(option.id), showDividerBefore: isDividerBefore({ option, previousOption: orderedCatalog[index - 1], pinnedIdSet }), providerHandle: providerHandles?.[index], onUpdate: (patch) => updateProvider(option.id, patch), onClear: () => clearProvider(option.id), onToggleKeyVisible: () => toggleKeyVisible(option.id) }, option.id))) }), _jsx(SaveRow, { labels: resolved, save: save, disabled: !hasPendingChanges || blockedByInvalidBaseUrl, onSave: saveChanges, agentHandle: agentHandle })] }));
43
+ }
44
+ /** A copy of `set` with `member` added if absent, removed if present. */
45
+ function toggledMember({ set, member }) {
46
+ const next = new Set(set);
47
+ if (next.has(member)) {
48
+ next.delete(member);
49
+ }
50
+ else {
51
+ next.add(member);
52
+ }
53
+ return next;
54
+ }
55
+ /**
56
+ * A divider renders once, directly above the first UNpinned card — i.e. exactly at the
57
+ * pinned/rest boundary `sortProvidersByConfigured` produced. Never renders above index 0
58
+ * (nothing pinned, or the pinned card IS index 0 with no boundary yet) and never when
59
+ * every card is pinned (no "rest" to separate from).
60
+ */
61
+ function isDividerBefore({ option, previousOption, pinnedIdSet, }) {
62
+ return !pinnedIdSet.has(option.id) && previousOption !== undefined && pinnedIdSet.has(previousOption.id);
63
+ }
64
+ function TabHead({ labels, loading, onReload, agentHandle, }) {
65
+ return (_jsxs("div", { className: "jini-section-head", children: [_jsxs("div", { children: [_jsx("h4", { children: labels.title }), _jsx("p", { className: "jini-hint", children: labels.description })] }), _jsxs("button", { type: "button", className: "jini-button jini-button-ghost", onClick: onReload, disabled: loading, ...agentHandleProps({}, { base: agentHandle, ...({ action: 'reload', role: 'button', label: labels.reloadLabel }) }), children: [_jsx(Icon, { name: "refresh", size: 13 }), _jsx("span", { children: loading ? labels.reloadingLabel : labels.reloadLabel })] })] }));
66
+ }
67
+ function SaveRow({ labels, save, disabled, onSave, agentHandle, }) {
68
+ const saving = save.status === 'saving';
69
+ return (_jsxs("div", { className: "jini-media-provider-save-row", children: [_jsx("button", { type: "button", className: "jini-button", onClick: onSave, disabled: saving || disabled, ...agentHandleProps({}, { base: agentHandle, ...({ action: 'save', role: 'button', label: labels.saveChangesLabel }) }), children: saving ? labels.savingLabel : labels.saveChangesLabel }), save.status === 'saved' ? (_jsx("span", { className: "jini-hint", role: "status", children: labels.savedNoticeLabel })) : null, save.status === 'save-error' ? (_jsx("span", { className: "jini-hint jini-hint-error", role: "alert", children: labels.saveErrorLabel })) : null] }));
102
70
  }
103
71
  //# sourceMappingURL=MediaProvidersTab.js.map
@@ -0,0 +1,11 @@
1
+ import type { I18nContextValue } from '../../../i18n/index.js';
2
+ import type { MediaProvidersTabLabels } from './MediaProvidersTab.js';
3
+ /** Every `MediaProvidersTabLabels` field, resolved to the string actually rendered. */
4
+ export type ResolvedMediaProvidersTabLabels = Required<MediaProvidersTabLabels>;
5
+ /** Host-supplied label where given, else the translated English default. */
6
+ export declare function resolveMediaProvidersTabLabels({ t }: {
7
+ t: I18nContextValue['t'];
8
+ }, { labels }?: {
9
+ labels?: MediaProvidersTabLabels | undefined;
10
+ }): ResolvedMediaProvidersTabLabels;
11
+ //# sourceMappingURL=media-providers-tab-labels.d.ts.map
@@ -0,0 +1,57 @@
1
+ /** English defaults, keyed exactly like `MediaProvidersTabLabels`; each is passed through `t`
2
+ * only when the host did not supply that label (the dictionaries are keyed by this English text). */
3
+ const DEFAULT_LABELS = {
4
+ title: 'Media providers',
5
+ description: 'Credentials this host uses to generate images and video.',
6
+ reloadLabel: 'Reload',
7
+ reloadingLabel: 'Reloading…',
8
+ unreachableLabel: 'Could not reach the server. Showing local changes only.',
9
+ emptyStateLabel: 'No media providers configured yet.',
10
+ apiKeyLabel: 'API key',
11
+ apiKeyPlaceholder: 'Paste your API key',
12
+ showKeyLabel: 'Show',
13
+ hideKeyLabel: 'Hide',
14
+ baseUrlLabel: 'Base URL',
15
+ baseUrlPlaceholder: 'https://api.example.com',
16
+ baseUrlDefaultHintTemplate: 'Uses {url} by default.',
17
+ baseUrlInvalidLabel: 'Enter an absolute http:// or https:// URL that is not a private or internal address.',
18
+ modelLabel: 'Model',
19
+ modelPlaceholder: 'Default model',
20
+ savedWithMaskTemplate: 'Saved ({mask})',
21
+ unsavedLabel: 'Unsaved',
22
+ clearLabel: 'Clear',
23
+ saveChangesLabel: 'Save changes',
24
+ savingLabel: 'Saving…',
25
+ savedNoticeLabel: 'Saved.',
26
+ saveErrorLabel: 'Could not save media providers.',
27
+ };
28
+ /** Host-supplied label where given, else the translated English default. */
29
+ export function resolveMediaProvidersTabLabels({ t }, { labels } = {}) {
30
+ const pick = (key) => labels?.[key] ?? t(DEFAULT_LABELS[key]);
31
+ return {
32
+ title: pick('title'),
33
+ description: pick('description'),
34
+ reloadLabel: pick('reloadLabel'),
35
+ reloadingLabel: pick('reloadingLabel'),
36
+ unreachableLabel: pick('unreachableLabel'),
37
+ emptyStateLabel: pick('emptyStateLabel'),
38
+ apiKeyLabel: pick('apiKeyLabel'),
39
+ apiKeyPlaceholder: pick('apiKeyPlaceholder'),
40
+ showKeyLabel: pick('showKeyLabel'),
41
+ hideKeyLabel: pick('hideKeyLabel'),
42
+ baseUrlLabel: pick('baseUrlLabel'),
43
+ baseUrlPlaceholder: pick('baseUrlPlaceholder'),
44
+ baseUrlDefaultHintTemplate: pick('baseUrlDefaultHintTemplate'),
45
+ baseUrlInvalidLabel: pick('baseUrlInvalidLabel'),
46
+ modelLabel: pick('modelLabel'),
47
+ modelPlaceholder: pick('modelPlaceholder'),
48
+ savedWithMaskTemplate: pick('savedWithMaskTemplate'),
49
+ unsavedLabel: pick('unsavedLabel'),
50
+ clearLabel: pick('clearLabel'),
51
+ saveChangesLabel: pick('saveChangesLabel'),
52
+ savingLabel: pick('savingLabel'),
53
+ savedNoticeLabel: pick('savedNoticeLabel'),
54
+ saveErrorLabel: pick('saveErrorLabel'),
55
+ };
56
+ }
57
+ //# sourceMappingURL=media-providers-tab-labels.js.map
@@ -1,4 +1,4 @@
1
- import type { MediaProvidersPort } from '../../ports.js';
1
+ import type { MediaProviderCatalogPolicy, MediaProvidersPort } from '../../ports.js';
2
2
  import type { MediaProviderCredentials, MediaProviderMap, MediaProvidersLoadState, MediaProvidersSaveState } from '../../types.js';
3
3
  /** The operator-editable fields of a provider entry. Server markers
4
4
  * (`apiKeyConfigured`/`apiKeyTail`) are deliberately excluded — they only
@@ -8,6 +8,8 @@ import type { MediaProviderCredentials, MediaProviderMap, MediaProvidersLoadStat
8
8
  export type MediaProviderEditPatch = Pick<MediaProviderCredentials, 'apiKey' | 'baseUrl' | 'model'>;
9
9
  export interface UseMediaProvidersTabOptions {
10
10
  port: MediaProvidersPort;
11
+ /** Identity rules shared by catalog rows, local edits and daemon responses. Stable while mounted. */
12
+ catalogPolicy: MediaProviderCatalogPolicy;
11
13
  /** Host-persisted local edits from before this tab mounted (e.g. the
12
14
  * host's own locally-cached config, read before the daemon round-trip
13
15
  * completes). Defaults to none. Threaded into the initial
@@ -54,5 +56,5 @@ export interface UseMediaProvidersTabResult {
54
56
  * `{}` before merging — would make a transient daemon blip read as "the
55
57
  * server manages nothing" and silently wipe local edits.
56
58
  */
57
- export declare function useMediaProvidersTab({ port, initialProviders }: UseMediaProvidersTabOptions): UseMediaProvidersTabResult;
59
+ export declare function useMediaProvidersTab({ port, catalogPolicy, initialProviders }: UseMediaProvidersTabOptions): UseMediaProvidersTabResult;
58
60
  //# sourceMappingURL=useMediaProvidersTab.d.ts.map
@@ -1,6 +1,33 @@
1
- import { normalizeMediaProviderMap, canonicalMediaProviderId } from '@jini-ai/integrations/media-providers/catalog';
2
1
  import { useCallback, useEffect, useRef, useState } from 'react';
3
2
  import { hasAnyConfiguredProvider, isEntryEmpty, mergeDaemonProviders, shouldSyncLocalProvidersToDaemon, } from '../../rules.js';
3
+ /**
4
+ * Releases the tombstones a successful write carried. The server has now accepted a map without
5
+ * these, so the local absence is no longer a claim waiting to be defended — the daemon agrees.
6
+ * Re-touched ids keep their tombstone: the operator changed their mind mid-flight and this
7
+ * response does not describe that decision.
8
+ */
9
+ function releaseAcknowledgedClears({ cleared, clearsInFlight, touched, }) {
10
+ for (const providerId of clearsInFlight) {
11
+ if (!touched.has(providerId))
12
+ cleared.delete(providerId);
13
+ }
14
+ }
15
+ /** The server's saved map with every provider touched during the flight reset to local truth —
16
+ * including local deletion (see `flushNow`'s doc comment). Mutates and returns `saved`. */
17
+ function overlayTouchedProviders({ saved, local, touched, }) {
18
+ for (const providerId of touched) {
19
+ const entry = local[providerId];
20
+ if (entry === undefined)
21
+ delete saved[providerId];
22
+ else
23
+ saved[providerId] = entry;
24
+ }
25
+ return saved;
26
+ }
27
+ /** A save rejection as display text; a non-`Error` rejection is stringified rather than read as `.message`. */
28
+ function saveErrorMessage({ error }) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
4
31
  /**
5
32
  * Owns the tab's async edges: the initial daemon fetch (plus the one-time
6
33
  * first-upload migration `shouldSyncLocalProvidersToDaemon` guards), manual
@@ -18,7 +45,8 @@ import { hasAnyConfiguredProvider, isEntryEmpty, mergeDaemonProviders, shouldSyn
18
45
  * `{}` before merging — would make a transient daemon blip read as "the
19
46
  * server manages nothing" and silently wipe local edits.
20
47
  */
21
- export function useMediaProvidersTab({ port, initialProviders }) {
48
+ export function useMediaProvidersTab({ port, catalogPolicy, initialProviders }) {
49
+ const { normalizeMediaProviderMap, canonicalMediaProviderId } = catalogPolicy;
22
50
  const [providers, setProviders] = useState(() => normalizeMediaProviderMap({ providers: initialProviders ?? {} }));
23
51
  const [pendingProviderIds, setPendingProviderIds] = useState(() => new Set());
24
52
  const [load, setLoad] = useState({ status: 'loading' });
@@ -107,22 +135,8 @@ export function useMediaProvidersTab({ port, initialProviders }) {
107
135
  if (!alive.current)
108
136
  return { status: 'abandoned' };
109
137
  const touched = mutatedSinceSend.current;
110
- // The server has now accepted a map without these, so the local absence is no longer a claim
111
- // waiting to be defended — the daemon agrees. Re-touched ids keep their tombstone: the
112
- // operator changed their mind mid-flight and this response does not describe that decision.
113
- for (const providerId of clearsInFlight) {
114
- if (!touched.has(providerId))
115
- clearedProviderIds.current.delete(providerId);
116
- }
117
- const next = normalizeMediaProviderMap({ providers: saved });
118
- for (const providerId of touched) {
119
- const local = providersRef.current[providerId];
120
- if (local === undefined)
121
- delete next[providerId];
122
- else
123
- next[providerId] = local;
124
- }
125
- applyProviders(next);
138
+ releaseAcknowledgedClears({ cleared: clearedProviderIds.current, clearsInFlight, touched });
139
+ applyProviders(overlayTouchedProviders({ saved: normalizeMediaProviderMap({ providers: saved }), local: providersRef.current, touched }));
126
140
  // Everything this write sent is now persisted; only edits made during
127
141
  // the flight are still unflushed.
128
142
  applyPending(new Set([...pendingRef.current].filter((providerId) => touched.has(providerId))));
@@ -132,10 +146,10 @@ export function useMediaProvidersTab({ port, initialProviders }) {
132
146
  catch (error) {
133
147
  if (!alive.current)
134
148
  return { status: 'abandoned' };
135
- setSave({ status: 'save-error', message: error instanceof Error ? error.message : String(error) });
149
+ setSave({ status: 'save-error', message: saveErrorMessage({ error }) });
136
150
  return { status: 'failed' };
137
151
  }
138
- }, [applyProviders, applyPending]);
152
+ }, [applyProviders, applyPending, normalizeMediaProviderMap]);
139
153
  /** True while a write is at the port. */
140
154
  const writing = useRef(false);
141
155
  /** The single coalesced follow-up write requested during that one. */
@@ -216,7 +230,7 @@ export function useMediaProvidersTab({ port, initialProviders }) {
216
230
  void persist();
217
231
  }
218
232
  });
219
- }, [persist, applyProviders]);
233
+ }, [persist, applyProviders, normalizeMediaProviderMap]);
220
234
  useEffect(() => {
221
235
  // Mount (or host-supplied port swap): migration is a one-time boot
222
236
  // check, matching the origin's App.tsx boot sequence — a manual
@@ -242,7 +256,7 @@ export function useMediaProvidersTab({ port, initialProviders }) {
242
256
  if (!currentPending.has(providerId))
243
257
  applyPending(new Set(currentPending).add(providerId));
244
258
  mutatedSinceSend.current.add(providerId);
245
- }, [applyProviders, applyPending]);
259
+ }, [applyProviders, applyPending, canonicalMediaProviderId]);
246
260
  const clearProvider = useCallback((providerId) => {
247
261
  providerId = canonicalMediaProviderId({ id: providerId });
248
262
  void (async () => {
@@ -278,7 +292,7 @@ export function useMediaProvidersTab({ port, initialProviders }) {
278
292
  return;
279
293
  applyProviders({ ...providersRef.current, [providerId]: previousEntry });
280
294
  })();
281
- }, [persist, applyProviders]);
295
+ }, [persist, applyProviders, canonicalMediaProviderId]);
282
296
  const saveChanges = useCallback(() => {
283
297
  void persist();
284
298
  }, [persist]);
@@ -89,27 +89,34 @@ export function hasAnyConfiguredProvider(providers) {
89
89
  */
90
90
  export function mergeDaemonProviders(localProviders, daemonProviders, options) {
91
91
  const dropped = options?.dropProviderIds;
92
- const withoutDropped = (map) => dropped === undefined || dropped.size === 0
93
- ? map
94
- : Object.fromEntries(Object.entries(map).filter(([providerId]) => !dropped.has(providerId)));
95
- const local = withoutDropped({ ...(localProviders ?? {}) });
92
+ const local = withoutProviderIds({ map: { ...(localProviders ?? {}) } }, { dropped });
96
93
  if (daemonProviders == null)
97
94
  return local;
98
95
  if (!hasAnyConfiguredProvider(daemonProviders)) {
99
96
  return Object.fromEntries(Object.entries(local).filter(([, entry]) => !isMarkerOnlyEntry(entry)));
100
97
  }
98
+ const preserveIds = options?.preserveLocalProviderIds;
101
99
  const merged = { ...local };
102
100
  for (const [providerId, daemonEntry] of Object.entries(daemonProviders)) {
103
- if (!isEntryPresent(daemonEntry))
101
+ if (!isEntryPresent(daemonEntry) || dropped?.has(providerId))
104
102
  continue;
105
- if (dropped?.has(providerId))
106
- continue;
107
- const localEntry = merged[providerId];
108
- const preserveLocalEdit = Boolean(options?.preserveLocalProviderIds?.has(providerId)) && hasRecoverableFields(localEntry);
109
- merged[providerId] = preserveLocalEdit ? { ...daemonEntry, ...localEntry } : { ...daemonEntry };
103
+ merged[providerId] = mergeDaemonEntry({ providerId, daemonEntry, localEntry: merged[providerId] }, { preserveIds });
110
104
  }
111
105
  return merged;
112
106
  }
107
+ /** `map` minus every id in `dropped` (see `mergeDaemonProviders`'s `dropProviderIds`). */
108
+ function withoutProviderIds({ map }, { dropped }) {
109
+ if (dropped === undefined || dropped.size === 0)
110
+ return map;
111
+ return Object.fromEntries(Object.entries(map).filter(([providerId]) => !dropped.has(providerId)));
112
+ }
113
+ /** Case 3 of `mergeDaemonProviders` for one provider: the server entry wins
114
+ * unless the caller asked to preserve a local edit that has recoverable
115
+ * fields, which is then layered ON TOP so server-only markers survive. */
116
+ function mergeDaemonEntry({ providerId, daemonEntry, localEntry, }, { preserveIds }) {
117
+ const preserveLocalEdit = Boolean(preserveIds?.has(providerId)) && hasRecoverableFields(localEntry);
118
+ return preserveLocalEdit ? { ...daemonEntry, ...localEntry } : { ...daemonEntry };
119
+ }
113
120
  /**
114
121
  * Whether local credentials should be pushed to the server.
115
122
  *
@@ -9,14 +9,9 @@
9
9
  * None of that is product-bound — it is the same problem any host has once
10
10
  * credentials can live in two places at once.
11
11
  *
12
- * What is NOT ported verbatim: the origin's full `MEDIA_PROVIDERS` catalog
13
- * (`media/models.ts`, 761 lines of concrete image/video vendors with their
14
- * endpoints and model lists). Instead this feature ships a small, curated
15
- * `DEFAULT_MEDIA_PROVIDER_CATALOG` (see `constants.ts`) as a starting point,
16
- * not a fixed list — same convention as `execution/constants.ts`'s
17
- * `DEFAULT_PROVIDER_PRESETS`. A host with its own provider roster (or a
18
- * larger/different one) passes its own `catalog` to `MediaProvidersTab` to
19
- * replace this wholesale.
12
+ * The host supplies the provider catalog and its identity policy. Vendor identities,
13
+ * endpoint defaults and alias rules belong to the integrations catalog, so importing
14
+ * generic UI never selects or initializes a vendor roster.
20
15
  *
21
16
  * Also not ported: the origin's functions take and return its whole
22
17
  * `AppConfig`. Here they take and return the providers RECORD alone. Threading
@@ -51,20 +46,7 @@ export interface MediaProviderCredentials {
51
46
  }
52
47
  /** Credentials keyed by provider id. */
53
48
  export type MediaProviderMap = Record<string, MediaProviderCredentials>;
54
- /**
55
- * One selectable provider in the catalog. A host supplies its own catalog —
56
- * this package ships `DEFAULT_MEDIA_PROVIDER_CATALOG` as a starting point,
57
- * not as a fixed list (same convention as `ProviderPreset`).
58
- */
59
- export interface MediaProviderOption {
60
- id: string;
61
- label: string;
62
- /** Prefilled when the operator picks this provider and typed nothing. */
63
- defaultBaseUrl?: string;
64
- /** Model ids this provider advertises. A host that discovers models at
65
- * runtime can leave this empty. */
66
- models?: readonly string[];
67
- }
49
+ export type { MediaProviderOption } from '@jini-ai/core';
68
50
  /**
69
51
  * Result of the tab's daemon read, surfaced while the initial fetch or a
70
52
  * manual reload is in flight. `'unreachable'` mirrors
@@ -9,14 +9,9 @@
9
9
  * None of that is product-bound — it is the same problem any host has once
10
10
  * credentials can live in two places at once.
11
11
  *
12
- * What is NOT ported verbatim: the origin's full `MEDIA_PROVIDERS` catalog
13
- * (`media/models.ts`, 761 lines of concrete image/video vendors with their
14
- * endpoints and model lists). Instead this feature ships a small, curated
15
- * `DEFAULT_MEDIA_PROVIDER_CATALOG` (see `constants.ts`) as a starting point,
16
- * not a fixed list — same convention as `execution/constants.ts`'s
17
- * `DEFAULT_PROVIDER_PRESETS`. A host with its own provider roster (or a
18
- * larger/different one) passes its own `catalog` to `MediaProvidersTab` to
19
- * replace this wholesale.
12
+ * The host supplies the provider catalog and its identity policy. Vendor identities,
13
+ * endpoint defaults and alias rules belong to the integrations catalog, so importing
14
+ * generic UI never selects or initializes a vendor roster.
20
15
  *
21
16
  * Also not ported: the origin's functions take and return its whole
22
17
  * `AppConfig`. Here they take and return the providers RECORD alone. Threading