@ai-matrx/agents 0.59.233 → 0.59.234

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.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## 0.59.233
3
+ ## 0.59.234
4
4
 
5
- UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
- Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/matrx-agents/src`.
5
+ - **The Voice row is the panel's own (owner, 2026-10-21: one settings experience).** `ModelSettingsPanel` draws `tts_voice` with `VoiceSettingControl` — one voice, or named speakers (name · voice · remove, "Add speaker", the model's `multi_speaker.max`) when the model declares `multi_speaker` — in every host, so the chat's "Model and overrides" edits voices exactly as the builder does. `SettingsWriteTarget.changeMany` (optional) writes `tts_voice` + `multi_speaker` as one change; without it the panel calls `change` per key. New `settingControl(controls, key)` in `./settings`. Removed: the panel's `renderRow` prop and the `SettingRowFrame` / `SettingRowLabel` / `formatSettingValue` exports of `./settings/react` (their one consumer, the builder's hand-drawn voice row, is gone). Guard: `settings/react/model-settings-panel.test.tsx`. Consumer action: drop `renderRow` and any `SettingRowFrame` / `SettingRowLabel` import; give a target that writes a record `changeMany`.
7
6
 
8
7
  ## 0.59.231
9
8
 
@@ -57,6 +57,7 @@ __export(settings_exports, {
57
57
  readSettingsEntry: () => readSettingsEntry,
58
58
  resolveConflicts: () => resolveConflicts,
59
59
  sanitizeSettings: () => sanitizeSettings,
60
+ settingControl: () => settingControl,
60
61
  settingLabel: () => settingLabel,
61
62
  settingStateOf: () => settingStateOf,
62
63
  startingValueFor: () => startingValueFor,
@@ -289,6 +290,9 @@ var CATCHALL_EXCLUDED = /* @__PURE__ */ new Set([
289
290
  // model now declares `tools` for the tool-support capability).
290
291
  ...UI_GATE_KEYS
291
292
  ]);
293
+ function settingControl(controls, key) {
294
+ return lookupControl(controls, key);
295
+ }
292
296
  function lookupControl(controls, key) {
293
297
  if (!controls) return null;
294
298
  if (CATCHALL_EXCLUDED.has(key)) {
@@ -229,6 +229,8 @@ interface RenderGroup {
229
229
  * NormalizedControls variant in the codebase (both are a key→ControlDefinition
230
230
  * map plus rawControls/unmappedControls). */
231
231
  type ControlsLike = Record<string, unknown> | null | undefined;
232
+ /** The model's control for one setting key (`null` when the model declares none). */
233
+ declare function settingControl(controls: ControlsLike, key: string): ControlDefinition | null;
232
234
  /**
233
235
  * Build the STANDARD settings list for the selected model.
234
236
  *
@@ -654,6 +656,11 @@ interface SettingsWriteTarget {
654
656
  read(key: string, control: ControlDefinition | null): SettingRowRead;
655
657
  /** The person edited the control. */
656
658
  change(key: string, value: unknown, control: ControlDefinition | null): void;
659
+ /**
660
+ * Several settings that are one choice, written together (the Voice row's `tts_voice` +
661
+ * `multi_speaker`). Without it the panel calls `change` once per key.
662
+ */
663
+ changeMany?(changes: Record<string, unknown>): void;
657
664
  /** The person ticked an unset row (store `startingValueFor`). */
658
665
  turnOn(key: string, control: ControlDefinition): void;
659
666
  /** The person cleared the row back to "not set". */
@@ -721,4 +728,4 @@ interface CapabilitiesWriteTarget {
721
728
  error?: string | null;
722
729
  }
723
730
 
724
- export { AUTO_MEANS_UNSET_KEYS, AUTO_VALUE, type AgentContext, type AgentSettings, type AgentSettingsEntry, type AgentSettingsState, type AgentSource, type AgentVariable, CATALOGUE_KEYS, type CapabilitiesWriteTarget, type CatalogueEntry, type CatalogueGroup, type ConflictAction, type ConflictItem, type ConflictReason, type ConflictSummary, type ControlDefinition, type ControlType, EMPTY_UI_GATES, ENUM_OFF_VALUE, type ExtraSettingKeys, type InitializeSettingsInput, type LLMParams, type ModelRowTarget, type NormalizedControls, PARAM_ALIASES, type PendingModelSwitch, type RenderGroup, type ResolutionMode, SETTINGS_CATALOGUE, type SettingRowFacts, type SettingRowRead, type SettingRowView, type SettingState, type SettingsGroupId, type SettingsModelInfo, type SettingsRow, type SettingsStore, type SettingsStoreConfig, type SettingsWriteTarget, UI_GATE_EDITABLE_KEYS, UI_GATE_KEYS, type UiGateEditableKey, type UiGateKey, type UiGates, type VariableCustomComponentRef, buildApiPayload, buildSettingsRows, computeOverrideDiff, createSettingsStore, detectConflicts, effectiveOfferingPinOf, extraSettingKeys, extractModelDefaults, gateEnabled, getActionForMode, isOffValue, isUiGateKey, isUnsetChoice, mergeEffectiveSettings, mergeVariableValues, offValueFor, parseModelControls, parseUiGates, readApiPayload, readConflictActions, readConflictSummary, readEffectiveModelId, readEffectiveSettings, readEffectiveVariableValues, readPreviewedResolution, readSettingsEntry, resolveConflicts, sanitizeSettings, settingLabel, settingStateOf, startingValueFor, withSettingState };
731
+ export { AUTO_MEANS_UNSET_KEYS, AUTO_VALUE, type AgentContext, type AgentSettings, type AgentSettingsEntry, type AgentSettingsState, type AgentSource, type AgentVariable, CATALOGUE_KEYS, type CapabilitiesWriteTarget, type CatalogueEntry, type CatalogueGroup, type ConflictAction, type ConflictItem, type ConflictReason, type ConflictSummary, type ControlDefinition, type ControlType, EMPTY_UI_GATES, ENUM_OFF_VALUE, type ExtraSettingKeys, type InitializeSettingsInput, type LLMParams, type ModelRowTarget, type NormalizedControls, PARAM_ALIASES, type PendingModelSwitch, type RenderGroup, type ResolutionMode, SETTINGS_CATALOGUE, type SettingRowFacts, type SettingRowRead, type SettingRowView, type SettingState, type SettingsGroupId, type SettingsModelInfo, type SettingsRow, type SettingsStore, type SettingsStoreConfig, type SettingsWriteTarget, UI_GATE_EDITABLE_KEYS, UI_GATE_KEYS, type UiGateEditableKey, type UiGateKey, type UiGates, type VariableCustomComponentRef, buildApiPayload, buildSettingsRows, computeOverrideDiff, createSettingsStore, detectConflicts, effectiveOfferingPinOf, extraSettingKeys, extractModelDefaults, gateEnabled, getActionForMode, isOffValue, isUiGateKey, isUnsetChoice, mergeEffectiveSettings, mergeVariableValues, offValueFor, parseModelControls, parseUiGates, readApiPayload, readConflictActions, readConflictSummary, readEffectiveModelId, readEffectiveSettings, readEffectiveVariableValues, readPreviewedResolution, readSettingsEntry, resolveConflicts, sanitizeSettings, settingControl, settingLabel, settingStateOf, startingValueFor, withSettingState };
@@ -229,6 +229,8 @@ interface RenderGroup {
229
229
  * NormalizedControls variant in the codebase (both are a key→ControlDefinition
230
230
  * map plus rawControls/unmappedControls). */
231
231
  type ControlsLike = Record<string, unknown> | null | undefined;
232
+ /** The model's control for one setting key (`null` when the model declares none). */
233
+ declare function settingControl(controls: ControlsLike, key: string): ControlDefinition | null;
232
234
  /**
233
235
  * Build the STANDARD settings list for the selected model.
234
236
  *
@@ -654,6 +656,11 @@ interface SettingsWriteTarget {
654
656
  read(key: string, control: ControlDefinition | null): SettingRowRead;
655
657
  /** The person edited the control. */
656
658
  change(key: string, value: unknown, control: ControlDefinition | null): void;
659
+ /**
660
+ * Several settings that are one choice, written together (the Voice row's `tts_voice` +
661
+ * `multi_speaker`). Without it the panel calls `change` once per key.
662
+ */
663
+ changeMany?(changes: Record<string, unknown>): void;
657
664
  /** The person ticked an unset row (store `startingValueFor`). */
658
665
  turnOn(key: string, control: ControlDefinition): void;
659
666
  /** The person cleared the row back to "not set". */
@@ -721,4 +728,4 @@ interface CapabilitiesWriteTarget {
721
728
  error?: string | null;
722
729
  }
723
730
 
724
- export { AUTO_MEANS_UNSET_KEYS, AUTO_VALUE, type AgentContext, type AgentSettings, type AgentSettingsEntry, type AgentSettingsState, type AgentSource, type AgentVariable, CATALOGUE_KEYS, type CapabilitiesWriteTarget, type CatalogueEntry, type CatalogueGroup, type ConflictAction, type ConflictItem, type ConflictReason, type ConflictSummary, type ControlDefinition, type ControlType, EMPTY_UI_GATES, ENUM_OFF_VALUE, type ExtraSettingKeys, type InitializeSettingsInput, type LLMParams, type ModelRowTarget, type NormalizedControls, PARAM_ALIASES, type PendingModelSwitch, type RenderGroup, type ResolutionMode, SETTINGS_CATALOGUE, type SettingRowFacts, type SettingRowRead, type SettingRowView, type SettingState, type SettingsGroupId, type SettingsModelInfo, type SettingsRow, type SettingsStore, type SettingsStoreConfig, type SettingsWriteTarget, UI_GATE_EDITABLE_KEYS, UI_GATE_KEYS, type UiGateEditableKey, type UiGateKey, type UiGates, type VariableCustomComponentRef, buildApiPayload, buildSettingsRows, computeOverrideDiff, createSettingsStore, detectConflicts, effectiveOfferingPinOf, extraSettingKeys, extractModelDefaults, gateEnabled, getActionForMode, isOffValue, isUiGateKey, isUnsetChoice, mergeEffectiveSettings, mergeVariableValues, offValueFor, parseModelControls, parseUiGates, readApiPayload, readConflictActions, readConflictSummary, readEffectiveModelId, readEffectiveSettings, readEffectiveVariableValues, readPreviewedResolution, readSettingsEntry, resolveConflicts, sanitizeSettings, settingLabel, settingStateOf, startingValueFor, withSettingState };
731
+ export { AUTO_MEANS_UNSET_KEYS, AUTO_VALUE, type AgentContext, type AgentSettings, type AgentSettingsEntry, type AgentSettingsState, type AgentSource, type AgentVariable, CATALOGUE_KEYS, type CapabilitiesWriteTarget, type CatalogueEntry, type CatalogueGroup, type ConflictAction, type ConflictItem, type ConflictReason, type ConflictSummary, type ControlDefinition, type ControlType, EMPTY_UI_GATES, ENUM_OFF_VALUE, type ExtraSettingKeys, type InitializeSettingsInput, type LLMParams, type ModelRowTarget, type NormalizedControls, PARAM_ALIASES, type PendingModelSwitch, type RenderGroup, type ResolutionMode, SETTINGS_CATALOGUE, type SettingRowFacts, type SettingRowRead, type SettingRowView, type SettingState, type SettingsGroupId, type SettingsModelInfo, type SettingsRow, type SettingsStore, type SettingsStoreConfig, type SettingsWriteTarget, UI_GATE_EDITABLE_KEYS, UI_GATE_KEYS, type UiGateEditableKey, type UiGateKey, type UiGates, type VariableCustomComponentRef, buildApiPayload, buildSettingsRows, computeOverrideDiff, createSettingsStore, detectConflicts, effectiveOfferingPinOf, extraSettingKeys, extractModelDefaults, gateEnabled, getActionForMode, isOffValue, isUiGateKey, isUnsetChoice, mergeEffectiveSettings, mergeVariableValues, offValueFor, parseModelControls, parseUiGates, readApiPayload, readConflictActions, readConflictSummary, readEffectiveModelId, readEffectiveSettings, readEffectiveVariableValues, readPreviewedResolution, readSettingsEntry, resolveConflicts, sanitizeSettings, settingControl, settingLabel, settingStateOf, startingValueFor, withSettingState };
@@ -223,6 +223,9 @@ var CATCHALL_EXCLUDED = /* @__PURE__ */ new Set([
223
223
  // model now declares `tools` for the tool-support capability).
224
224
  ...UI_GATE_KEYS
225
225
  ]);
226
+ function settingControl(controls, key) {
227
+ return lookupControl(controls, key);
228
+ }
226
229
  function lookupControl(controls, key) {
227
230
  if (!controls) return null;
228
231
  if (CATCHALL_EXCLUDED.has(key)) {
@@ -1032,6 +1035,7 @@ export {
1032
1035
  readSettingsEntry,
1033
1036
  resolveConflicts,
1034
1037
  sanitizeSettings,
1038
+ settingControl,
1035
1039
  settingLabel,
1036
1040
  settingStateOf,
1037
1041
  startingValueFor,
@@ -23,9 +23,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
23
23
  var react_exports = {};
24
24
  __export(react_exports, {
25
25
  ModelSettingsPanel: () => ModelSettingsPanel,
26
- SettingRowFrame: () => SettingRowFrame,
27
- SettingRowLabel: () => RowLabel,
28
- formatSettingValue: () => formatSettingValue,
29
26
  useEffectiveSettings: () => useEffectiveSettings,
30
27
  useSettingsEntry: () => useSettingsEntry,
31
28
  useSettingsSelector: () => useSettingsSelector
@@ -210,7 +207,7 @@ function readEffectiveSettings(entry) {
210
207
 
211
208
  // settings/react/ModelSettingsPanel.tsx
212
209
  var import_design_system4 = require("@ai-matrx/design-system");
213
- var import_controls3 = require("@ai-matrx/design-system/controls");
210
+ var import_controls4 = require("@ai-matrx/design-system/controls");
214
211
  var import_react2 = require("@ai-matrx/agents/models/react");
215
212
 
216
213
  // settings/settings-catalogue.ts
@@ -326,6 +323,9 @@ var CATCHALL_EXCLUDED = /* @__PURE__ */ new Set([
326
323
  // model now declares `tools` for the tool-support capability).
327
324
  ...UI_GATE_KEYS
328
325
  ]);
326
+ function settingControl(controls, key) {
327
+ return lookupControl(controls, key);
328
+ }
329
329
  function lookupControl(controls, key) {
330
330
  if (!controls) return null;
331
331
  if (CATCHALL_EXCLUDED.has(key)) {
@@ -868,29 +868,142 @@ function NumberInput({
868
868
  );
869
869
  }
870
870
 
871
- // settings/react/ModelSettingsPanel.tsx
871
+ // settings/react/VoiceSettingControl.tsx
872
+ var import_controls3 = require("@ai-matrx/design-system/controls");
873
+ var import_icons2 = require("@ai-matrx/design-system/icons");
872
874
  var import_jsx_runtime4 = require("react/jsx-runtime");
875
+ function speakersOf(value, voices) {
876
+ if (!Array.isArray(value)) return [];
877
+ return value.map((entry, index) => {
878
+ const record = entry !== null && typeof entry === "object" ? entry : {};
879
+ return {
880
+ name: typeof record.name === "string" ? record.name : `Speaker ${index + 1}`,
881
+ voice: typeof record.voice === "string" ? record.voice : voices[0] ?? ""
882
+ };
883
+ });
884
+ }
885
+ function VoiceSettingControl({ voices, value, multiSpeaker, onChange, idPrefix, disabled = false }) {
886
+ const isMulti = Array.isArray(value);
887
+ const speakers = speakersOf(value, voices);
888
+ const single = typeof value === "string" && value ? value : voices[0] ?? "";
889
+ const max = multiSpeaker?.max !== void 0 && multiSpeaker.max > 0 ? multiSpeaker.max : void 0;
890
+ const canAdd = max === void 0 || speakers.length < max;
891
+ const options = voices.map((voice) => ({ value: voice, label: voice }));
892
+ const voiceSelect = (current, label, pick) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
893
+ import_controls3.Select,
894
+ {
895
+ value: current,
896
+ options: options.some((o) => o.value === current) ? options : [{ value: current, label: current }, ...options],
897
+ onValueChange: pick,
898
+ "aria-label": label,
899
+ disabled,
900
+ className: "min-w-0"
901
+ }
902
+ );
903
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 flex-col gap-1", "data-voice-control": "", children: [
904
+ multiSpeaker || isMulti ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
905
+ import_controls3.SegmentedControl,
906
+ {
907
+ "aria-label": "Voices",
908
+ value: isMulti ? "multi" : "single",
909
+ onValueChange: (next) => {
910
+ if (next === "multi" && !isMulti) {
911
+ onChange(
912
+ [
913
+ { name: "Speaker 1", voice: single },
914
+ { name: "Speaker 2", voice: voices[1] ?? single }
915
+ ],
916
+ true
917
+ );
918
+ } else if (next === "single" && isMulti) {
919
+ onChange(speakers[0]?.voice || voices[0] || "", false);
920
+ }
921
+ },
922
+ data: [
923
+ { value: "single", label: "One voice", disabled },
924
+ { value: "multi", label: "Speakers", disabled: disabled || !multiSpeaker }
925
+ ]
926
+ }
927
+ ) : null,
928
+ !isMulti ? voiceSelect(single, "Voice", (next) => onChange(next, false)) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
929
+ speakers.map((speaker, index) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-w-0 items-center gap-1", "data-speaker": index, children: [
930
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
931
+ import_controls3.Field,
932
+ {
933
+ id: `${idPrefix}-speaker-${index}`,
934
+ "aria-label": `Speaker ${index + 1} name`,
935
+ value: speaker.name,
936
+ onChange: (event) => onChange(
937
+ speakers.map((s, i) => i === index ? { ...s, name: event.target.value } : s),
938
+ true
939
+ ),
940
+ disabled,
941
+ className: "min-w-0 flex-1"
942
+ }
943
+ ),
944
+ voiceSelect(
945
+ speaker.voice,
946
+ `Speaker ${index + 1} voice`,
947
+ (next) => onChange(
948
+ speakers.map((s, i) => i === index ? { ...s, voice: next } : s),
949
+ true
950
+ )
951
+ ),
952
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
953
+ import_controls3.Button,
954
+ {
955
+ variant: "quiet",
956
+ removes: true,
957
+ icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_icons2.XIcon, {}),
958
+ onClick: () => onChange(speakers.filter((_, i) => i !== index), true),
959
+ disabled: disabled || speakers.length <= 2,
960
+ "aria-label": `Remove speaker ${index + 1}`,
961
+ title: "Remove speaker"
962
+ }
963
+ )
964
+ ] }, index)),
965
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
966
+ import_controls3.Button,
967
+ {
968
+ variant: "quiet",
969
+ icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_icons2.PlusIcon, {}),
970
+ onClick: () => onChange([...speakers, { name: `Speaker ${speakers.length + 1}`, voice: voices[0] ?? "" }], true),
971
+ disabled: disabled || !canAdd,
972
+ title: canAdd ? "Add a speaker" : `This model takes up to ${max} speakers`,
973
+ children: "Add speaker"
974
+ }
975
+ ) })
976
+ ] })
977
+ ] });
978
+ }
979
+
980
+ // settings/react/ModelSettingsPanel.tsx
981
+ var import_jsx_runtime5 = require("react/jsx-runtime");
873
982
  function StatusGlyph({ issue, source }) {
874
983
  const title = issue ? issue.message : source ? `Valid \xB7 ${source}` : "Valid";
875
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
984
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
876
985
  "span",
877
986
  {
878
987
  className: (0, import_cn.cn)("inline-flex h-7 w-6 shrink-0 items-center justify-center", issue ? "text-warning" : "text-success"),
879
988
  role: "img",
880
989
  "aria-label": title,
881
990
  title,
882
- children: issue ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TriangleAlertGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CircleCheckGlyph, { className: "h-3.5 w-3.5" })
991
+ children: issue ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TriangleAlertGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CircleCheckGlyph, { className: "h-3.5 w-3.5" })
883
992
  }
884
993
  );
885
994
  }
886
- function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
995
+ function changeTogether(target, changes, controls) {
996
+ if (target.changeMany) target.changeMany(changes);
997
+ else for (const [key, value] of Object.entries(changes)) target.change(key, value, controls[key] ?? null);
998
+ }
999
+ function ModelSettingRow({ row, control, target, idPrefix, disabled, multiSpeaker }) {
887
1000
  const read = target.read(row.key, control);
888
1001
  const resetLabel = target.resetLabel ?? "Reset to agent";
889
- const reset = read.changedHere && target.resetToInherited ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
890
- import_controls3.Button,
1002
+ const reset = read.changedHere && target.resetToInherited ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1003
+ import_controls4.Button,
891
1004
  {
892
1005
  variant: "quiet",
893
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RotateCcwGlyph, {}),
1006
+ icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RotateCcwGlyph, {}),
894
1007
  onClick: () => target.resetToInherited?.(row.key),
895
1008
  disabled,
896
1009
  "aria-label": `${resetLabel}: ${row.label}`,
@@ -899,20 +1012,20 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
899
1012
  }
900
1013
  ) : null;
901
1014
  if (read.state === "bound") {
902
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1015
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
903
1016
  SettingRowFrame,
904
1017
  {
905
1018
  rowKey: row.key,
906
1019
  state: "bound",
907
- leading: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_design_system4.Checkbox, { checked: true, disabled: true, "aria-label": `${row.label} is a run input` }),
908
- label: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowLabel, { children: row.label }),
909
- actions: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1020
+ leading: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system4.Checkbox, { checked: true, disabled: true, "aria-label": `${row.label} is a run input` }),
1021
+ label: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RowLabel, { children: row.label }),
1022
+ actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
910
1023
  reset,
911
- target.unbind ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
912
- import_controls3.Button,
1024
+ target.unbind ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1025
+ import_controls4.Button,
913
1026
  {
914
1027
  variant: "quiet",
915
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UnlinkGlyph, {}),
1028
+ icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(UnlinkGlyph, {}),
916
1029
  onClick: () => target.unbind?.(row.key, control),
917
1030
  disabled,
918
1031
  "aria-label": `Make ${row.label} a fixed setting`,
@@ -920,9 +1033,9 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
920
1033
  }
921
1034
  ) : null
922
1035
  ] }),
923
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex min-h-7 min-w-0 items-center gap-2", "data-bound-control": row.key, children: [
924
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_controls3.Chip, { tone: "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(VariableGlyph, {}), label: `{{${read.variableName}}}` }),
925
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate type-meta text-muted-foreground", children: read.value === void 0 || read.value === null || read.value === "" ? "Model default" : formatSettingValue(read.value) })
1036
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex min-h-7 min-w-0 items-center gap-2", "data-bound-control": row.key, children: [
1037
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls4.Chip, { tone: "primary", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(VariableGlyph, {}), label: `{{${read.variableName}}}` }),
1038
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "truncate type-meta text-muted-foreground", children: read.value === void 0 || read.value === null || read.value === "" ? "Model default" : formatSettingValue(read.value) })
926
1039
  ] })
927
1040
  }
928
1041
  );
@@ -932,12 +1045,12 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
932
1045
  const isOff = isSet && isOffValue(value, control);
933
1046
  const checkboxId = `${idPrefix}-setting-${row.key}`;
934
1047
  const canBind = Boolean(target.bind) && (target.canBind?.(row.key) ?? true);
935
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1048
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
936
1049
  SettingRowFrame,
937
1050
  {
938
1051
  rowKey: row.key,
939
1052
  state: isOff ? "off" : read.state,
940
- leading: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1053
+ leading: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
941
1054
  import_design_system4.Checkbox,
942
1055
  {
943
1056
  id: checkboxId,
@@ -946,14 +1059,14 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
946
1059
  onCheckedChange: (next) => next === true ? target.turnOn(row.key, control) : target.clear(row.key)
947
1060
  }
948
1061
  ),
949
- label: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowLabel, { htmlFor: checkboxId, muted: !isSet, children: row.label }),
950
- actions: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
951
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusGlyph, { issue: read.issue, source: read.source }),
952
- canBind ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
953
- import_controls3.Button,
1062
+ label: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RowLabel, { htmlFor: checkboxId, muted: !isSet, children: row.label }),
1063
+ actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1064
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatusGlyph, { issue: read.issue, source: read.source }),
1065
+ canBind ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1066
+ import_controls4.Button,
954
1067
  {
955
1068
  variant: "quiet",
956
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(VariableGlyph, {}),
1069
+ icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(VariableGlyph, {}),
957
1070
  onClick: () => target.bind?.(row.key, control),
958
1071
  disabled,
959
1072
  "aria-label": `Make ${row.label} a run input`,
@@ -961,11 +1074,11 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
961
1074
  "data-bind-control": row.key
962
1075
  }
963
1076
  ) : null,
964
- isSet && read.issue?.fixable && target.fix ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
965
- import_controls3.Button,
1077
+ isSet && read.issue?.fixable && target.fix ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1078
+ import_controls4.Button,
966
1079
  {
967
1080
  variant: "quiet",
968
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(WrenchGlyph, {}),
1081
+ icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WrenchGlyph, {}),
969
1082
  onClick: () => target.fix?.(row.key),
970
1083
  disabled,
971
1084
  "aria-label": `Fix ${row.label} automatically`,
@@ -973,12 +1086,12 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
973
1086
  }
974
1087
  ) : null,
975
1088
  reset,
976
- isSet ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
977
- import_controls3.Button,
1089
+ isSet ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1090
+ import_controls4.Button,
978
1091
  {
979
1092
  variant: "quiet",
980
1093
  removes: true,
981
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Trash2Glyph, {}),
1094
+ icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Trash2Glyph, {}),
982
1095
  onClick: () => target.clear(row.key),
983
1096
  disabled,
984
1097
  "aria-label": `Clear ${row.label} to not set`,
@@ -987,8 +1100,18 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
987
1100
  }
988
1101
  ) : null
989
1102
  ] }),
990
- children: isSet ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
991
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1103
+ children: isSet && row.key === "tts_voice" && control.enum?.length && (multiSpeaker || Array.isArray(value)) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1104
+ VoiceSettingControl,
1105
+ {
1106
+ voices: control.enum.map(String),
1107
+ value,
1108
+ multiSpeaker: multiSpeaker ? { max: multiSpeaker.max } : null,
1109
+ onChange: (voice, multi) => changeTogether(target, { tts_voice: voice, multi_speaker: multi }, { tts_voice: control, multi_speaker: multiSpeaker }),
1110
+ idPrefix: `${idPrefix}-voice`,
1111
+ disabled
1112
+ }
1113
+ ) : isSet ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1114
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
992
1115
  SettingControlInput,
993
1116
  {
994
1117
  settingKey: row.key,
@@ -999,40 +1122,40 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
999
1122
  id: `${idPrefix}-control-${row.key}`
1000
1123
  }
1001
1124
  ),
1002
- read.issue ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "pt-1 type-meta text-warning-ink", children: read.issue.message }) : null
1003
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowText, { children: read.modelDefault !== void 0 && read.modelDefault !== null ? `Not set \xB7 Model default: ${formatSettingValue(read.modelDefault)}` : "Not set" })
1125
+ read.issue ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "pt-1 type-meta text-warning-ink", children: read.issue.message }) : null
1126
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RowText, { children: read.modelDefault !== void 0 && read.modelDefault !== null ? `Not set \xB7 Model default: ${formatSettingValue(read.modelDefault)}` : "Not set" })
1004
1127
  }
1005
1128
  );
1006
1129
  }
1007
1130
  function ExtraSettingRow({ rowKey, label, value, kind, onClear, onReset, resetLabel = "Reset to agent", disabled = false }) {
1008
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1131
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1009
1132
  SettingRowFrame,
1010
1133
  {
1011
1134
  rowKey,
1012
1135
  state: kind,
1013
- label: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowLabel, { children: label }),
1014
- actions: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1015
- kind === "translated" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_controls3.Chip, { tone: "sky", label: "Translated", title: "Kept as set and sent as this model's closest equivalent" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_controls3.Chip, { tone: "warning", label: "Not on this model", title: "This model has no such setting" }),
1016
- onReset ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_controls3.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RotateCcwGlyph, {}), onClick: onReset, disabled, "aria-label": `${resetLabel}: ${label}`, title: resetLabel, "data-setting-reset": rowKey }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_controls3.Button, { variant: "quiet", removes: true, icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Trash2Glyph, {}), onClick: onClear, disabled, "aria-label": `Clear ${label}`, title: "Clear to not set", "data-setting-clear": rowKey })
1136
+ label: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RowLabel, { children: label }),
1137
+ actions: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1138
+ kind === "translated" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls4.Chip, { tone: "sky", label: "Translated", title: "Kept as set and sent as this model's closest equivalent" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls4.Chip, { tone: "warning", label: "Not on this model", title: "This model has no such setting" }),
1139
+ onReset ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls4.Button, { variant: "quiet", icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RotateCcwGlyph, {}), onClick: onReset, disabled, "aria-label": `${resetLabel}: ${label}`, title: resetLabel, "data-setting-reset": rowKey }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_controls4.Button, { variant: "quiet", removes: true, icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Trash2Glyph, {}), onClick: onClear, disabled, "aria-label": `Clear ${label}`, title: "Clear to not set", "data-setting-clear": rowKey })
1017
1140
  ] }),
1018
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowText, { mono: value !== void 0 && value !== null, children: value === void 0 || value === null ? "Not set" : isOffValue(value, null) ? "Off" : formatSettingValue(value) })
1141
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RowText, { mono: value !== void 0 && value !== null, children: value === void 0 || value === null ? "Not set" : isOffValue(value, null) ? "Off" : formatSettingValue(value) })
1019
1142
  }
1020
1143
  );
1021
1144
  }
1022
1145
  function ModelRow({ model, resetLabel, idPrefix, disabled }) {
1023
1146
  const off = disabled || model.disabled === true;
1024
1147
  const oneMessageId = `${idPrefix}-model-one-message`;
1025
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1148
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1026
1149
  SettingRowFrame,
1027
1150
  {
1028
1151
  rowKey: "model",
1029
1152
  state: model.value ? "set" : "unset",
1030
- label: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RowLabel, { children: "Model" }),
1031
- actions: model.changedHere && model.resetToInherited ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1032
- import_controls3.Button,
1153
+ label: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RowLabel, { children: "Model" }),
1154
+ actions: model.changedHere && model.resetToInherited ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1155
+ import_controls4.Button,
1033
1156
  {
1034
1157
  variant: "quiet",
1035
- icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RotateCcwGlyph, {}),
1158
+ icon: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RotateCcwGlyph, {}),
1036
1159
  onClick: () => model.resetToInherited?.(),
1037
1160
  disabled: off,
1038
1161
  "aria-label": `${resetLabel}: Model`,
@@ -1040,8 +1163,8 @@ function ModelRow({ model, resetLabel, idPrefix, disabled }) {
1040
1163
  "data-setting-reset": "model"
1041
1164
  }
1042
1165
  ) : null,
1043
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex min-h-7 min-w-0 flex-wrap items-center gap-x-2", children: [
1044
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1166
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex min-h-7 min-w-0 flex-wrap items-center gap-x-2", children: [
1167
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1045
1168
  import_react2.ModelListDropdown,
1046
1169
  {
1047
1170
  "aria-label": "Model",
@@ -1058,9 +1181,9 @@ function ModelRow({ model, resetLabel, idPrefix, disabled }) {
1058
1181
  ...model.disabledTitle ? { disabledTitle: model.disabledTitle } : {}
1059
1182
  }
1060
1183
  ),
1061
- model.oneMessage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { htmlFor: oneMessageId, className: "flex min-h-7 items-center gap-1 type-secondary text-muted-foreground", title: "Use this model for the next message, then go back", children: [
1062
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1063
- import_controls3.Switch,
1184
+ model.oneMessage ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { htmlFor: oneMessageId, className: "flex min-h-7 items-center gap-1 type-secondary text-muted-foreground", title: "Use this model for the next message, then go back", children: [
1185
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1186
+ import_controls4.Switch,
1064
1187
  {
1065
1188
  id: oneMessageId,
1066
1189
  checked: model.oneMessage.on,
@@ -1081,7 +1204,6 @@ function ModelSettingsPanel({
1081
1204
  idPrefix,
1082
1205
  model,
1083
1206
  notices,
1084
- renderRow,
1085
1207
  capabilities,
1086
1208
  emptyNote,
1087
1209
  disabled = false
@@ -1089,7 +1211,8 @@ function ModelSettingsPanel({
1089
1211
  const groups = controls ? buildSettingsRows(controls, values).filter((group) => group.rows.length > 0) : [];
1090
1212
  const extras = extraSettingKeys(controls, values, target.changedKeys);
1091
1213
  const resetLabel = target.resetLabel ?? "Reset to agent";
1092
- const extraRow = (key, kind) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1214
+ const multiSpeaker = settingControl(controls, "multi_speaker");
1215
+ const extraRow = (key, kind) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1093
1216
  ExtraSettingRow,
1094
1217
  {
1095
1218
  rowKey: key,
@@ -1103,29 +1226,28 @@ function ModelSettingsPanel({
1103
1226
  },
1104
1227
  key
1105
1228
  );
1106
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { "data-model-settings-panel": "", className: "@container flex min-w-0 flex-col", children: [
1107
- model ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ModelRow, { model, resetLabel, idPrefix, disabled }) : null,
1229
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { "data-model-settings-panel": "", className: "@container flex min-w-0 flex-col", children: [
1230
+ model ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ModelRow, { model, resetLabel, idPrefix, disabled }) : null,
1108
1231
  notices,
1109
1232
  extras.unsupported.map((key) => extraRow(key, "unsupported")),
1110
- extras.translated.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SettingsGroupSection, { id: "translated", title: "Translated for this model", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { "data-translated-settings": "", children: extras.translated.map((key) => extraRow(key, "translated")) }) }) : null,
1111
- groups.length === 0 && emptyNote ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "py-2 type-secondary text-muted-foreground", children: emptyNote }) : null,
1112
- groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SettingsGroupSection, { id: group.id, title: group.label || void 0, children: group.rows.map((row) => {
1113
- const custom = renderRow?.(row);
1114
- if (custom !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: custom }, row.key);
1233
+ extras.translated.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SettingsGroupSection, { id: "translated", title: "Translated for this model", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "data-translated-settings": "", children: extras.translated.map((key) => extraRow(key, "translated")) }) }) : null,
1234
+ groups.length === 0 && emptyNote ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "py-2 type-secondary text-muted-foreground", children: emptyNote }) : null,
1235
+ groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SettingsGroupSection, { id: group.id, title: group.label || void 0, children: group.rows.map((row) => {
1115
1236
  if (!row.control) return null;
1116
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1237
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1117
1238
  ModelSettingRow,
1118
1239
  {
1119
1240
  row,
1120
1241
  control: row.control,
1121
1242
  target,
1122
1243
  idPrefix,
1123
- disabled
1244
+ disabled,
1245
+ multiSpeaker
1124
1246
  },
1125
1247
  row.key
1126
1248
  );
1127
1249
  }) }, group.id)),
1128
- capabilities ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1250
+ capabilities ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1129
1251
  InputCapabilitiesEditor,
1130
1252
  {
1131
1253
  values: capabilities.values,
@@ -167,49 +167,6 @@ interface SettingsStore {
167
167
  cancelModelSwitch(agentId: string): void;
168
168
  }
169
169
 
170
- /**
171
- * Model Settings Catalog — the single source of truth for the STANDARD
172
- * settings list, and the one chokepoint that decides which keys it contains.
173
- *
174
- * ── The model (two distinct surfaces) ────────────────────────────────────────
175
- * 1. STANDARD list — ONLY the keys the selected model explicitly declares in
176
- * its `controls` config (i.e. the parameters this model actually supports).
177
- * `buildSettingsRows()` returns exactly these, grouped + ordered. Catalog
178
- * keys the model does not declare are NOT shown here.
179
- * 2. CAUTION surface — settings the agent has SET that the model does NOT
180
- * support. These are never hidden: they are flagged as "not valid for this
181
- * model" and made repairable by the validation layer (the IssueTable, fed
182
- * by the `unsupported_by_model` rule). They do NOT belong in the standard
183
- * list — that is exactly the "showing all possible keys" bug.
184
- *
185
- * The catalog exists so the standard list's labels, grouping and order live
186
- * in ONE place (previously copy-pasted as `textModelSettings` /
187
- * `booleanSettings` / ... into three components, each filtering by
188
- * `getControl(key)` inline — which drifted and regressed). Components map over
189
- * `buildSettingsRows()`; they must not filter by model themselves (ESLint
190
- * enforces). The original recurring bug — set values silently dropped when a
191
- * model didn't support them — is fixed by the CAUTION surface, not by dumping
192
- * every key into the standard list.
193
- *
194
- * Guardrail: `settings-catalogue.test.ts` asserts the standard list contains
195
- * ONLY supported keys (never the full catalog), and `unsupported-by-model.
196
- * test.ts` asserts set-but-unsupported keys surface in the caution layer.
197
- */
198
-
199
- type SettingsGroupId = "text" | "featureFlags" | "imageVideo" | "audio" | "other";
200
- interface SettingsRow {
201
- key: string;
202
- label: string;
203
- group: SettingsGroupId;
204
- /** The model's control definition for this key. Standard rows are always
205
- * supported, so this is present; typed nullable for consumer convenience. */
206
- control: ControlDefinition | null;
207
- /** Always true for rows returned by buildSettingsRows (standard = supported). */
208
- supported: boolean;
209
- /** True when the current settings hold a non-null value for this key. */
210
- hasValue: boolean;
211
- }
212
-
213
170
  /**
214
171
  * UI Gates — canonical model-gated UI flags.
215
172
  *
@@ -303,6 +260,11 @@ interface SettingsWriteTarget {
303
260
  read(key: string, control: ControlDefinition | null): SettingRowRead;
304
261
  /** The person edited the control. */
305
262
  change(key: string, value: unknown, control: ControlDefinition | null): void;
263
+ /**
264
+ * Several settings that are one choice, written together (the Voice row's `tts_voice` +
265
+ * `multi_speaker`). Without it the panel calls `change` once per key.
266
+ */
267
+ changeMany?(changes: Record<string, unknown>): void;
306
268
  /** The person ticked an unset row (store `startingValueFor`). */
307
269
  turnOn(key: string, control: ControlDefinition): void;
308
270
  /** The person cleared the row back to "not set". */
@@ -383,41 +345,13 @@ interface ModelSettingsPanelProps {
383
345
  model?: ModelRowTarget | undefined;
384
346
  /** Drawn between the model row and the groups: notices, the issue list. */
385
347
  notices?: ReactNode;
386
- /** A row the host draws itself (the multi-speaker voice editor); `undefined` = the standard row. */
387
- renderRow?: (row: SettingsRow) => ReactNode | undefined;
388
348
  /** The input capabilities group, under the settings groups. */
389
349
  capabilities?: CapabilitiesWriteTarget | undefined;
390
350
  /** Said where the rows would be when the model declares none (or no model is chosen). */
391
351
  emptyNote?: ReactNode;
392
352
  disabled?: boolean;
393
353
  }
394
- declare function ModelSettingsPanel({ controls, values, target, idPrefix, model, notices, renderRow, capabilities, emptyNote, disabled, }: ModelSettingsPanelProps): React.JSX.Element;
395
-
396
- /** A catalog value as the person reads it ("On", "auto", "1024"). */
397
- declare function formatSettingValue(value: unknown): string;
398
- interface SettingRowFrameProps {
399
- /** `data-setting-row` — the setting key (or a host row name such as "model"). */
400
- rowKey: string;
401
- /** `data-setting-state` — unset | set | off | bound | translated | … */
402
- state?: string;
403
- /** The 16px leading column: the row's checkbox, or an icon. */
404
- leading?: ReactNode;
405
- label: ReactNode;
406
- /** The control, or the row's value text. */
407
- children?: ReactNode;
408
- /** Fixed-width icons on the right: status, then actions. */
409
- actions?: ReactNode;
410
- }
411
- /**
412
- * The one row geometry every line of the panel uses — setting rows, the model row, the
413
- * translated rows, the voice row and the input capabilities — so their edges line up.
414
- */
415
- declare function SettingRowFrame({ rowKey, state, leading, label, children, actions }: SettingRowFrameProps): React.JSX.Element;
416
- declare function RowLabel({ htmlFor, muted, children }: {
417
- htmlFor?: string;
418
- muted?: boolean;
419
- children: ReactNode;
420
- }): React.JSX.Element;
354
+ declare function ModelSettingsPanel({ controls, values, target, idPrefix, model, notices, capabilities, emptyNote, disabled, }: ModelSettingsPanelProps): React.JSX.Element;
421
355
 
422
356
  /** Subscribe to a slice of a settings store. `select` must return a stable value for unchanged state. */
423
357
  declare function useSettingsSelector<T>(store: SettingsStore, select: (state: AgentSettingsState) => T): T;
@@ -426,4 +360,4 @@ declare function useSettingsEntry(store: SettingsStore, agentId: string | null |
426
360
  /** Defaults merged with overrides for one agent; recomputed only when the entry changes. */
427
361
  declare function useEffectiveSettings(store: SettingsStore, agentId: string | null | undefined): Partial<AgentSettings>;
428
362
 
429
- export { ModelSettingsPanel, type ModelSettingsPanelProps, SettingRowFrame, type SettingRowFrameProps, RowLabel as SettingRowLabel, formatSettingValue, useEffectiveSettings, useSettingsEntry, useSettingsSelector };
363
+ export { ModelSettingsPanel, type ModelSettingsPanelProps, useEffectiveSettings, useSettingsEntry, useSettingsSelector };
@@ -167,49 +167,6 @@ interface SettingsStore {
167
167
  cancelModelSwitch(agentId: string): void;
168
168
  }
169
169
 
170
- /**
171
- * Model Settings Catalog — the single source of truth for the STANDARD
172
- * settings list, and the one chokepoint that decides which keys it contains.
173
- *
174
- * ── The model (two distinct surfaces) ────────────────────────────────────────
175
- * 1. STANDARD list — ONLY the keys the selected model explicitly declares in
176
- * its `controls` config (i.e. the parameters this model actually supports).
177
- * `buildSettingsRows()` returns exactly these, grouped + ordered. Catalog
178
- * keys the model does not declare are NOT shown here.
179
- * 2. CAUTION surface — settings the agent has SET that the model does NOT
180
- * support. These are never hidden: they are flagged as "not valid for this
181
- * model" and made repairable by the validation layer (the IssueTable, fed
182
- * by the `unsupported_by_model` rule). They do NOT belong in the standard
183
- * list — that is exactly the "showing all possible keys" bug.
184
- *
185
- * The catalog exists so the standard list's labels, grouping and order live
186
- * in ONE place (previously copy-pasted as `textModelSettings` /
187
- * `booleanSettings` / ... into three components, each filtering by
188
- * `getControl(key)` inline — which drifted and regressed). Components map over
189
- * `buildSettingsRows()`; they must not filter by model themselves (ESLint
190
- * enforces). The original recurring bug — set values silently dropped when a
191
- * model didn't support them — is fixed by the CAUTION surface, not by dumping
192
- * every key into the standard list.
193
- *
194
- * Guardrail: `settings-catalogue.test.ts` asserts the standard list contains
195
- * ONLY supported keys (never the full catalog), and `unsupported-by-model.
196
- * test.ts` asserts set-but-unsupported keys surface in the caution layer.
197
- */
198
-
199
- type SettingsGroupId = "text" | "featureFlags" | "imageVideo" | "audio" | "other";
200
- interface SettingsRow {
201
- key: string;
202
- label: string;
203
- group: SettingsGroupId;
204
- /** The model's control definition for this key. Standard rows are always
205
- * supported, so this is present; typed nullable for consumer convenience. */
206
- control: ControlDefinition | null;
207
- /** Always true for rows returned by buildSettingsRows (standard = supported). */
208
- supported: boolean;
209
- /** True when the current settings hold a non-null value for this key. */
210
- hasValue: boolean;
211
- }
212
-
213
170
  /**
214
171
  * UI Gates — canonical model-gated UI flags.
215
172
  *
@@ -303,6 +260,11 @@ interface SettingsWriteTarget {
303
260
  read(key: string, control: ControlDefinition | null): SettingRowRead;
304
261
  /** The person edited the control. */
305
262
  change(key: string, value: unknown, control: ControlDefinition | null): void;
263
+ /**
264
+ * Several settings that are one choice, written together (the Voice row's `tts_voice` +
265
+ * `multi_speaker`). Without it the panel calls `change` once per key.
266
+ */
267
+ changeMany?(changes: Record<string, unknown>): void;
306
268
  /** The person ticked an unset row (store `startingValueFor`). */
307
269
  turnOn(key: string, control: ControlDefinition): void;
308
270
  /** The person cleared the row back to "not set". */
@@ -383,41 +345,13 @@ interface ModelSettingsPanelProps {
383
345
  model?: ModelRowTarget | undefined;
384
346
  /** Drawn between the model row and the groups: notices, the issue list. */
385
347
  notices?: ReactNode;
386
- /** A row the host draws itself (the multi-speaker voice editor); `undefined` = the standard row. */
387
- renderRow?: (row: SettingsRow) => ReactNode | undefined;
388
348
  /** The input capabilities group, under the settings groups. */
389
349
  capabilities?: CapabilitiesWriteTarget | undefined;
390
350
  /** Said where the rows would be when the model declares none (or no model is chosen). */
391
351
  emptyNote?: ReactNode;
392
352
  disabled?: boolean;
393
353
  }
394
- declare function ModelSettingsPanel({ controls, values, target, idPrefix, model, notices, renderRow, capabilities, emptyNote, disabled, }: ModelSettingsPanelProps): React.JSX.Element;
395
-
396
- /** A catalog value as the person reads it ("On", "auto", "1024"). */
397
- declare function formatSettingValue(value: unknown): string;
398
- interface SettingRowFrameProps {
399
- /** `data-setting-row` — the setting key (or a host row name such as "model"). */
400
- rowKey: string;
401
- /** `data-setting-state` — unset | set | off | bound | translated | … */
402
- state?: string;
403
- /** The 16px leading column: the row's checkbox, or an icon. */
404
- leading?: ReactNode;
405
- label: ReactNode;
406
- /** The control, or the row's value text. */
407
- children?: ReactNode;
408
- /** Fixed-width icons on the right: status, then actions. */
409
- actions?: ReactNode;
410
- }
411
- /**
412
- * The one row geometry every line of the panel uses — setting rows, the model row, the
413
- * translated rows, the voice row and the input capabilities — so their edges line up.
414
- */
415
- declare function SettingRowFrame({ rowKey, state, leading, label, children, actions }: SettingRowFrameProps): React.JSX.Element;
416
- declare function RowLabel({ htmlFor, muted, children }: {
417
- htmlFor?: string;
418
- muted?: boolean;
419
- children: ReactNode;
420
- }): React.JSX.Element;
354
+ declare function ModelSettingsPanel({ controls, values, target, idPrefix, model, notices, capabilities, emptyNote, disabled, }: ModelSettingsPanelProps): React.JSX.Element;
421
355
 
422
356
  /** Subscribe to a slice of a settings store. `select` must return a stable value for unchanged state. */
423
357
  declare function useSettingsSelector<T>(store: SettingsStore, select: (state: AgentSettingsState) => T): T;
@@ -426,4 +360,4 @@ declare function useSettingsEntry(store: SettingsStore, agentId: string | null |
426
360
  /** Defaults merged with overrides for one agent; recomputed only when the entry changes. */
427
361
  declare function useEffectiveSettings(store: SettingsStore, agentId: string | null | undefined): Partial<AgentSettings>;
428
362
 
429
- export { ModelSettingsPanel, type ModelSettingsPanelProps, SettingRowFrame, type SettingRowFrameProps, RowLabel as SettingRowLabel, formatSettingValue, useEffectiveSettings, useSettingsEntry, useSettingsSelector };
363
+ export { ModelSettingsPanel, type ModelSettingsPanelProps, useEffectiveSettings, useSettingsEntry, useSettingsSelector };
@@ -181,7 +181,7 @@ function readEffectiveSettings(entry) {
181
181
 
182
182
  // settings/react/ModelSettingsPanel.tsx
183
183
  import { Checkbox as Checkbox2 } from "@ai-matrx/design-system";
184
- import { Button as Button2, Chip, Switch as Switch2 } from "@ai-matrx/design-system/controls";
184
+ import { Button as Button3, Chip, Switch as Switch2 } from "@ai-matrx/design-system/controls";
185
185
  import { ModelListDropdown } from "@ai-matrx/agents/models/react";
186
186
 
187
187
  // settings/settings-catalogue.ts
@@ -297,6 +297,9 @@ var CATCHALL_EXCLUDED = /* @__PURE__ */ new Set([
297
297
  // model now declares `tools` for the tool-support capability).
298
298
  ...UI_GATE_KEYS
299
299
  ]);
300
+ function settingControl(controls, key) {
301
+ return lookupControl(controls, key);
302
+ }
300
303
  function lookupControl(controls, key) {
301
304
  if (!controls) return null;
302
305
  if (CATCHALL_EXCLUDED.has(key)) {
@@ -865,29 +868,142 @@ function NumberInput({
865
868
  );
866
869
  }
867
870
 
868
- // settings/react/ModelSettingsPanel.tsx
871
+ // settings/react/VoiceSettingControl.tsx
872
+ import { Button as Button2, Field, SegmentedControl, Select as Select2 } from "@ai-matrx/design-system/controls";
873
+ import { PlusIcon, XIcon } from "@ai-matrx/design-system/icons";
869
874
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
875
+ function speakersOf(value, voices) {
876
+ if (!Array.isArray(value)) return [];
877
+ return value.map((entry, index) => {
878
+ const record = entry !== null && typeof entry === "object" ? entry : {};
879
+ return {
880
+ name: typeof record.name === "string" ? record.name : `Speaker ${index + 1}`,
881
+ voice: typeof record.voice === "string" ? record.voice : voices[0] ?? ""
882
+ };
883
+ });
884
+ }
885
+ function VoiceSettingControl({ voices, value, multiSpeaker, onChange, idPrefix, disabled = false }) {
886
+ const isMulti = Array.isArray(value);
887
+ const speakers = speakersOf(value, voices);
888
+ const single = typeof value === "string" && value ? value : voices[0] ?? "";
889
+ const max = multiSpeaker?.max !== void 0 && multiSpeaker.max > 0 ? multiSpeaker.max : void 0;
890
+ const canAdd = max === void 0 || speakers.length < max;
891
+ const options = voices.map((voice) => ({ value: voice, label: voice }));
892
+ const voiceSelect = (current, label, pick) => /* @__PURE__ */ jsx4(
893
+ Select2,
894
+ {
895
+ value: current,
896
+ options: options.some((o) => o.value === current) ? options : [{ value: current, label: current }, ...options],
897
+ onValueChange: pick,
898
+ "aria-label": label,
899
+ disabled,
900
+ className: "min-w-0"
901
+ }
902
+ );
903
+ return /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-col gap-1", "data-voice-control": "", children: [
904
+ multiSpeaker || isMulti ? /* @__PURE__ */ jsx4(
905
+ SegmentedControl,
906
+ {
907
+ "aria-label": "Voices",
908
+ value: isMulti ? "multi" : "single",
909
+ onValueChange: (next) => {
910
+ if (next === "multi" && !isMulti) {
911
+ onChange(
912
+ [
913
+ { name: "Speaker 1", voice: single },
914
+ { name: "Speaker 2", voice: voices[1] ?? single }
915
+ ],
916
+ true
917
+ );
918
+ } else if (next === "single" && isMulti) {
919
+ onChange(speakers[0]?.voice || voices[0] || "", false);
920
+ }
921
+ },
922
+ data: [
923
+ { value: "single", label: "One voice", disabled },
924
+ { value: "multi", label: "Speakers", disabled: disabled || !multiSpeaker }
925
+ ]
926
+ }
927
+ ) : null,
928
+ !isMulti ? voiceSelect(single, "Voice", (next) => onChange(next, false)) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
929
+ speakers.map((speaker, index) => /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 items-center gap-1", "data-speaker": index, children: [
930
+ /* @__PURE__ */ jsx4(
931
+ Field,
932
+ {
933
+ id: `${idPrefix}-speaker-${index}`,
934
+ "aria-label": `Speaker ${index + 1} name`,
935
+ value: speaker.name,
936
+ onChange: (event) => onChange(
937
+ speakers.map((s, i) => i === index ? { ...s, name: event.target.value } : s),
938
+ true
939
+ ),
940
+ disabled,
941
+ className: "min-w-0 flex-1"
942
+ }
943
+ ),
944
+ voiceSelect(
945
+ speaker.voice,
946
+ `Speaker ${index + 1} voice`,
947
+ (next) => onChange(
948
+ speakers.map((s, i) => i === index ? { ...s, voice: next } : s),
949
+ true
950
+ )
951
+ ),
952
+ /* @__PURE__ */ jsx4(
953
+ Button2,
954
+ {
955
+ variant: "quiet",
956
+ removes: true,
957
+ icon: /* @__PURE__ */ jsx4(XIcon, {}),
958
+ onClick: () => onChange(speakers.filter((_, i) => i !== index), true),
959
+ disabled: disabled || speakers.length <= 2,
960
+ "aria-label": `Remove speaker ${index + 1}`,
961
+ title: "Remove speaker"
962
+ }
963
+ )
964
+ ] }, index)),
965
+ /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
966
+ Button2,
967
+ {
968
+ variant: "quiet",
969
+ icon: /* @__PURE__ */ jsx4(PlusIcon, {}),
970
+ onClick: () => onChange([...speakers, { name: `Speaker ${speakers.length + 1}`, voice: voices[0] ?? "" }], true),
971
+ disabled: disabled || !canAdd,
972
+ title: canAdd ? "Add a speaker" : `This model takes up to ${max} speakers`,
973
+ children: "Add speaker"
974
+ }
975
+ ) })
976
+ ] })
977
+ ] });
978
+ }
979
+
980
+ // settings/react/ModelSettingsPanel.tsx
981
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
870
982
  function StatusGlyph({ issue, source }) {
871
983
  const title = issue ? issue.message : source ? `Valid \xB7 ${source}` : "Valid";
872
- return /* @__PURE__ */ jsx4(
984
+ return /* @__PURE__ */ jsx5(
873
985
  "span",
874
986
  {
875
987
  className: cn("inline-flex h-7 w-6 shrink-0 items-center justify-center", issue ? "text-warning" : "text-success"),
876
988
  role: "img",
877
989
  "aria-label": title,
878
990
  title,
879
- children: issue ? /* @__PURE__ */ jsx4(TriangleAlertGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx4(CircleCheckGlyph, { className: "h-3.5 w-3.5" })
991
+ children: issue ? /* @__PURE__ */ jsx5(TriangleAlertGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(CircleCheckGlyph, { className: "h-3.5 w-3.5" })
880
992
  }
881
993
  );
882
994
  }
883
- function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
995
+ function changeTogether(target, changes, controls) {
996
+ if (target.changeMany) target.changeMany(changes);
997
+ else for (const [key, value] of Object.entries(changes)) target.change(key, value, controls[key] ?? null);
998
+ }
999
+ function ModelSettingRow({ row, control, target, idPrefix, disabled, multiSpeaker }) {
884
1000
  const read = target.read(row.key, control);
885
1001
  const resetLabel = target.resetLabel ?? "Reset to agent";
886
- const reset = read.changedHere && target.resetToInherited ? /* @__PURE__ */ jsx4(
887
- Button2,
1002
+ const reset = read.changedHere && target.resetToInherited ? /* @__PURE__ */ jsx5(
1003
+ Button3,
888
1004
  {
889
1005
  variant: "quiet",
890
- icon: /* @__PURE__ */ jsx4(RotateCcwGlyph, {}),
1006
+ icon: /* @__PURE__ */ jsx5(RotateCcwGlyph, {}),
891
1007
  onClick: () => target.resetToInherited?.(row.key),
892
1008
  disabled,
893
1009
  "aria-label": `${resetLabel}: ${row.label}`,
@@ -896,20 +1012,20 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
896
1012
  }
897
1013
  ) : null;
898
1014
  if (read.state === "bound") {
899
- return /* @__PURE__ */ jsx4(
1015
+ return /* @__PURE__ */ jsx5(
900
1016
  SettingRowFrame,
901
1017
  {
902
1018
  rowKey: row.key,
903
1019
  state: "bound",
904
- leading: /* @__PURE__ */ jsx4(Checkbox2, { checked: true, disabled: true, "aria-label": `${row.label} is a run input` }),
905
- label: /* @__PURE__ */ jsx4(RowLabel, { children: row.label }),
906
- actions: /* @__PURE__ */ jsxs4(Fragment2, { children: [
1020
+ leading: /* @__PURE__ */ jsx5(Checkbox2, { checked: true, disabled: true, "aria-label": `${row.label} is a run input` }),
1021
+ label: /* @__PURE__ */ jsx5(RowLabel, { children: row.label }),
1022
+ actions: /* @__PURE__ */ jsxs5(Fragment3, { children: [
907
1023
  reset,
908
- target.unbind ? /* @__PURE__ */ jsx4(
909
- Button2,
1024
+ target.unbind ? /* @__PURE__ */ jsx5(
1025
+ Button3,
910
1026
  {
911
1027
  variant: "quiet",
912
- icon: /* @__PURE__ */ jsx4(UnlinkGlyph, {}),
1028
+ icon: /* @__PURE__ */ jsx5(UnlinkGlyph, {}),
913
1029
  onClick: () => target.unbind?.(row.key, control),
914
1030
  disabled,
915
1031
  "aria-label": `Make ${row.label} a fixed setting`,
@@ -917,9 +1033,9 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
917
1033
  }
918
1034
  ) : null
919
1035
  ] }),
920
- children: /* @__PURE__ */ jsxs4("span", { className: "flex min-h-7 min-w-0 items-center gap-2", "data-bound-control": row.key, children: [
921
- /* @__PURE__ */ jsx4(Chip, { tone: "primary", icon: /* @__PURE__ */ jsx4(VariableGlyph, {}), label: `{{${read.variableName}}}` }),
922
- /* @__PURE__ */ jsx4("span", { className: "truncate type-meta text-muted-foreground", children: read.value === void 0 || read.value === null || read.value === "" ? "Model default" : formatSettingValue(read.value) })
1036
+ children: /* @__PURE__ */ jsxs5("span", { className: "flex min-h-7 min-w-0 items-center gap-2", "data-bound-control": row.key, children: [
1037
+ /* @__PURE__ */ jsx5(Chip, { tone: "primary", icon: /* @__PURE__ */ jsx5(VariableGlyph, {}), label: `{{${read.variableName}}}` }),
1038
+ /* @__PURE__ */ jsx5("span", { className: "truncate type-meta text-muted-foreground", children: read.value === void 0 || read.value === null || read.value === "" ? "Model default" : formatSettingValue(read.value) })
923
1039
  ] })
924
1040
  }
925
1041
  );
@@ -929,12 +1045,12 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
929
1045
  const isOff = isSet && isOffValue(value, control);
930
1046
  const checkboxId = `${idPrefix}-setting-${row.key}`;
931
1047
  const canBind = Boolean(target.bind) && (target.canBind?.(row.key) ?? true);
932
- return /* @__PURE__ */ jsx4(
1048
+ return /* @__PURE__ */ jsx5(
933
1049
  SettingRowFrame,
934
1050
  {
935
1051
  rowKey: row.key,
936
1052
  state: isOff ? "off" : read.state,
937
- leading: /* @__PURE__ */ jsx4(
1053
+ leading: /* @__PURE__ */ jsx5(
938
1054
  Checkbox2,
939
1055
  {
940
1056
  id: checkboxId,
@@ -943,14 +1059,14 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
943
1059
  onCheckedChange: (next) => next === true ? target.turnOn(row.key, control) : target.clear(row.key)
944
1060
  }
945
1061
  ),
946
- label: /* @__PURE__ */ jsx4(RowLabel, { htmlFor: checkboxId, muted: !isSet, children: row.label }),
947
- actions: /* @__PURE__ */ jsxs4(Fragment2, { children: [
948
- /* @__PURE__ */ jsx4(StatusGlyph, { issue: read.issue, source: read.source }),
949
- canBind ? /* @__PURE__ */ jsx4(
950
- Button2,
1062
+ label: /* @__PURE__ */ jsx5(RowLabel, { htmlFor: checkboxId, muted: !isSet, children: row.label }),
1063
+ actions: /* @__PURE__ */ jsxs5(Fragment3, { children: [
1064
+ /* @__PURE__ */ jsx5(StatusGlyph, { issue: read.issue, source: read.source }),
1065
+ canBind ? /* @__PURE__ */ jsx5(
1066
+ Button3,
951
1067
  {
952
1068
  variant: "quiet",
953
- icon: /* @__PURE__ */ jsx4(VariableGlyph, {}),
1069
+ icon: /* @__PURE__ */ jsx5(VariableGlyph, {}),
954
1070
  onClick: () => target.bind?.(row.key, control),
955
1071
  disabled,
956
1072
  "aria-label": `Make ${row.label} a run input`,
@@ -958,11 +1074,11 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
958
1074
  "data-bind-control": row.key
959
1075
  }
960
1076
  ) : null,
961
- isSet && read.issue?.fixable && target.fix ? /* @__PURE__ */ jsx4(
962
- Button2,
1077
+ isSet && read.issue?.fixable && target.fix ? /* @__PURE__ */ jsx5(
1078
+ Button3,
963
1079
  {
964
1080
  variant: "quiet",
965
- icon: /* @__PURE__ */ jsx4(WrenchGlyph, {}),
1081
+ icon: /* @__PURE__ */ jsx5(WrenchGlyph, {}),
966
1082
  onClick: () => target.fix?.(row.key),
967
1083
  disabled,
968
1084
  "aria-label": `Fix ${row.label} automatically`,
@@ -970,12 +1086,12 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
970
1086
  }
971
1087
  ) : null,
972
1088
  reset,
973
- isSet ? /* @__PURE__ */ jsx4(
974
- Button2,
1089
+ isSet ? /* @__PURE__ */ jsx5(
1090
+ Button3,
975
1091
  {
976
1092
  variant: "quiet",
977
1093
  removes: true,
978
- icon: /* @__PURE__ */ jsx4(Trash2Glyph, {}),
1094
+ icon: /* @__PURE__ */ jsx5(Trash2Glyph, {}),
979
1095
  onClick: () => target.clear(row.key),
980
1096
  disabled,
981
1097
  "aria-label": `Clear ${row.label} to not set`,
@@ -984,8 +1100,18 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
984
1100
  }
985
1101
  ) : null
986
1102
  ] }),
987
- children: isSet ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
988
- /* @__PURE__ */ jsx4(
1103
+ children: isSet && row.key === "tts_voice" && control.enum?.length && (multiSpeaker || Array.isArray(value)) ? /* @__PURE__ */ jsx5(
1104
+ VoiceSettingControl,
1105
+ {
1106
+ voices: control.enum.map(String),
1107
+ value,
1108
+ multiSpeaker: multiSpeaker ? { max: multiSpeaker.max } : null,
1109
+ onChange: (voice, multi) => changeTogether(target, { tts_voice: voice, multi_speaker: multi }, { tts_voice: control, multi_speaker: multiSpeaker }),
1110
+ idPrefix: `${idPrefix}-voice`,
1111
+ disabled
1112
+ }
1113
+ ) : isSet ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
1114
+ /* @__PURE__ */ jsx5(
989
1115
  SettingControlInput,
990
1116
  {
991
1117
  settingKey: row.key,
@@ -996,40 +1122,40 @@ function ModelSettingRow({ row, control, target, idPrefix, disabled }) {
996
1122
  id: `${idPrefix}-control-${row.key}`
997
1123
  }
998
1124
  ),
999
- read.issue ? /* @__PURE__ */ jsx4("p", { className: "pt-1 type-meta text-warning-ink", children: read.issue.message }) : null
1000
- ] }) : /* @__PURE__ */ jsx4(RowText, { children: read.modelDefault !== void 0 && read.modelDefault !== null ? `Not set \xB7 Model default: ${formatSettingValue(read.modelDefault)}` : "Not set" })
1125
+ read.issue ? /* @__PURE__ */ jsx5("p", { className: "pt-1 type-meta text-warning-ink", children: read.issue.message }) : null
1126
+ ] }) : /* @__PURE__ */ jsx5(RowText, { children: read.modelDefault !== void 0 && read.modelDefault !== null ? `Not set \xB7 Model default: ${formatSettingValue(read.modelDefault)}` : "Not set" })
1001
1127
  }
1002
1128
  );
1003
1129
  }
1004
1130
  function ExtraSettingRow({ rowKey, label, value, kind, onClear, onReset, resetLabel = "Reset to agent", disabled = false }) {
1005
- return /* @__PURE__ */ jsx4(
1131
+ return /* @__PURE__ */ jsx5(
1006
1132
  SettingRowFrame,
1007
1133
  {
1008
1134
  rowKey,
1009
1135
  state: kind,
1010
- label: /* @__PURE__ */ jsx4(RowLabel, { children: label }),
1011
- actions: /* @__PURE__ */ jsxs4(Fragment2, { children: [
1012
- kind === "translated" ? /* @__PURE__ */ jsx4(Chip, { tone: "sky", label: "Translated", title: "Kept as set and sent as this model's closest equivalent" }) : /* @__PURE__ */ jsx4(Chip, { tone: "warning", label: "Not on this model", title: "This model has no such setting" }),
1013
- onReset ? /* @__PURE__ */ jsx4(Button2, { variant: "quiet", icon: /* @__PURE__ */ jsx4(RotateCcwGlyph, {}), onClick: onReset, disabled, "aria-label": `${resetLabel}: ${label}`, title: resetLabel, "data-setting-reset": rowKey }) : /* @__PURE__ */ jsx4(Button2, { variant: "quiet", removes: true, icon: /* @__PURE__ */ jsx4(Trash2Glyph, {}), onClick: onClear, disabled, "aria-label": `Clear ${label}`, title: "Clear to not set", "data-setting-clear": rowKey })
1136
+ label: /* @__PURE__ */ jsx5(RowLabel, { children: label }),
1137
+ actions: /* @__PURE__ */ jsxs5(Fragment3, { children: [
1138
+ kind === "translated" ? /* @__PURE__ */ jsx5(Chip, { tone: "sky", label: "Translated", title: "Kept as set and sent as this model's closest equivalent" }) : /* @__PURE__ */ jsx5(Chip, { tone: "warning", label: "Not on this model", title: "This model has no such setting" }),
1139
+ onReset ? /* @__PURE__ */ jsx5(Button3, { variant: "quiet", icon: /* @__PURE__ */ jsx5(RotateCcwGlyph, {}), onClick: onReset, disabled, "aria-label": `${resetLabel}: ${label}`, title: resetLabel, "data-setting-reset": rowKey }) : /* @__PURE__ */ jsx5(Button3, { variant: "quiet", removes: true, icon: /* @__PURE__ */ jsx5(Trash2Glyph, {}), onClick: onClear, disabled, "aria-label": `Clear ${label}`, title: "Clear to not set", "data-setting-clear": rowKey })
1014
1140
  ] }),
1015
- children: /* @__PURE__ */ jsx4(RowText, { mono: value !== void 0 && value !== null, children: value === void 0 || value === null ? "Not set" : isOffValue(value, null) ? "Off" : formatSettingValue(value) })
1141
+ children: /* @__PURE__ */ jsx5(RowText, { mono: value !== void 0 && value !== null, children: value === void 0 || value === null ? "Not set" : isOffValue(value, null) ? "Off" : formatSettingValue(value) })
1016
1142
  }
1017
1143
  );
1018
1144
  }
1019
1145
  function ModelRow({ model, resetLabel, idPrefix, disabled }) {
1020
1146
  const off = disabled || model.disabled === true;
1021
1147
  const oneMessageId = `${idPrefix}-model-one-message`;
1022
- return /* @__PURE__ */ jsx4(
1148
+ return /* @__PURE__ */ jsx5(
1023
1149
  SettingRowFrame,
1024
1150
  {
1025
1151
  rowKey: "model",
1026
1152
  state: model.value ? "set" : "unset",
1027
- label: /* @__PURE__ */ jsx4(RowLabel, { children: "Model" }),
1028
- actions: model.changedHere && model.resetToInherited ? /* @__PURE__ */ jsx4(
1029
- Button2,
1153
+ label: /* @__PURE__ */ jsx5(RowLabel, { children: "Model" }),
1154
+ actions: model.changedHere && model.resetToInherited ? /* @__PURE__ */ jsx5(
1155
+ Button3,
1030
1156
  {
1031
1157
  variant: "quiet",
1032
- icon: /* @__PURE__ */ jsx4(RotateCcwGlyph, {}),
1158
+ icon: /* @__PURE__ */ jsx5(RotateCcwGlyph, {}),
1033
1159
  onClick: () => model.resetToInherited?.(),
1034
1160
  disabled: off,
1035
1161
  "aria-label": `${resetLabel}: Model`,
@@ -1037,8 +1163,8 @@ function ModelRow({ model, resetLabel, idPrefix, disabled }) {
1037
1163
  "data-setting-reset": "model"
1038
1164
  }
1039
1165
  ) : null,
1040
- children: /* @__PURE__ */ jsxs4("div", { className: "flex min-h-7 min-w-0 flex-wrap items-center gap-x-2", children: [
1041
- /* @__PURE__ */ jsx4(
1166
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex min-h-7 min-w-0 flex-wrap items-center gap-x-2", children: [
1167
+ /* @__PURE__ */ jsx5(
1042
1168
  ModelListDropdown,
1043
1169
  {
1044
1170
  "aria-label": "Model",
@@ -1055,8 +1181,8 @@ function ModelRow({ model, resetLabel, idPrefix, disabled }) {
1055
1181
  ...model.disabledTitle ? { disabledTitle: model.disabledTitle } : {}
1056
1182
  }
1057
1183
  ),
1058
- model.oneMessage ? /* @__PURE__ */ jsxs4("label", { htmlFor: oneMessageId, className: "flex min-h-7 items-center gap-1 type-secondary text-muted-foreground", title: "Use this model for the next message, then go back", children: [
1059
- /* @__PURE__ */ jsx4(
1184
+ model.oneMessage ? /* @__PURE__ */ jsxs5("label", { htmlFor: oneMessageId, className: "flex min-h-7 items-center gap-1 type-secondary text-muted-foreground", title: "Use this model for the next message, then go back", children: [
1185
+ /* @__PURE__ */ jsx5(
1060
1186
  Switch2,
1061
1187
  {
1062
1188
  id: oneMessageId,
@@ -1078,7 +1204,6 @@ function ModelSettingsPanel({
1078
1204
  idPrefix,
1079
1205
  model,
1080
1206
  notices,
1081
- renderRow,
1082
1207
  capabilities,
1083
1208
  emptyNote,
1084
1209
  disabled = false
@@ -1086,7 +1211,8 @@ function ModelSettingsPanel({
1086
1211
  const groups = controls ? buildSettingsRows(controls, values).filter((group) => group.rows.length > 0) : [];
1087
1212
  const extras = extraSettingKeys(controls, values, target.changedKeys);
1088
1213
  const resetLabel = target.resetLabel ?? "Reset to agent";
1089
- const extraRow = (key, kind) => /* @__PURE__ */ jsx4(
1214
+ const multiSpeaker = settingControl(controls, "multi_speaker");
1215
+ const extraRow = (key, kind) => /* @__PURE__ */ jsx5(
1090
1216
  ExtraSettingRow,
1091
1217
  {
1092
1218
  rowKey: key,
@@ -1100,29 +1226,28 @@ function ModelSettingsPanel({
1100
1226
  },
1101
1227
  key
1102
1228
  );
1103
- return /* @__PURE__ */ jsxs4("div", { "data-model-settings-panel": "", className: "@container flex min-w-0 flex-col", children: [
1104
- model ? /* @__PURE__ */ jsx4(ModelRow, { model, resetLabel, idPrefix, disabled }) : null,
1229
+ return /* @__PURE__ */ jsxs5("div", { "data-model-settings-panel": "", className: "@container flex min-w-0 flex-col", children: [
1230
+ model ? /* @__PURE__ */ jsx5(ModelRow, { model, resetLabel, idPrefix, disabled }) : null,
1105
1231
  notices,
1106
1232
  extras.unsupported.map((key) => extraRow(key, "unsupported")),
1107
- extras.translated.length > 0 ? /* @__PURE__ */ jsx4(SettingsGroupSection, { id: "translated", title: "Translated for this model", children: /* @__PURE__ */ jsx4("div", { "data-translated-settings": "", children: extras.translated.map((key) => extraRow(key, "translated")) }) }) : null,
1108
- groups.length === 0 && emptyNote ? /* @__PURE__ */ jsx4("div", { className: "py-2 type-secondary text-muted-foreground", children: emptyNote }) : null,
1109
- groups.map((group) => /* @__PURE__ */ jsx4(SettingsGroupSection, { id: group.id, title: group.label || void 0, children: group.rows.map((row) => {
1110
- const custom = renderRow?.(row);
1111
- if (custom !== void 0) return /* @__PURE__ */ jsx4("div", { children: custom }, row.key);
1233
+ extras.translated.length > 0 ? /* @__PURE__ */ jsx5(SettingsGroupSection, { id: "translated", title: "Translated for this model", children: /* @__PURE__ */ jsx5("div", { "data-translated-settings": "", children: extras.translated.map((key) => extraRow(key, "translated")) }) }) : null,
1234
+ groups.length === 0 && emptyNote ? /* @__PURE__ */ jsx5("div", { className: "py-2 type-secondary text-muted-foreground", children: emptyNote }) : null,
1235
+ groups.map((group) => /* @__PURE__ */ jsx5(SettingsGroupSection, { id: group.id, title: group.label || void 0, children: group.rows.map((row) => {
1112
1236
  if (!row.control) return null;
1113
- return /* @__PURE__ */ jsx4(
1237
+ return /* @__PURE__ */ jsx5(
1114
1238
  ModelSettingRow,
1115
1239
  {
1116
1240
  row,
1117
1241
  control: row.control,
1118
1242
  target,
1119
1243
  idPrefix,
1120
- disabled
1244
+ disabled,
1245
+ multiSpeaker
1121
1246
  },
1122
1247
  row.key
1123
1248
  );
1124
1249
  }) }, group.id)),
1125
- capabilities ? /* @__PURE__ */ jsx4(
1250
+ capabilities ? /* @__PURE__ */ jsx5(
1126
1251
  InputCapabilitiesEditor,
1127
1252
  {
1128
1253
  values: capabilities.values,
@@ -1165,9 +1290,6 @@ function memoEffective(entry) {
1165
1290
  }
1166
1291
  export {
1167
1292
  ModelSettingsPanel,
1168
- SettingRowFrame,
1169
- RowLabel as SettingRowLabel,
1170
- formatSettingValue,
1171
1293
  useEffectiveSettings,
1172
1294
  useSettingsEntry,
1173
1295
  useSettingsSelector
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/agents",
3
- "version": "0.59.233",
3
+ "version": "0.59.234",
4
4
  "description": "Portable AI Matrx agent stream protocol, event projection, and safe result-presentation primitives.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -508,15 +508,15 @@
508
508
  },
509
509
  "sideEffects": false,
510
510
  "dependencies": {
511
- "@ai-matrx/alchemy": "latest",
512
511
  "@ai-matrx/api": "latest",
513
- "@ai-matrx/associations": "latest",
514
512
  "@ai-matrx/data": "latest",
515
- "@ai-matrx/design-system": "latest",
516
513
  "@ai-matrx/content-ir": "latest",
514
+ "@ai-matrx/design-system": "latest",
515
+ "@ai-matrx/associations": "latest",
516
+ "@ai-matrx/alchemy": "latest",
517
+ "@ai-matrx/icons": "latest",
517
518
  "@ai-matrx/diff": "latest",
518
- "@ai-matrx/kit": "latest",
519
- "@ai-matrx/icons": "latest"
519
+ "@ai-matrx/kit": "latest"
520
520
  },
521
521
  "peerDependencies": {
522
522
  "react": ">=18.0.0",
@@ -561,5 +561,5 @@
561
561
  "verify:tarball": "node ./scripts/verify-tarball.mjs",
562
562
  "typegen:self-test": "node scripts/typegen/drop-guard.self-test.mjs && node scripts/typegen/check-api-types-fresh.mjs --self-test"
563
563
  },
564
- "gitHead": "976e132072571eaf8e301e73396655730a2f2acc"
564
+ "gitHead": "899dceeede75f9c863ff2e80199f2bcd9c907444"
565
565
  }