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

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 (227) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/useDismissOnOutsideOrEscape.js +1 -1
  3. package/dist/browser/useGlobalKeydown.js +2 -2
  4. package/dist/browser/useModalWindowDragGuard.js +1 -1
  5. package/dist/core/theme/css-name.d.ts +3 -0
  6. package/dist/core/theme/css-name.js +5 -0
  7. package/dist/core/theme/stylesheet.js +2 -1
  8. package/dist/core/theme/validation.js +15 -4
  9. package/dist/core.d.ts +1 -1
  10. package/dist/core.js +1 -1
  11. package/dist/features/about/rules.js +2 -1
  12. package/dist/features/admin-widgets/components/InfoTip.hooks.js +22 -12
  13. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +1 -1
  14. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +1 -1
  15. package/dist/features/admin-widgets/components/SeeMore/SeeMore.hooks.js +2 -2
  16. package/dist/features/admin-widgets/components/SeeMore/SeeMore.js +4 -2
  17. package/dist/features/admin-widgets/components/Select/Select.hooks.js +2 -1
  18. package/dist/features/admin-widgets/components/Select/Select.js +2 -1
  19. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +4 -2
  20. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +3 -2
  21. package/dist/features/appearance/constants.d.ts +28 -1
  22. package/dist/features/appearance/constants.js +12 -9
  23. package/dist/features/appearance/react/components/AppearanceTab.js +4 -4
  24. package/dist/features/asset-grid/react/components/AssetGrid.js +8 -3
  25. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +7 -6
  26. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +8 -10
  27. package/dist/features/asset-grid/react/hooks/useRubberBandDrag.js +1 -1
  28. package/dist/features/asset-tree-browser/react/components/AssetTreeFileRow.js +6 -8
  29. package/dist/features/asset-tree-browser/react/hooks/useAssetTreeRowMenu.js +1 -1
  30. package/dist/features/asset-tree-browser/rules.js +1 -3
  31. package/dist/features/browser-chrome/rules.js +10 -8
  32. package/dist/features/charts/browser.js +2 -1
  33. package/dist/features/charts/contract.d.ts +4 -4
  34. package/dist/features/charts/react.js +5 -3
  35. package/dist/features/charts/wire.d.ts +19 -0
  36. package/dist/features/charts/wire.js +20 -2
  37. package/dist/features/connectors/ConnectorsBrowser.js +3 -1
  38. package/dist/features/connectors/hooks/useConnectorAuthorization.js +1 -1
  39. package/dist/features/connectors/hooks/useConnectorCatalog.js +2 -2
  40. package/dist/features/connectors/rules.js +10 -14
  41. package/dist/features/execution/constants.js +0 -1
  42. package/dist/features/execution/react/components/AgentDiagnosticRow.js +2 -0
  43. package/dist/features/execution/react/components/ByokProviderForm.js +1 -3
  44. package/dist/features/execution/react/components/LocalCliAgentSettings.js +2 -1
  45. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +6 -2
  46. package/dist/features/execution/react/hooks/useExecutionTab.js +9 -5
  47. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +3 -6
  48. package/dist/features/execution/react/hooks/useReasoningControl.js +3 -3
  49. package/dist/features/execution/rules.js +23 -18
  50. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +10 -1
  51. package/dist/features/file-dropzone/rules.js +3 -2
  52. package/dist/features/folder-path-drop/rules.d.ts +2 -3
  53. package/dist/features/folder-path-drop/rules.js +3 -4
  54. package/dist/features/html-editor/canvas-embed-placeholders.js +2 -3
  55. package/dist/features/html-editor/css.js +7 -5
  56. package/dist/features/html-editor/editor-blocks.js +60 -6
  57. package/dist/features/html-editor/protected-elements.js +5 -3
  58. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +3 -3
  59. package/dist/features/html-editor/source-splice.js +6 -4
  60. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +8 -0
  61. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +8 -0
  62. package/dist/features/i18n/dictionaries/settings-dialog.de.js +8 -0
  63. package/dist/features/i18n/dictionaries/settings-dialog.en.js +8 -0
  64. package/dist/features/i18n/dictionaries/settings-dialog.es.js +8 -0
  65. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +8 -0
  66. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +8 -0
  67. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +8 -0
  68. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +8 -0
  69. package/dist/features/i18n/dictionaries/settings-dialog.id.js +8 -0
  70. package/dist/features/i18n/dictionaries/settings-dialog.it.js +8 -0
  71. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +8 -0
  72. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +8 -0
  73. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +8 -0
  74. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +8 -0
  75. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +8 -0
  76. package/dist/features/i18n/dictionaries/settings-dialog.th.js +8 -0
  77. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +8 -0
  78. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +8 -0
  79. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +8 -0
  80. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +8 -0
  81. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +8 -0
  82. package/dist/features/iframe-pool/react/components/IframeKeepAliveProvider.js +5 -5
  83. package/dist/features/iframe-pool/react/dom-sync.js +2 -2
  84. package/dist/features/integrations/react/components/IntegrationsTab.js +3 -1
  85. package/dist/features/integrations/rules.js +6 -4
  86. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -5
  87. package/dist/features/interactive-ui/table-data.js +1 -1
  88. package/dist/features/lexical-rich-text-editor/deserialize.js +2 -2
  89. package/dist/features/lexical-rich-text-editor/mention-parser.js +4 -4
  90. package/dist/features/lexical-rich-text-editor/react/components/RichTextInput.js +2 -1
  91. package/dist/features/lexical-rich-text-editor/react/hooks/useCaretFloatingLayerPosition.js +1 -1
  92. package/dist/features/lexical-rich-text-editor/react/hooks/useMentionColorStamping.js +1 -1
  93. package/dist/features/lexical-rich-text-editor/serialize.js +13 -10
  94. package/dist/features/list-detail-panel/react/components/ListDetailPanel.js +1 -1
  95. package/dist/features/mcp-ui/confirmation-store.js +3 -6
  96. package/dist/features/mcp-ui/surfaces/checkbox.d.ts +2 -0
  97. package/dist/features/mcp-ui/surfaces/choice-group.d.ts +2 -0
  98. package/dist/features/mcp-ui/surfaces/choice-group.js +2 -2
  99. package/dist/features/mcp-ui/surfaces/confirmation.d.ts +5 -0
  100. package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
  101. package/dist/features/mcp-ui/surfaces/document.d.ts +14 -16
  102. package/dist/features/mcp-ui/surfaces/document.js +46 -7
  103. package/dist/features/mcp-ui/surfaces/fields.d.ts +7 -1
  104. package/dist/features/mcp-ui/surfaces/fields.js +14 -11
  105. package/dist/features/mcp-ui/surfaces/form.js +6 -1
  106. package/dist/features/mcp-ui/surfaces/host-theme.js +2 -1
  107. package/dist/features/mcp-ui/surfaces/index.d.ts +1 -0
  108. package/dist/features/mcp-ui/surfaces/index.js +1 -0
  109. package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
  110. package/dist/features/mcp-ui/surfaces/select.d.ts +2 -0
  111. package/dist/features/mcp-ui/surfaces/select.js +2 -1
  112. package/dist/features/mcp-ui/surfaces/structured-details.d.ts +40 -0
  113. package/dist/features/mcp-ui/surfaces/structured-details.js +159 -0
  114. package/dist/features/mcp-ui/surfaces/text-input.d.ts +2 -0
  115. package/dist/features/mcp-ui/surfaces/text-input.js +1 -1
  116. package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
  117. package/dist/features/mcp-ui/surfaces/tool-approval.js +42 -0
  118. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +2 -1
  119. package/dist/features/memory/async-commit-guard.js +4 -1
  120. package/dist/features/memory/formatters.js +5 -1
  121. package/dist/features/memory/react/components/MemoryConnectedPanel.js +3 -1
  122. package/dist/features/memory/react/components/MemoryEntryCard.js +1 -1
  123. package/dist/features/memory/react/components/MemorySettingsPanel.js +3 -1
  124. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +12 -10
  125. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +2 -5
  126. package/dist/features/memory/react/hooks/useMemoryConnectors.rules.js +10 -9
  127. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -2
  128. package/dist/features/memory/react/hooks/useMemoryFlash.hooks.js +1 -1
  129. package/dist/features/memory/rules.js +2 -1
  130. package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -2
  131. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +3 -2
  132. package/dist/features/panel-kit/fetch-query/binding.react.js +2 -1
  133. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +13 -4
  134. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -1
  135. package/dist/features/panel-kit/hooks/use-focus-trap.hooks.js +1 -1
  136. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +14 -10
  137. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +2 -2
  138. package/dist/features/progress-card/components/ProgressCard.d.ts +1 -1
  139. package/dist/features/progress-card/reference-adapters.js +23 -13
  140. package/dist/features/progress-card/rules.d.ts +0 -1
  141. package/dist/features/progress-card/rules.js +14 -20
  142. package/dist/features/progress-card/types.d.ts +1 -1
  143. package/dist/features/project-locations/dependencies.js +6 -1
  144. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +2 -1
  145. package/dist/features/project-locations/rules.js +2 -1
  146. package/dist/features/resource-dashboard/react/components/ResourceBoardToolbar.js +8 -1
  147. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +2 -1
  148. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +3 -1
  149. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +2 -1
  150. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +1 -1
  151. package/dist/features/resource-dashboard/react/hooks/useResourceBoard.js +1 -1
  152. package/dist/features/schedule-picker/rules.js +13 -7
  153. package/dist/features/sketch-editor/constants.d.ts +0 -6
  154. package/dist/features/sketch-editor/constants.js +17 -15
  155. package/dist/features/sketch-editor/dom.js +4 -7
  156. package/dist/features/sketch-editor/react/components/SketchMainMenu.js +2 -1
  157. package/dist/features/sketch-editor/react/components/SketchSaveStateBadge.js +2 -1
  158. package/dist/features/sketch-editor/react/hooks/useSketchDomEnhancements.js +1 -1
  159. package/dist/features/sketch-editor/rules.js +13 -12
  160. package/dist/features/skills/dependencies.js +9 -4
  161. package/dist/features/skills/react/components/SkillRow.js +2 -1
  162. package/dist/features/skills/react/components/SkillsTab.js +2 -1
  163. package/dist/features/skills/rules.js +3 -3
  164. package/dist/features/source-config-list/agent-handles.js +4 -3
  165. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +3 -1
  166. package/dist/features/tab-strip/react/components/TabBar.hooks.js +4 -2
  167. package/dist/features/version-manager/react/components/VersionPreviewFrame.js +4 -1
  168. package/dist/features/version-manager/react/components/VersionSidebar.js +8 -6
  169. package/dist/features/version-manager/react/hooks/usePreviewCanvasSize.js +1 -1
  170. package/dist/features/version-manager/react/hooks/useVersionManager.js +5 -7
  171. package/dist/features/version-manager/rules.js +3 -5
  172. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +2 -1
  173. package/dist/features/viewer-shell/react/components/SegmentedToggle.js +2 -1
  174. package/dist/features/viewer-shell/react/components/ViewerShell.js +2 -1
  175. package/dist/features/viewer-shell/rules.js +8 -7
  176. package/dist/react/components/AgentIcon.js +0 -1
  177. package/dist/react/components/BrandLogo.js +10 -2
  178. package/dist/react/components/CustomSelect.js +1 -1
  179. package/dist/react/components/EditorIcon.js +17 -14
  180. package/dist/react/components/Loading.js +2 -1
  181. package/dist/react/components/OnboardingChipField.js +5 -1
  182. package/dist/react/components/OnboardingDropdown.js +6 -4
  183. package/dist/react/components/PaletteTweaks.js +7 -6
  184. package/dist/react/components/PillButton.js +2 -1
  185. package/dist/react/components/PopoverMenu.js +2 -1
  186. package/dist/react/components/RemixIcon.js +2 -1
  187. package/dist/react/components/SecretInput.hooks.d.ts +2 -25
  188. package/dist/react/components/SecretInput.hooks.js +2 -24
  189. package/dist/react/components/SecretInput.js +5 -4
  190. package/dist/react/components/Toast.js +4 -2
  191. package/dist/react/components/TooltipLayer.js +4 -1
  192. package/dist/react/components/WorkingDirPicker.js +5 -4
  193. package/dist/react/components/agent-icon-data-uris.generated.js +0 -1
  194. package/dist/react/components/remixicon-font-recovery.js +9 -1
  195. package/dist/react/components/select-popover.js +2 -2
  196. package/dist/react/hooks/useBrandFonts.js +1 -1
  197. package/dist/react/hooks/useEdgeAutoScroll.js +1 -1
  198. package/dist/react/hooks/useInView.js +9 -9
  199. package/dist/react/hooks/useResizableSplitPane.js +12 -20
  200. package/dist/react/mcp-ui/useHostSurfaceTokens.js +1 -1
  201. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +7 -1
  202. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.controller.js +4 -2
  203. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.js +9 -5
  204. package/dist/renderers/annotation-canvas/rules.js +9 -13
  205. package/dist/renderers/annotation-canvas/theme.js +5 -2
  206. package/dist/renderers/renderers/index.js +1 -1
  207. package/dist/renderers/renderers/markdown.js +4 -6
  208. package/dist/renderers/shiki.js +7 -1
  209. package/dist/theme/apply.js +7 -3
  210. package/dist/utils/class-names.d.ts +3 -0
  211. package/dist/utils/class-names.js +5 -0
  212. package/dist/utils/design-md.js +3 -3
  213. package/dist/utils/ime-composing.d.ts +1 -1
  214. package/dist/utils/ime-composing.js +1 -2
  215. package/dist/utils/localized-url.js +2 -11
  216. package/dist/utils/markdown-scroll-sync.js +1 -1
  217. package/dist/utils/omit-keys.d.ts +3 -0
  218. package/dist/utils/omit-keys.js +8 -0
  219. package/dist/utils/polygon-selection.js +1 -1
  220. package/dist/utils/sse.js +3 -0
  221. package/dist/utils/trim-chars.d.ts +13 -0
  222. package/dist/utils/trim-chars.js +30 -0
  223. package/dist/utils/unhandled-case.d.ts +10 -0
  224. package/dist/utils/unhandled-case.js +12 -0
  225. package/dist/utils/unit-bezier.js +1 -3
  226. package/dist/utils/zip.js +6 -10
  227. package/package.json +32 -29
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_HOME_NAVIGATION_ENTRY, EMPTY_URL, HISTORY_LIMIT } from './constants.js';
2
+ import { trimTrailingChars } from '../../utils/trim-chars.js';
2
3
  function escapeRegExp(value) {
3
4
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4
5
  }
@@ -46,7 +47,7 @@ export function formatAddressDisplayParts(url, title, homeLabel = DEFAULT_HOME_N
46
47
  const fallback = labelFromUrl(url, homeLabel);
47
48
  if (cleanTitle === fallback || cleanTitle === url)
48
49
  return { url };
49
- return { url: url.replace(/\/+$/, ''), title: cleanTitle };
50
+ return { url: trimTrailingChars(url, '/'), title: cleanTitle };
50
51
  }
51
52
  export function formatAddressDisplay(url, title, homeLabel = DEFAULT_HOME_NAVIGATION_ENTRY.title) {
52
53
  const parts = formatAddressDisplayParts(url, title, homeLabel);
@@ -81,7 +82,7 @@ export function isHistoryUrl(url) {
81
82
  return url !== EMPTY_URL && (isHttpLikeUrl(url) || /^file:\/\//i.test(url));
82
83
  }
83
84
  export function sameUrl(left, right) {
84
- return left.replace(/\/+$/, '') === right.replace(/\/+$/, '');
85
+ return trimTrailingChars(left, '/') === trimTrailingChars(right, '/');
85
86
  }
86
87
  function cleanIconUrl(url) {
87
88
  const value = url?.trim();
@@ -98,11 +99,11 @@ export function isHistoryEntry(value) {
98
99
  if (typeof value !== 'object' || value == null || Array.isArray(value))
99
100
  return false;
100
101
  const record = value;
101
- return (typeof record.url === 'string'
102
- && typeof record.title === 'string'
103
- && typeof record.lastVisitedAt === 'number'
104
- && typeof record.visitCount === 'number'
105
- && (record.iconUrl === undefined || typeof record.iconUrl === 'string'));
102
+ if (typeof record.url !== 'string' || typeof record.title !== 'string')
103
+ return false;
104
+ if (typeof record.lastVisitedAt !== 'number' || typeof record.visitCount !== 'number')
105
+ return false;
106
+ return record.iconUrl === undefined || typeof record.iconUrl === 'string';
106
107
  }
107
108
  /** Namespace + scope key -> storage key. Namespace defaults to a generic Jini prefix (see `constants.ts`); scope key is host-supplied (e.g. a project or tab id). */
108
109
  export function historyStorageKey(namespace, scopeKey) {
@@ -234,7 +235,8 @@ function navigationTitle({ url }, { title, homeLabel }) {
234
235
  function findRejoinTarget({ state, url }, { replacePendingTarget, pendingTarget }) {
235
236
  const { navigationStack: stack, navigationIndex: index } = state;
236
237
  const current = stack[index];
237
- if (current && (sameUrl(current.url, url) || (replacePendingTarget && pendingTarget && sameUrl(current.url, pendingTarget)))) {
238
+ const replacingCurrent = current && replacePendingTarget && pendingTarget && sameUrl(current.url, pendingTarget);
239
+ if (current && (sameUrl(current.url, url) || replacingCurrent)) {
238
240
  return { index, entry: current };
239
241
  }
240
242
  return [index - 1, index + 1]
@@ -111,7 +111,8 @@ export function mountChart({ element, spec, registry }, options = {}) {
111
111
  async update({ spec: nextSpec, theme = input.theme }, _options = {}) {
112
112
  if (destroyed)
113
113
  return;
114
- const updateRevision = ++revision;
114
+ revision += 1;
115
+ const updateRevision = revision;
115
116
  input = { spec: nextSpec, theme };
116
117
  showData();
117
118
  const preferred = candidates()[0];
@@ -15,12 +15,12 @@ export declare const chartSpecSchema: z.ZodObject<{
15
15
  color: z.ZodOptional<z.ZodString>;
16
16
  }, "strict", z.ZodTypeAny, {
17
17
  key: string;
18
- label?: string | undefined;
19
18
  color?: string | undefined;
19
+ label?: string | undefined;
20
20
  }, {
21
21
  key: string;
22
- label?: string | undefined;
23
22
  color?: string | undefined;
23
+ label?: string | undefined;
24
24
  }>, "many">;
25
25
  title: z.ZodOptional<z.ZodString>;
26
26
  description: z.ZodOptional<z.ZodString>;
@@ -122,8 +122,8 @@ export declare const chartSpecSchema: z.ZodObject<{
122
122
  data: Record<string, ChartValue>[];
123
123
  series: {
124
124
  key: string;
125
- label?: string | undefined;
126
125
  color?: string | undefined;
126
+ label?: string | undefined;
127
127
  }[];
128
128
  title?: string | undefined;
129
129
  provider?: string | undefined;
@@ -154,8 +154,8 @@ export declare const chartSpecSchema: z.ZodObject<{
154
154
  data: Record<string, ChartValue>[];
155
155
  series: {
156
156
  key: string;
157
- label?: string | undefined;
158
157
  color?: string | undefined;
158
+ label?: string | undefined;
159
159
  }[];
160
160
  title?: string | undefined;
161
161
  provider?: string | undefined;
@@ -6,7 +6,7 @@ import { InteractiveUiRegistry } from '../interactive-ui/registry.js';
6
6
  import { chartSpecSchema, DEFAULT_CHART_THEME } from './contract.js';
7
7
  import { mountChart } from './browser.js';
8
8
  import { chartCapability, chartProvidersByKind } from './registry.js';
9
- import { chartWireManifests, legacyChartSpec } from './wire.js';
9
+ import { chartWireManifests, legacyChartSpec, withSeriesLabels } from './wire.js';
10
10
  /** React owns only the container; mountChart owns its descendants and lifecycle. */
11
11
  export function ChartHost({ spec, registry, theme = DEFAULT_CHART_THEME, siteProvider, onProviderError }) {
12
12
  const element = useRef(null);
@@ -34,10 +34,12 @@ export function ChartHost({ spec, registry, theme = DEFAULT_CHART_THEME, sitePro
34
34
  /** Known wire kinds use the established shapes. Other providers register spec-based entries without
35
35
  * adding fields or kinds to the core grammar. This closure is built once per host snapshot.
36
36
  */
37
+ /** Wire kinds that predate spec-based entries and keep their established prop shapes. */
38
+ const LEGACY_CHART_KINDS = ['bar', 'line', 'pie'];
37
39
  function chartComponent(kind, providers, options) {
38
40
  return function RegisteredChart(props) {
39
- const legacyKind = kind === 'bar' ? 'bar' : kind === 'line' ? 'line' : kind === 'pie' ? 'pie' : undefined;
40
- const spec = legacyKind ? legacyChartSpec({ kind: legacyKind, props }) : chartSpecSchema.parse(props.spec);
41
+ const legacyKind = LEGACY_CHART_KINDS.find((legacy) => legacy === kind);
42
+ const spec = withSeriesLabels({ spec: legacyKind ? legacyChartSpec({ kind: legacyKind, props }) : chartSpecSchema.parse(props.spec) });
41
43
  return _jsx(ChartHost, { spec: spec, registry: providers, ...options });
42
44
  };
43
45
  }
@@ -8,32 +8,44 @@ export declare const rechartsBarChartPropsSchema: z.ZodObject<{
8
8
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
9
9
  categoryKey: z.ZodString;
10
10
  valueKey: z.ZodString;
11
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
12
+ valueLabel: z.ZodOptional<z.ZodString>;
11
13
  color: z.ZodOptional<z.ZodString>;
12
14
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
13
15
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
14
16
  categoryKey: z.ZodString;
15
17
  valueKey: z.ZodString;
18
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
19
+ valueLabel: z.ZodOptional<z.ZodString>;
16
20
  color: z.ZodOptional<z.ZodString>;
17
21
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
18
22
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
19
23
  categoryKey: z.ZodString;
20
24
  valueKey: z.ZodString;
25
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
26
+ valueLabel: z.ZodOptional<z.ZodString>;
21
27
  color: z.ZodOptional<z.ZodString>;
22
28
  }, z.ZodTypeAny, "passthrough">>;
23
29
  export declare const rechartsLineChartPropsSchema: z.ZodObject<{
24
30
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
25
31
  categoryKey: z.ZodString;
26
32
  valueKey: z.ZodString;
33
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
34
+ valueLabel: z.ZodOptional<z.ZodString>;
27
35
  color: z.ZodOptional<z.ZodString>;
28
36
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
29
37
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
30
38
  categoryKey: z.ZodString;
31
39
  valueKey: z.ZodString;
40
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
41
+ valueLabel: z.ZodOptional<z.ZodString>;
32
42
  color: z.ZodOptional<z.ZodString>;
33
43
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
34
44
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
35
45
  categoryKey: z.ZodString;
36
46
  valueKey: z.ZodString;
47
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
48
+ valueLabel: z.ZodOptional<z.ZodString>;
37
49
  color: z.ZodOptional<z.ZodString>;
38
50
  }, z.ZodTypeAny, "passthrough">>;
39
51
  export declare const rechartsPieChartPropsSchema: z.ZodObject<{
@@ -85,4 +97,11 @@ export declare function legacyChartSpec({ kind, props }: {
85
97
  kind: 'bar' | 'line' | 'pie';
86
98
  props: Record<string, unknown>;
87
99
  }, _options?: Record<string, never>): ChartSpec;
100
+ /**
101
+ * Give every series a label, so a provider's legend and tooltip never show a raw data key
102
+ * (owner, 2026-10-30: a sign-ups chart's legend read "signups"). A label the caller passed wins.
103
+ */
104
+ export declare function withSeriesLabels({ spec }: {
105
+ spec: ChartSpec;
106
+ }, _options?: Record<string, never>): ChartSpec;
88
107
  //# sourceMappingURL=wire.d.ts.map
@@ -4,7 +4,9 @@ import { chartSpecSchema } from './contract.js';
4
4
  /** Same passthrough/record reasoning as native data-table: A2UI action bindings and extra row data
5
5
  * belong to the wire protocol, not a renderer. Retain their established shapes during extraction. */
6
6
  export const rechartsBarChartPropsSchema = z.object({
7
- data: z.array(z.record(z.unknown())).min(1), categoryKey: z.string(), valueKey: z.string(), color: z.string().optional(),
7
+ data: z.array(z.record(z.unknown())).min(1), categoryKey: z.string(), valueKey: z.string(),
8
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
9
+ valueLabel: z.string().min(1).optional().describe('Human name for valueKey shown in the legend and tooltip, e.g. "Sign-ups". Defaults to the humanized key.'), color: z.string().optional(),
8
10
  }).passthrough();
9
11
  export const rechartsLineChartPropsSchema = rechartsBarChartPropsSchema;
10
12
  export const rechartsPieChartPropsSchema = z.object({
@@ -31,8 +33,24 @@ export function legacyChartSpec({ kind, props }, _options = {}) {
31
33
  const parsed = (kind === 'pie' ? rechartsPieChartPropsSchema : rechartsBarChartPropsSchema).parse(props);
32
34
  return chartSpecSchema.parse({
33
35
  kind, data: parsed.data, categoryKey: kind === 'pie' ? 'name' : parsed.categoryKey,
34
- series: [{ key: kind === 'pie' ? 'value' : parsed.valueKey, ...(parsed.color !== undefined ? { color: parsed.color } : {}) }],
36
+ series: [{
37
+ key: kind === 'pie' ? 'value' : parsed.valueKey,
38
+ ...('valueLabel' in parsed && typeof parsed.valueLabel === 'string' ? { label: parsed.valueLabel } : {}),
39
+ ...(parsed.color !== undefined ? { color: parsed.color } : {}),
40
+ }],
35
41
  ...(typeof parsed.provider === 'string' ? { provider: parsed.provider } : {}),
36
42
  });
37
43
  }
44
+ /** `signups` -> `Signups`, `sign_ups`/`signUps` -> `Sign ups`. Display only; the key still addresses the data. */
45
+ function humanizeSeriesKey(key) {
46
+ const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim().toLowerCase();
47
+ return words ? words[0].toUpperCase() + words.slice(1) : key;
48
+ }
49
+ /**
50
+ * Give every series a label, so a provider's legend and tooltip never show a raw data key
51
+ * (owner, 2026-10-30: a sign-ups chart's legend read "signups"). A label the caller passed wins.
52
+ */
53
+ export function withSeriesLabels({ spec }, _options = {}) {
54
+ return { ...spec, series: spec.series.map((series) => ({ ...series, label: series.label ?? humanizeSeriesKey(series.key) })) };
55
+ }
38
56
  //# sourceMappingURL=wire.js.map
@@ -67,6 +67,8 @@ export function ConnectorsBrowser({ unlocked, providerTabs = DEFAULT_PROVIDER_TA
67
67
  onCancelAuthorization: (connectorId) => void authorization.cancelAuthorization(connectorId),
68
68
  onOpenExternalUrl: (url) => void deps.data.openExternalUrl(url),
69
69
  };
70
- return (_jsxs("div", { className: "tab-panel connectors-panel connectors-panel-embedded", children: [_jsxs("div", { className: "tab-panel-toolbar", children: [_jsx("div", { className: "toolbar-left connectors-heading", children: _jsxs("div", { children: [_jsx("h2", { children: t('Connectors') }), _jsx("p", { children: t('Connect third-party tools and services.') })] }) }), _jsxs("div", { className: "toolbar-right", children: [_jsx(ProviderTabBar, { tabs: providerTabs, selectedId: selectedProvider, onSelect: handleProviderTabSelect, ...subHandleProp({ base: agentHandle, action: 'provider' }) }), _jsx(ConnectorSearchBar, { value: filter, onChange: setFilter, disabled: !unlocked, onFocus: () => onProviderTabClick?.('search_connectors'), ...subHandleProp({ base: agentHandle, action: 'search' }) })] })] }), _jsx(ConnectorAlertList, { alerts: alerts, onOpenDetails: detail.openDetails }), catalog.loading ? (_jsx(CenteredLoader, { label: t('Loading…') })) : (_jsx(ConnectorGrid, { connectors: filteredConnectors, locked: !unlocked, hasNoResults: hasNoResults, searchQuery: filter, pendingConnectorAction: authorization.pendingConnectorAction, authorizationPending: authorization.pending, authorizationCancelFailed: authorization.cancelFailed, toolsLoaded: catalog.enriched, ...actions, onOpenDetails: detail.openDetails, ...(getCategoryLabel ? { getCategoryLabel } : {}), onClearSearch: () => setFilter(''), ...(gate ? { gate: { ...gate, onClick: handleGateClick } } : {}), ...subHandleProp({ base: agentHandle, action: 'connector' }) })), _jsx(ConnectorsDetailPane, { detail: detail, authorization: authorization, unlocked: unlocked, actions: actions, getCategoryLabel: getCategoryLabel, getDisplayableAccountLabel: getDisplayableAccountLabel, agentHandle: agentHandle })] }));
70
+ const categoryLabelProps = getCategoryLabel ? { getCategoryLabel } : {};
71
+ const gateProps = gate ? { gate: { ...gate, onClick: handleGateClick } } : {};
72
+ return (_jsxs("div", { className: "tab-panel connectors-panel connectors-panel-embedded", children: [_jsxs("div", { className: "tab-panel-toolbar", children: [_jsx("div", { className: "toolbar-left connectors-heading", children: _jsxs("div", { children: [_jsx("h2", { children: t('Connectors') }), _jsx("p", { children: t('Connect third-party tools and services.') })] }) }), _jsxs("div", { className: "toolbar-right", children: [_jsx(ProviderTabBar, { tabs: providerTabs, selectedId: selectedProvider, onSelect: handleProviderTabSelect, ...subHandleProp({ base: agentHandle, action: 'provider' }) }), _jsx(ConnectorSearchBar, { value: filter, onChange: setFilter, disabled: !unlocked, onFocus: () => onProviderTabClick?.('search_connectors'), ...subHandleProp({ base: agentHandle, action: 'search' }) })] })] }), _jsx(ConnectorAlertList, { alerts: alerts, onOpenDetails: detail.openDetails }), catalog.loading ? (_jsx(CenteredLoader, { label: t('Loading…') })) : (_jsx(ConnectorGrid, { connectors: filteredConnectors, locked: !unlocked, hasNoResults: hasNoResults, searchQuery: filter, pendingConnectorAction: authorization.pendingConnectorAction, authorizationPending: authorization.pending, authorizationCancelFailed: authorization.cancelFailed, toolsLoaded: catalog.enriched, ...actions, onOpenDetails: detail.openDetails, ...categoryLabelProps, onClearSearch: () => setFilter(''), ...gateProps, ...subHandleProp({ base: agentHandle, action: 'connector' }) })), _jsx(ConnectorsDetailPane, { detail: detail, authorization: authorization, unlocked: unlocked, actions: actions, getCategoryLabel: getCategoryLabel, getDisplayableAccountLabel: getDisplayableAccountLabel, agentHandle: agentHandle })] }));
71
73
  }
72
74
  //# sourceMappingURL=ConnectorsBrowser.js.map
@@ -176,7 +176,7 @@ export function useConnectorAuthorization(port, authPendingStorage, authBridge,
176
176
  // Poll statuses while any authorization is in flight.
177
177
  useEffect(() => {
178
178
  if (Object.keys(pending).length === 0)
179
- return;
179
+ return undefined;
180
180
  const interval = setInterval(() => {
181
181
  setPending((curr) => pruneConnectorAuthorizationPending(curr, Date.now()));
182
182
  void reloadStatuses();
@@ -42,10 +42,10 @@ export function useConnectorCatalog(port, options) {
42
42
  if (!unlocked || !port.fetchConnectorEnrichment) {
43
43
  setEnriched(false);
44
44
  setEnriching(false);
45
- return;
45
+ return undefined;
46
46
  }
47
47
  if (enriched)
48
- return;
48
+ return undefined;
49
49
  let cancelled = false;
50
50
  setEnriching(true);
51
51
  void (async () => {
@@ -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;
@@ -70,7 +70,6 @@ export const DEFAULT_AGENT_DESCRIPTIONS = {
70
70
  'cursor-agent': 'Cursor command line',
71
71
  opencode: 'Open-source agent CLI',
72
72
  qwen: 'Qwen coding CLI',
73
- copilot: 'GitHub coding CLI',
74
73
  devin: 'Cognition terminal CLI',
75
74
  kimi: 'Moonshot Kimi CLI',
76
75
  qoder: 'Alibaba coding CLI',
@@ -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. */
@@ -24,9 +24,8 @@ import type { CaptureFolderPathDropInput } from './types.js';
24
24
  */
25
25
  export declare function folderPathsFromDataTransfer(dataTransfer: DataTransfer, port: FolderPathDropPort): string[];
26
26
  /**
27
- * The text a folder drop inserts: every path, space-joined, in drop order. The one place the format
28
- * lives — a path that itself contains a space is ambiguous in this format, so any change to quoting
29
- * or separators belongs here and nowhere else.
27
+ * The text a folder drop inserts: every path, space-joined, in drop order. Quote whitespace-containing
28
+ * paths so text consumers can distinguish the path from adjacent prose or secrets.
30
29
  *
31
30
  * @complexity Time/space: O(total path length).
32
31
  */