@camstack/ui-library 1.1.45 → 1.1.46

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/dist/index.cjs CHANGED
@@ -3775,7 +3775,6 @@ function mirror(input) {
3775
3775
  }
3776
3776
  }
3777
3777
  const patch = {
3778
- enabled: srcCfg.enabled,
3779
3778
  ...chosenModelId ? { modelId: chosenModelId } : {},
3780
3779
  settings: dropModelSpecific(srcCfg.settings, targetAddon)
3781
3780
  };
@@ -13874,6 +13873,228 @@ function buildStepTreeFromSchema(schema) {
13874
13873
  return [...roots, ...audioRoots];
13875
13874
  }
13876
13875
  //#endregion
13876
+ //#region src/composites/grouped-model-selector.tsx
13877
+ /**
13878
+ * GroupedModelSelector — the shared Family → Tier → Variant model picker.
13879
+ *
13880
+ * The catalog is a FLAT list of models (ids like `yolo26s`, `yolo26s-int8`);
13881
+ * this folds it into a `family → tier → variant` tree (see
13882
+ * `buildModelVariantGroups` in `@camstack/types`) so the operator picks
13883
+ * "YOLO26 → Small → Int8" instead of scanning a flat dropdown of every
13884
+ * size×quantization. The selected value stays the flat model id.
13885
+ *
13886
+ * Parameterised on the minimal `ModelVariantSource` shape so BOTH the config-UI
13887
+ * catalog (`ModelCatalogEntry`) and the pipeline/device steppers
13888
+ * (`PipelineModelOption`) drive the same component. Legacy / ungrouped models
13889
+ * never appear here (the grouping helpers skip them).
13890
+ */
13891
+ function Chip({ active, onClick, disabled, children, title }) {
13892
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
13893
+ type: "button",
13894
+ onClick,
13895
+ disabled,
13896
+ title,
13897
+ className: cn("rounded-full px-3 py-1 text-[11px] font-medium border transition-colors", active ? "border-primary bg-primary/10 text-primary" : "border-border bg-background text-foreground-subtle hover:text-foreground hover:border-foreground-subtle/40", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
13898
+ children
13899
+ });
13900
+ }
13901
+ function GroupedModelSelector({ catalog, value, onChange, disabled }) {
13902
+ const families = (0, _camstack_types.buildModelVariantGroups)(catalog);
13903
+ const current = (0, _camstack_types.describeModelVariant)(catalog, value);
13904
+ const activeFamily = families.find((f) => f.family === current?.family)?.family ?? families[0]?.family ?? "";
13905
+ const family = families.find((f) => f.family === activeFamily) ?? families[0];
13906
+ if (!family) return null;
13907
+ const activeTier = family.tiers.find((t) => t.tier === current?.tier)?.tier ?? family.tiers[0]?.tier ?? "";
13908
+ const tier = family.tiers.find((t) => t.tier === activeTier) ?? family.tiers[0];
13909
+ if (!tier) return null;
13910
+ const resolutions = [...new Set(tier.options.map((o) => o.resolution))].sort((a, b) => (b ?? Infinity) - (a ?? Infinity));
13911
+ const selectedResolution = resolutions.includes(current?.resolution) ? current?.resolution : resolutions[0];
13912
+ const variantOptions = tier.options.filter((o) => o.resolution === selectedResolution);
13913
+ const resLabel = (r) => r === void 0 ? "640 · native" : `${r}`;
13914
+ const selectTier = (tierId) => {
13915
+ const t = family.tiers.find((x) => x.tier === tierId);
13916
+ if (!t) return;
13917
+ onChange((t.options.find((o) => o.precision === (current?.precision ?? "fp32") && o.optimization === (current?.optimization ?? "standard") && o.resolution === current?.resolution) ?? t.options[0]).modelId);
13918
+ };
13919
+ const selectResolution = (resolution) => {
13920
+ const opts = tier.options.filter((o) => o.resolution === resolution);
13921
+ const pick = opts.find((o) => o.precision === (current?.precision ?? "fp32") && o.optimization === (current?.optimization ?? "standard")) ?? opts[0];
13922
+ if (pick) onChange(pick.modelId);
13923
+ };
13924
+ const selectVariant = (opt) => {
13925
+ const id = (0, _camstack_types.resolveVariantModelId)(catalog, {
13926
+ family: family.family,
13927
+ tier: tier.tier,
13928
+ precision: opt.precision,
13929
+ optimization: opt.optimization,
13930
+ resolution: opt.resolution
13931
+ });
13932
+ if (id) onChange(id);
13933
+ };
13934
+ const selectedOption = tier.options.find((o) => o.modelId === value) ?? null;
13935
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13936
+ className: "space-y-3",
13937
+ children: [
13938
+ families.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13939
+ className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
13940
+ children: "Family"
13941
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13942
+ className: "flex flex-wrap gap-1.5",
13943
+ children: families.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
13944
+ active: f.family === family.family,
13945
+ disabled,
13946
+ onClick: () => onChange(f.tiers[0]?.baseModelId ?? ""),
13947
+ children: f.label
13948
+ }, f.family))
13949
+ })] }),
13950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
13951
+ className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
13952
+ children: [family.label, " — size"]
13953
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13954
+ className: "grid grid-cols-2 gap-1.5 sm:grid-cols-4",
13955
+ children: family.tiers.map((t) => {
13956
+ const active = t.tier === tier.tier;
13957
+ const base = t.options.find((o) => o.modelId === t.baseModelId) ?? t.options[0];
13958
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13959
+ type: "button",
13960
+ disabled,
13961
+ onClick: () => selectTier(t.tier),
13962
+ className: cn("rounded-lg border px-2 py-2 text-left transition-all", active ? "border-primary bg-primary/5 ring-1 ring-primary/30" : "border-border bg-background hover:border-foreground-subtle/40", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
13963
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13964
+ className: "flex items-center gap-1",
13965
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13966
+ className: "text-xs font-semibold text-foreground",
13967
+ children: t.label
13968
+ }), active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3 w-3 text-primary" })]
13969
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
13970
+ className: "text-[10px] text-foreground-subtle",
13971
+ children: [
13972
+ "~",
13973
+ base?.sizeMB ?? 0,
13974
+ " MB"
13975
+ ]
13976
+ })]
13977
+ }, t.tier);
13978
+ })
13979
+ })] }),
13980
+ resolutions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13981
+ className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
13982
+ children: "Resolution"
13983
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13984
+ className: "flex flex-wrap gap-1.5",
13985
+ children: resolutions.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
13986
+ active: r === selectedResolution,
13987
+ disabled,
13988
+ title: r === void 0 ? "native (best accuracy)" : `${r}×${r} (faster)`,
13989
+ onClick: () => selectResolution(r),
13990
+ children: resLabel(r)
13991
+ }, r ?? "native"))
13992
+ })] }),
13993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13994
+ className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
13995
+ children: "Variant"
13996
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13997
+ className: "flex flex-wrap gap-1.5",
13998
+ children: variantOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
13999
+ active: opt.modelId === value,
14000
+ disabled,
14001
+ title: `${opt.formats.join(", ")} · ~${opt.sizeMB} MB`,
14002
+ onClick: () => selectVariant(opt),
14003
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14004
+ className: "inline-flex items-center gap-1",
14005
+ children: [opt.optimization === "fast" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, { className: "h-3 w-3" }), opt.label]
14006
+ })
14007
+ }, opt.modelId))
14008
+ })] }),
14009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14010
+ className: "flex items-center gap-2 text-[10px] text-foreground-subtle pt-0.5",
14011
+ children: [
14012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cpu, { className: "h-3 w-3" }),
14013
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14014
+ className: "font-mono",
14015
+ children: value || "—"
14016
+ }),
14017
+ selectedOption && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
14018
+ "· ",
14019
+ selectedOption.formats.join(", "),
14020
+ " · ~",
14021
+ selectedOption.sizeMB,
14022
+ " MB"
14023
+ ] })
14024
+ ]
14025
+ })
14026
+ ]
14027
+ });
14028
+ }
14029
+ //#endregion
14030
+ //#region src/composites/model-picker.tsx
14031
+ /**
14032
+ * Shared model picker — the ONE model-selection control across the app.
14033
+ *
14034
+ * Renders the grouped Family→Tier→Resolution→Variant selector
14035
+ * ({@link GroupedModelSelector}) when the step's models declare variant groups
14036
+ * (yolo26 / yolov9 …), else a flat `<select>` (face / plate / classifier
14037
+ * catalogs and operator-registered custom models). Custom / ungrouped ids stay
14038
+ * selectable via an "Other models" flat select alongside the grouped picker, and
14039
+ * a legacy/removed pin is surfaced with an explicit banner rather than a
14040
+ * silently mis-highlighted chip.
14041
+ *
14042
+ * Used by the pipeline stepper (`PipelineStep`), the agent/device step editor
14043
+ * (`AgentStepEditor`), and the benchmark — so the picker looks identical
14044
+ * everywhere. `value === ''` renders nothing selected (callers that support an
14045
+ * "Auto / node default" mode own that affordance and hide this picker while it
14046
+ * is active).
14047
+ */
14048
+ function ModelPicker({ models, value, onChange, disabled }) {
14049
+ const hasGroups = (0, react$1.useMemo)(() => (0, _camstack_types.buildModelVariantGroups)(models).length > 0, [models]);
14050
+ const ungrouped = (0, react$1.useMemo)(() => models.filter((m) => m.group === void 0), [models]);
14051
+ const unresolvedBanner = value !== "" && !models.some((m) => m.id === value) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14052
+ className: "rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[10px] text-amber-300",
14053
+ children: [
14054
+ "Current pin (",
14055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14056
+ className: "font-mono",
14057
+ children: value
14058
+ }),
14059
+ ") is a legacy/removed model — no longer offered below. Pick a replacement to change it; Save persists whatever is selected here."
14060
+ ]
14061
+ });
14062
+ const flatSelect = (opts, selectPlaceholder) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
14063
+ disabled,
14064
+ className: "w-full bg-surface border border-border rounded px-2 py-1 text-xs disabled:opacity-50",
14065
+ value: opts.some((m) => m.id === value) ? value : "",
14066
+ onChange: (e) => onChange(e.target.value),
14067
+ children: [selectPlaceholder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
14068
+ value: "",
14069
+ disabled: true,
14070
+ children: selectPlaceholder
14071
+ }), opts.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
14072
+ value: m.id,
14073
+ children: m.name
14074
+ }, m.id))]
14075
+ });
14076
+ if (!hasGroups) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14077
+ className: "space-y-2",
14078
+ children: [unresolvedBanner, flatSelect(models)]
14079
+ });
14080
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14081
+ className: "space-y-3",
14082
+ children: [
14083
+ unresolvedBanner,
14084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedModelSelector, {
14085
+ catalog: models,
14086
+ value,
14087
+ onChange,
14088
+ disabled
14089
+ }),
14090
+ ungrouped.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14091
+ className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
14092
+ children: "Other models"
14093
+ }), flatSelect(ungrouped, "— custom / ungrouped model —")] })
14094
+ ]
14095
+ });
14096
+ }
14097
+ //#endregion
13877
14098
  //#region src/composites/pipeline-step.tsx
13878
14099
  /**
13879
14100
  * PipelineStep — single step card in the pipeline builder.
@@ -13883,11 +14104,7 @@ function buildStepTreeFromSchema(schema) {
13883
14104
  * (Model → Confidence), children recursively.
13884
14105
  */
13885
14106
  function modelsForStep(schema) {
13886
- if (!schema) return [];
13887
- return schema.models.map((m) => ({
13888
- id: m.id,
13889
- name: m.name
13890
- }));
14107
+ return schema?.models ?? [];
13891
14108
  }
13892
14109
  function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0, onChange, onDelete: _onDelete, readOnly = false, toggleMode = "simple", overrideState = null, onOverrideChange, inheritedEnabled, hideModelAndSettings = false, allowAutoModel = false }) {
13893
14110
  const [expanded, setExpanded] = (0, react$1.useState)(false);
@@ -13978,21 +14195,35 @@ function PipelineStep({ step, schema, allSchemas: _allSchemas, depth: _depth = 0
13978
14195
  children: "Model and detection settings are managed per agent — open the Pipeline page for this camera's agent to edit them."
13979
14196
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13980
14197
  className: "space-y-3",
13981
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSelect, {
13982
- label: "Model",
13983
- value: step.modelId,
13984
- disabled: readOnly,
13985
- options: [...allowAutoModel ? [{
13986
- value: "",
13987
- label: "Auto (node default)"
13988
- }] : [], ...models.map((m) => ({
13989
- value: m.id,
13990
- label: m.name
13991
- }))],
13992
- onChange: (v) => onChange({
13993
- ...step,
13994
- modelId: v
13995
- })
14198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14199
+ className: "space-y-2",
14200
+ children: [
14201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
14202
+ className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide",
14203
+ children: "Model"
14204
+ }),
14205
+ allowAutoModel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
14206
+ className: "flex items-center gap-2 text-[11px] text-foreground-subtle",
14207
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14208
+ type: "checkbox",
14209
+ checked: isAutoSentinel,
14210
+ disabled: readOnly,
14211
+ onChange: (e) => onChange({
14212
+ ...step,
14213
+ modelId: e.target.checked ? "" : schema?.defaultModelId ?? models[0]?.id ?? ""
14214
+ })
14215
+ }), "Auto (node default)"]
14216
+ }),
14217
+ !isAutoSentinel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
14218
+ models,
14219
+ value: step.modelId,
14220
+ disabled: readOnly,
14221
+ onChange: (id) => onChange({
14222
+ ...step,
14223
+ modelId: id
14224
+ })
14225
+ })
14226
+ ]
13996
14227
  }), schema?.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
13997
14228
  field,
13998
14229
  allFields: schema.configSchema ?? [],
@@ -14198,218 +14429,7 @@ function ThreeStateButton({ label, active, variant, subtext, onClick }) {
14198
14429
  });
14199
14430
  }
14200
14431
  //#endregion
14201
- //#region src/composites/grouped-model-selector.tsx
14202
- /**
14203
- * GroupedModelSelector — the shared Family → Tier → Variant model picker.
14204
- *
14205
- * The catalog is a FLAT list of models (ids like `yolo26s`, `yolo26s-int8`);
14206
- * this folds it into a `family → tier → variant` tree (see
14207
- * `buildModelVariantGroups` in `@camstack/types`) so the operator picks
14208
- * "YOLO26 → Small → Int8" instead of scanning a flat dropdown of every
14209
- * size×quantization. The selected value stays the flat model id.
14210
- *
14211
- * Parameterised on the minimal `ModelVariantSource` shape so BOTH the config-UI
14212
- * catalog (`ModelCatalogEntry`) and the pipeline/device steppers
14213
- * (`PipelineModelOption`) drive the same component. Legacy / ungrouped models
14214
- * never appear here (the grouping helpers skip them).
14215
- */
14216
- function Chip({ active, onClick, disabled, children, title }) {
14217
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14218
- type: "button",
14219
- onClick,
14220
- disabled,
14221
- title,
14222
- className: cn("rounded-full px-3 py-1 text-[11px] font-medium border transition-colors", active ? "border-primary bg-primary/10 text-primary" : "border-border bg-background text-foreground-subtle hover:text-foreground hover:border-foreground-subtle/40", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
14223
- children
14224
- });
14225
- }
14226
- function GroupedModelSelector({ catalog, value, onChange, disabled }) {
14227
- const families = (0, _camstack_types.buildModelVariantGroups)(catalog);
14228
- const current = (0, _camstack_types.describeModelVariant)(catalog, value);
14229
- const activeFamily = families.find((f) => f.family === current?.family)?.family ?? families[0]?.family ?? "";
14230
- const family = families.find((f) => f.family === activeFamily) ?? families[0];
14231
- if (!family) return null;
14232
- const activeTier = family.tiers.find((t) => t.tier === current?.tier)?.tier ?? family.tiers[0]?.tier ?? "";
14233
- const tier = family.tiers.find((t) => t.tier === activeTier) ?? family.tiers[0];
14234
- if (!tier) return null;
14235
- const resolutions = [...new Set(tier.options.map((o) => o.resolution))].sort((a, b) => (b ?? Infinity) - (a ?? Infinity));
14236
- const selectedResolution = resolutions.includes(current?.resolution) ? current?.resolution : resolutions[0];
14237
- const variantOptions = tier.options.filter((o) => o.resolution === selectedResolution);
14238
- const resLabel = (r) => r === void 0 ? "640 · native" : `${r}`;
14239
- const selectTier = (tierId) => {
14240
- const t = family.tiers.find((x) => x.tier === tierId);
14241
- if (!t) return;
14242
- onChange((t.options.find((o) => o.precision === (current?.precision ?? "fp32") && o.optimization === (current?.optimization ?? "standard") && o.resolution === current?.resolution) ?? t.options[0]).modelId);
14243
- };
14244
- const selectResolution = (resolution) => {
14245
- const opts = tier.options.filter((o) => o.resolution === resolution);
14246
- const pick = opts.find((o) => o.precision === (current?.precision ?? "fp32") && o.optimization === (current?.optimization ?? "standard")) ?? opts[0];
14247
- if (pick) onChange(pick.modelId);
14248
- };
14249
- const selectVariant = (opt) => {
14250
- const id = (0, _camstack_types.resolveVariantModelId)(catalog, {
14251
- family: family.family,
14252
- tier: tier.tier,
14253
- precision: opt.precision,
14254
- optimization: opt.optimization,
14255
- resolution: opt.resolution
14256
- });
14257
- if (id) onChange(id);
14258
- };
14259
- const selectedOption = tier.options.find((o) => o.modelId === value) ?? null;
14260
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14261
- className: "space-y-3",
14262
- children: [
14263
- families.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14264
- className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
14265
- children: "Family"
14266
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14267
- className: "flex flex-wrap gap-1.5",
14268
- children: families.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
14269
- active: f.family === family.family,
14270
- disabled,
14271
- onClick: () => onChange(f.tiers[0]?.baseModelId ?? ""),
14272
- children: f.label
14273
- }, f.family))
14274
- })] }),
14275
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
14276
- className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
14277
- children: [family.label, " — size"]
14278
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14279
- className: "grid grid-cols-2 gap-1.5 sm:grid-cols-4",
14280
- children: family.tiers.map((t) => {
14281
- const active = t.tier === tier.tier;
14282
- const base = t.options.find((o) => o.modelId === t.baseModelId) ?? t.options[0];
14283
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14284
- type: "button",
14285
- disabled,
14286
- onClick: () => selectTier(t.tier),
14287
- className: cn("rounded-lg border px-2 py-2 text-left transition-all", active ? "border-primary bg-primary/5 ring-1 ring-primary/30" : "border-border bg-background hover:border-foreground-subtle/40", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
14288
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14289
- className: "flex items-center gap-1",
14290
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14291
- className: "text-xs font-semibold text-foreground",
14292
- children: t.label
14293
- }), active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3 w-3 text-primary" })]
14294
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14295
- className: "text-[10px] text-foreground-subtle",
14296
- children: [
14297
- "~",
14298
- base?.sizeMB ?? 0,
14299
- " MB"
14300
- ]
14301
- })]
14302
- }, t.tier);
14303
- })
14304
- })] }),
14305
- resolutions.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14306
- className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
14307
- children: "Resolution"
14308
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14309
- className: "flex flex-wrap gap-1.5",
14310
- children: resolutions.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
14311
- active: r === selectedResolution,
14312
- disabled,
14313
- title: r === void 0 ? "native (best accuracy)" : `${r}×${r} (faster)`,
14314
- onClick: () => selectResolution(r),
14315
- children: resLabel(r)
14316
- }, r ?? "native"))
14317
- })] }),
14318
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14319
- className: "text-[10px] uppercase tracking-wide text-foreground-subtle mb-1.5",
14320
- children: "Variant"
14321
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14322
- className: "flex flex-wrap gap-1.5",
14323
- children: variantOptions.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
14324
- active: opt.modelId === value,
14325
- disabled,
14326
- title: `${opt.formats.join(", ")} · ~${opt.sizeMB} MB`,
14327
- onClick: () => selectVariant(opt),
14328
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14329
- className: "inline-flex items-center gap-1",
14330
- children: [opt.optimization === "fast" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Zap, { className: "h-3 w-3" }), opt.label]
14331
- })
14332
- }, opt.modelId))
14333
- })] }),
14334
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14335
- className: "flex items-center gap-2 text-[10px] text-foreground-subtle pt-0.5",
14336
- children: [
14337
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cpu, { className: "h-3 w-3" }),
14338
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14339
- className: "font-mono",
14340
- children: value || "—"
14341
- }),
14342
- selectedOption && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
14343
- "· ",
14344
- selectedOption.formats.join(", "),
14345
- " · ~",
14346
- selectedOption.sizeMB,
14347
- " MB"
14348
- ] })
14349
- ]
14350
- })
14351
- ]
14352
- });
14353
- }
14354
- //#endregion
14355
14432
  //#region src/composites/agent-step-editor.tsx
14356
- /**
14357
- * Shared model picker for the step editor — renders the grouped
14358
- * Family→Tier→Variant selector when the step's models declare variant groups
14359
- * (yolo26 …), else a flat `<select>` (face / plate / classifier catalogs and
14360
- * custom models). Same component both the agent-default and per-device-override
14361
- * modes use, so the picker looks identical across the pipeline stepper, the
14362
- * device stepper and the cluster matrix.
14363
- */
14364
- function ModelPicker({ models, value, onChange, disabled }) {
14365
- const hasGroups = (0, react$1.useMemo)(() => (0, _camstack_types.buildModelVariantGroups)(models).length > 0, [models]);
14366
- const ungrouped = (0, react$1.useMemo)(() => models.filter((m) => m.group === void 0), [models]);
14367
- const unresolvedBanner = value !== "" && !models.some((m) => m.id === value) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14368
- className: "rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[10px] text-amber-300",
14369
- children: [
14370
- "Current pin (",
14371
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14372
- className: "font-mono",
14373
- children: value
14374
- }),
14375
- ") is a legacy/removed model — no longer offered below. Pick a replacement to change it; Save persists whatever is selected here."
14376
- ]
14377
- });
14378
- const flatSelect = (opts, selectPlaceholder) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
14379
- disabled,
14380
- className: "w-full bg-surface border border-border rounded px-2 py-1 text-xs disabled:opacity-50",
14381
- value: opts.some((m) => m.id === value) ? value : "",
14382
- onChange: (e) => onChange(e.target.value),
14383
- children: [selectPlaceholder !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
14384
- value: "",
14385
- disabled: true,
14386
- children: selectPlaceholder
14387
- }), opts.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
14388
- value: m.id,
14389
- children: m.name
14390
- }, m.id))]
14391
- });
14392
- if (!hasGroups) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14393
- className: "space-y-2",
14394
- children: [unresolvedBanner, flatSelect(models)]
14395
- });
14396
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14397
- className: "space-y-3",
14398
- children: [
14399
- unresolvedBanner,
14400
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedModelSelector, {
14401
- catalog: models,
14402
- value,
14403
- onChange,
14404
- disabled
14405
- }),
14406
- ungrouped.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14407
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
14408
- children: "Other models"
14409
- }), flatSelect(ungrouped, "— custom / ungrouped model —")] })
14410
- ]
14411
- });
14412
- }
14413
14433
  function buildDisplayStep(addon, cfg) {
14414
14434
  return {
14415
14435
  addonId: addon.id,
@@ -14469,14 +14489,14 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
14469
14489
  onChangePatch?.(hasAnyField ? next : null);
14470
14490
  };
14471
14491
  const setModel = (modelId) => {
14472
- const { enabled: _enabled, modelId: _prev, ...rest } = currentPatch ?? {};
14492
+ const { modelId: _prev, ...rest } = currentPatch ?? {};
14473
14493
  commitPatch(modelId === void 0 ? rest : {
14474
14494
  ...rest,
14475
14495
  modelId
14476
14496
  });
14477
14497
  };
14478
14498
  const setSettings = (settings) => {
14479
- const { enabled: _enabled, settings: _prev, ...rest } = currentPatch ?? {};
14499
+ const { settings: _prev, ...rest } = currentPatch ?? {};
14480
14500
  commitPatch(settings === void 0 ? rest : {
14481
14501
  ...rest,
14482
14502
  settings
@@ -14597,6 +14617,42 @@ function AgentStepEditor(props) {
14597
14617
  onChangePatch
14598
14618
  });
14599
14619
  }
14620
+ //#endregion
14621
+ //#region src/composites/pipeline-matrix-shared.tsx
14622
+ /**
14623
+ * Stable column identity. Plain per-node columns key by `agentNodeId` (so the
14624
+ * rendered `key` is byte-identical to the pre-C7.4 matrix); device sub-columns
14625
+ * disambiguate with their `deviceKey`. MUST be used everywhere a React `key`,
14626
+ * selection compare, or column identity is derived.
14627
+ */
14628
+ function agentColumnKey(a) {
14629
+ return a.deviceKey !== void 0 ? `${a.agentNodeId}::${a.deviceKey}` : a.agentNodeId;
14630
+ }
14631
+ /**
14632
+ * Coalesce CONSECUTIVE columns that share an `agentNodeId` into node groups.
14633
+ * Order-preserving: the flat column order is untouched, so body cells still map
14634
+ * 1:1 to `agents`. A run of length 1 is a plain (ungrouped) node header.
14635
+ */
14636
+ function groupAgentColumns(agents) {
14637
+ const groups = [];
14638
+ for (const col of agents) {
14639
+ const last = groups[groups.length - 1];
14640
+ if (last && last.agentNodeId === col.agentNodeId) {
14641
+ const columns = [...last.columns, col];
14642
+ groups[groups.length - 1] = {
14643
+ ...last,
14644
+ columns,
14645
+ grouped: columns.length > 1
14646
+ };
14647
+ } else groups.push({
14648
+ agentNodeId: col.agentNodeId,
14649
+ engineLabel: col.engineLabel,
14650
+ columns: [col],
14651
+ grouped: false
14652
+ });
14653
+ }
14654
+ return groups;
14655
+ }
14600
14656
  function flattenTree(nodes) {
14601
14657
  const out = [];
14602
14658
  const walk = (list, depth) => {
@@ -14675,6 +14731,41 @@ function ClassChips({ inputs, outputs }) {
14675
14731
  ]
14676
14732
  });
14677
14733
  }
14734
+ /**
14735
+ * Grouped node header (C7.4): a node super-label spanning its device
14736
+ * sub-columns, with a per-device sub-label row beneath. Rendered ONLY for a
14737
+ * node expanded into 2+ device sub-columns; a single plain column keeps each
14738
+ * matrix's own byte-identical single-header markup. The spanning cell uses
14739
+ * `gridColumn: span N` so it aligns to its N body columns; the inner flex row
14740
+ * gives each device its own equal-width, padded sub-label slot.
14741
+ */
14742
+ function GroupedAgentHeader({ group }) {
14743
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14744
+ className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60",
14745
+ style: { gridColumn: `span ${group.columns.length}` },
14746
+ children: [
14747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14748
+ className: "truncate px-3 pt-2 text-xs font-semibold text-foreground",
14749
+ children: group.agentNodeId
14750
+ }),
14751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14752
+ className: "truncate px-3 text-[10px] text-foreground-subtle",
14753
+ children: group.engineLabel
14754
+ }),
14755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14756
+ className: "mt-1 flex border-t border-border/50",
14757
+ children: group.columns.map((c, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14758
+ className: cn("min-w-0 flex-1 px-3 py-1", i > 0 && "border-l border-border/50"),
14759
+ title: c.deviceLabel ?? c.deviceKey,
14760
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14761
+ className: "truncate text-[10px] font-medium text-foreground/90",
14762
+ children: c.deviceLabel ?? c.deviceKey
14763
+ })
14764
+ }, agentColumnKey(c)))
14765
+ })
14766
+ ]
14767
+ });
14768
+ }
14678
14769
  /** Step label (slot heading + addon name + class chips), shared by both views. */
14679
14770
  function StepLabel({ node }) {
14680
14771
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -14777,13 +14868,13 @@ function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell,
14777
14868
  })
14778
14869
  })]
14779
14870
  }), agents.map((a) => {
14780
- const state = getCellState(node.addonId, a.agentNodeId);
14871
+ const state = getCellState(node.addonId, a);
14781
14872
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14782
14873
  type: "button",
14783
- onClick: () => onCellClick(node.addonId, a.agentNodeId),
14784
- className: cellButtonClass(state, selectedCell?.addonId === node.addonId && selectedCell.agentNodeId === a.agentNodeId, "min-w-0 overflow-hidden px-3 py-1.5 border-b border-l border-border"),
14874
+ onClick: () => onCellClick(node.addonId, a),
14875
+ className: cellButtonClass(state, selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a), "min-w-0 overflow-hidden px-3 py-1.5 border-b border-l border-border"),
14785
14876
  children: renderCellContent(state)
14786
- }, a.agentNodeId);
14877
+ }, agentColumnKey(a));
14787
14878
  })] });
14788
14879
  }
14789
14880
  /**
@@ -14795,7 +14886,8 @@ function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell,
14795
14886
  function MatrixGrid$1({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
14796
14887
  const gridTemplate = `${STEP_COL_REM$1}rem repeat(${agents.length}, 13rem)`;
14797
14888
  const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
14798
- const onlyAgent = agents[0]?.agentNodeId;
14889
+ const onlyAgent = agents[0];
14890
+ const groups = groupAgentColumns(agents);
14799
14891
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14800
14892
  className: "overflow-auto border border-border rounded",
14801
14893
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -14806,16 +14898,16 @@ function MatrixGrid$1({ rows, agents, getCellState, onCellClick, selectedCell, o
14806
14898
  className: "sticky top-0 left-0 z-20 bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle border-b border-border",
14807
14899
  children: "Step"
14808
14900
  }),
14809
- agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14901
+ groups.map((group) => group.grouped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedAgentHeader, { group }, group.agentNodeId) : group.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14810
14902
  className: "sticky top-0 z-10 min-w-0 bg-muted/60 px-3 py-2 border-b border-l border-border",
14811
14903
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14812
14904
  className: "text-xs font-semibold text-foreground truncate",
14813
- children: a.agentNodeId
14905
+ children: col.agentNodeId
14814
14906
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14815
14907
  className: "text-[10px] text-foreground-subtle truncate",
14816
- children: a.engineLabel
14908
+ children: col.engineLabel
14817
14909
  })]
14818
- }, a.agentNodeId)),
14910
+ }, agentColumnKey(col)))),
14819
14911
  rows.map(({ node, depth }) => {
14820
14912
  const cellState = showToggle && onlyAgent !== void 0 ? getCellState(node.addonId, onlyAgent) : null;
14821
14913
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridRow, {
@@ -14851,14 +14943,14 @@ function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, o
14851
14943
  className: "bg-muted/60 px-3 py-2 border-b border-border",
14852
14944
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14853
14945
  className: "text-xs font-semibold text-foreground truncate",
14854
- children: a.agentNodeId
14946
+ children: a.deviceLabel ? `${a.agentNodeId} · ${a.deviceLabel}` : a.agentNodeId
14855
14947
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14856
14948
  className: "text-[10px] text-foreground-subtle truncate",
14857
14949
  children: a.engineLabel
14858
14950
  })]
14859
14951
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: rows.map(({ node, depth }) => {
14860
- const state = getCellState(node.addonId, a.agentNodeId);
14861
- const isSelected = selectedCell?.addonId === node.addonId && selectedCell.agentNodeId === a.agentNodeId;
14952
+ const state = getCellState(node.addonId, a);
14953
+ const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
14862
14954
  const toggleProps = showToggle && state !== null ? {
14863
14955
  enabled: state.kind === "enabled",
14864
14956
  onChange: (next) => onToggleEnabled?.(node.addonId, next),
@@ -14870,7 +14962,7 @@ function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, o
14870
14962
  className: "flex items-center gap-2",
14871
14963
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14872
14964
  type: "button",
14873
- onClick: () => onCellClick(node.addonId, a.agentNodeId),
14965
+ onClick: () => onCellClick(node.addonId, a),
14874
14966
  className: cellButtonClass(state, isSelected, "flex flex-1 min-w-0 items-start justify-between gap-3 px-3 py-2"),
14875
14967
  style: { paddingLeft: `${12 + depth * 14}px` },
14876
14968
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -14888,7 +14980,7 @@ function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, o
14888
14980
  })
14889
14981
  }, node.addonId);
14890
14982
  }) })]
14891
- }, a.agentNodeId))
14983
+ }, agentColumnKey(a)))
14892
14984
  });
14893
14985
  }
14894
14986
  function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
@@ -15009,11 +15101,12 @@ function StepGateCell({ node, depth, gate, busy, onSet }) {
15009
15101
  }
15010
15102
  /**
15011
15103
  * Agents-scoped node selector for the B fallback. Styled like the shared
15012
- * `NodePicker` but driven by THIS matrix's `agents` set — the cluster-wide
15104
+ * `NodePicker` but driven by THIS matrix's NODE set — the cluster-wide
15013
15105
  * NodePicker can list nodes (e.g. the hub) that own no pipeline column here,
15014
- * which would let the operator select a node the matrix can't show.
15106
+ * which would let the operator select a node the matrix can't show. Selects a
15107
+ * NODE (grouping its device sub-columns), never a raw device column.
15015
15108
  */
15016
- function NodeSelectorBar({ agents, selected, onSelect }) {
15109
+ function NodeSelectorBar({ nodes, selected, onSelect }) {
15017
15110
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15018
15111
  className: "flex flex-wrap items-center gap-1.5",
15019
15112
  role: "tablist",
@@ -15026,7 +15119,7 @@ function NodeSelectorBar({ agents, selected, onSelect }) {
15026
15119
  }),
15027
15120
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15028
15121
  className: "flex overflow-hidden rounded-md border border-border text-[11px] font-medium",
15029
- children: agents.map((a, idx) => {
15122
+ children: nodes.map((a, idx) => {
15030
15123
  const isSelected = a.agentNodeId === selected;
15031
15124
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15032
15125
  type: "button",
@@ -15044,6 +15137,7 @@ function NodeSelectorBar({ agents, selected, onSelect }) {
15044
15137
  }
15045
15138
  function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single }) {
15046
15139
  const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
15140
+ const groups = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
15047
15141
  const gridTemplate = single ? `minmax(0, 1.35fr) repeat(${agents.length}, minmax(0, 1fr))` : `${STEP_COL_REM}rem repeat(${agents.length}, 13rem)`;
15048
15142
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15049
15143
  className: cn("rounded border border-border", single ? "overflow-hidden" : "overflow-auto"),
@@ -15055,16 +15149,16 @@ function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selected
15055
15149
  className: "sticky left-0 top-0 z-20 border-b border-border bg-muted/60 px-3 py-2 text-[10px] uppercase tracking-widest text-foreground-subtle",
15056
15150
  children: "Step · gate"
15057
15151
  }),
15058
- agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15152
+ groups.map((group) => group.grouped ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GroupedAgentHeader, { group }, group.agentNodeId) : group.columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15059
15153
  className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2",
15060
15154
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15061
15155
  className: "truncate text-xs font-semibold text-foreground",
15062
- children: a.agentNodeId
15156
+ children: col.agentNodeId
15063
15157
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15064
15158
  className: "truncate text-[10px] text-foreground-subtle",
15065
- children: a.engineLabel
15159
+ children: col.engineLabel
15066
15160
  })]
15067
- }, a.agentNodeId)),
15161
+ }, agentColumnKey(col)))),
15068
15162
  rows.map(({ node, depth }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixRow, {
15069
15163
  node,
15070
15164
  depth,
@@ -15088,16 +15182,16 @@ function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selec
15088
15182
  busy: gateBusy,
15089
15183
  onSet: (next) => onSetGate(node.addonId, next)
15090
15184
  }), agents.map((a) => {
15091
- const state = getCellState(node.addonId, a.agentNodeId);
15092
- const isSelected = selectedCell?.addonId === node.addonId && selectedCell.agentNodeId === a.agentNodeId;
15185
+ const state = getCellState(node.addonId, a);
15186
+ const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
15093
15187
  const actionable = state.kind === "enabled" || state.kind === "skip";
15094
15188
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15095
15189
  type: "button",
15096
15190
  disabled: !actionable,
15097
- onClick: () => onCellClick(node.addonId, a.agentNodeId),
15191
+ onClick: () => onCellClick(node.addonId, a),
15098
15192
  className: cellButtonClass(state, isSelected, cn("min-w-0 overflow-hidden border-b border-l border-border px-3 py-2", !actionable && "cursor-default hover:bg-transparent")),
15099
15193
  children: renderCellContent(state)
15100
- }, a.agentNodeId);
15194
+ }, agentColumnKey(a));
15101
15195
  })] });
15102
15196
  }
15103
15197
  function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange }) {
@@ -15112,17 +15206,18 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15112
15206
  RO?.observe(el);
15113
15207
  return () => RO?.disconnect();
15114
15208
  }, []);
15115
- const single = shouldUseSingleNode(width, agents.length, STEP_COL_PX);
15209
+ const nodes = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
15210
+ const single = shouldUseSingleNode(width, nodes.length, STEP_COL_PX);
15116
15211
  const selectedNode = (0, react$1.useMemo)(() => {
15117
- if (agents.length === 0) return null;
15118
- return agents.find((a) => a.agentNodeId === fallbackNodeId) ?? agents[0];
15119
- }, [agents, fallbackNodeId]);
15120
- const shownAgents = single && selectedNode ? [selectedNode] : agents;
15212
+ if (nodes.length === 0) return null;
15213
+ return nodes.find((n) => n.agentNodeId === fallbackNodeId) ?? nodes[0] ?? null;
15214
+ }, [nodes, fallbackNodeId]);
15215
+ const shownAgents = single && selectedNode ? selectedNode.columns : agents;
15121
15216
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15122
15217
  ref: containerRef,
15123
15218
  className: "w-full space-y-2",
15124
15219
  children: [single && selectedNode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectorBar, {
15125
- agents,
15220
+ nodes,
15126
15221
  selected: selectedNode.agentNodeId,
15127
15222
  onSelect: onFallbackNodeChange
15128
15223
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
@@ -18850,8 +18945,6 @@ var usePipelineExecutorGetAvailableEngines = trpc.pipelineExecutor.getAvailableE
18850
18945
  var usePipelineExecutorGetSelectedEngine = trpc.pipelineExecutor.getSelectedEngine.useQuery;
18851
18946
  /** Generated alias around `trpc.pipelineExecutor.getDefaultSteps.useQuery`. */
18852
18947
  var usePipelineExecutorGetDefaultSteps = trpc.pipelineExecutor.getDefaultSteps.useQuery;
18853
- /** Generated alias around `trpc.pipelineExecutor.reprobeEngine.useMutation`. */
18854
- var usePipelineExecutorReprobeEngine = trpc.pipelineExecutor.reprobeEngine.useMutation;
18855
18948
  /** Generated alias around `trpc.pipelineExecutor.getEngineProvisioning.useQuery`. */
18856
18949
  var usePipelineExecutorGetEngineProvisioning = trpc.pipelineExecutor.getEngineProvisioning.useQuery;
18857
18950
  /** Generated alias around `trpc.pipelineExecutor.getVideoPipelineSteps.useQuery`. */
@@ -18922,6 +19015,10 @@ var usePipelineExecutorGetDetectionConfigSchema = trpc.pipelineExecutor.getDetec
18922
19015
  var usePipelineOrchestratorAssignPipeline = trpc.pipelineOrchestrator.assignPipeline.useMutation;
18923
19016
  /** Generated alias around `trpc.pipelineOrchestrator.unassignPipeline.useMutation`. */
18924
19017
  var usePipelineOrchestratorUnassignPipeline = trpc.pipelineOrchestrator.unassignPipeline.useMutation;
19018
+ /** Generated alias around `trpc.pipelineOrchestrator.setPipelineDevicePin.useMutation`. */
19019
+ var usePipelineOrchestratorSetPipelineDevicePin = trpc.pipelineOrchestrator.setPipelineDevicePin.useMutation;
19020
+ /** Generated alias around `trpc.pipelineOrchestrator.getPipelineDevicePin.useQuery`. */
19021
+ var usePipelineOrchestratorGetPipelineDevicePin = trpc.pipelineOrchestrator.getPipelineDevicePin.useQuery;
18925
19022
  /** Generated alias around `trpc.pipelineOrchestrator.rebalance.useMutation`. */
18926
19023
  var usePipelineOrchestratorRebalance = trpc.pipelineOrchestrator.rebalance.useMutation;
18927
19024
  /** Generated alias around `trpc.pipelineOrchestrator.getPipelineAssignments.useQuery`. */
@@ -18954,8 +19051,6 @@ var usePipelineOrchestratorGetAudioAssignments = trpc.pipelineOrchestrator.getAu
18954
19051
  var usePipelineOrchestratorGetAgentSettings = trpc.pipelineOrchestrator.getAgentSettings.useQuery;
18955
19052
  /** Generated alias around `trpc.pipelineOrchestrator.listAgentSettings.useQuery`. */
18956
19053
  var usePipelineOrchestratorListAgentSettings = trpc.pipelineOrchestrator.listAgentSettings.useQuery;
18957
- /** Generated alias around `trpc.pipelineOrchestrator.setAgentAddonDefaults.useMutation`. */
18958
- var usePipelineOrchestratorSetAgentAddonDefaults = trpc.pipelineOrchestrator.setAgentAddonDefaults.useMutation;
18959
19054
  /** Generated alias around `trpc.pipelineOrchestrator.removeAgentSettings.useMutation`. */
18960
19055
  var usePipelineOrchestratorRemoveAgentSettings = trpc.pipelineOrchestrator.removeAgentSettings.useMutation;
18961
19056
  /** Generated alias around `trpc.pipelineOrchestrator.setAgentMaxCameras.useMutation`. */
@@ -18968,6 +19063,8 @@ var usePipelineOrchestratorSetAgentCapabilities = trpc.pipelineOrchestrator.setA
18968
19063
  var usePipelineOrchestratorSetAgentReachableHost = trpc.pipelineOrchestrator.setAgentReachableHost.useMutation;
18969
19064
  /** Generated alias around `trpc.pipelineOrchestrator.setAgentInferenceDevices.useMutation`. */
18970
19065
  var usePipelineOrchestratorSetAgentInferenceDevices = trpc.pipelineOrchestrator.setAgentInferenceDevices.useMutation;
19066
+ /** Generated alias around `trpc.pipelineOrchestrator.getNodeInferenceDevices.useQuery`. */
19067
+ var usePipelineOrchestratorGetNodeInferenceDevices = trpc.pipelineOrchestrator.getNodeInferenceDevices.useQuery;
18971
19068
  /** Generated alias around `trpc.pipelineOrchestrator.resetNodePipelineDefaults.useMutation`. */
18972
19069
  var usePipelineOrchestratorResetNodePipelineDefaults = trpc.pipelineOrchestrator.resetNodePipelineDefaults.useMutation;
18973
19070
  /** Generated alias around `trpc.pipelineOrchestrator.getCameraSettings.useQuery`. */
@@ -28829,15 +28926,34 @@ function DeviceItemPreview({ trpc, device, status, enabled, showStatusPills = fa
28829
28926
  }
28830
28927
  //#endregion
28831
28928
  //#region src/composites/device-item/status-dot.tsx
28929
+ /** The states that breathe (are "alive"). Offline + disabled are static. */
28930
+ function isAlive(status) {
28931
+ return status === "online" || status === "recording-continuous" || status === "recording-events";
28932
+ }
28832
28933
  function resolveTitle(status, lastChangedAt) {
28833
- if (status === "disabled") return "Disabled";
28834
- if (status === "online") return "Online";
28835
- const lastSeen = formatLastSeen(lastChangedAt);
28836
- return lastSeen === null ? "Offline" : `Offline · last seen ${lastSeen}`;
28934
+ switch (status) {
28935
+ case "disabled": return "Disabled";
28936
+ case "online": return "Online";
28937
+ case "recording-continuous": return "Recording · always";
28938
+ case "recording-events": return "Recording · on motion";
28939
+ case "offline": {
28940
+ const lastSeen = formatLastSeen(lastChangedAt);
28941
+ return lastSeen === null ? "Offline" : `Offline · last seen ${lastSeen}`;
28942
+ }
28943
+ }
28944
+ }
28945
+ function colorClass(status) {
28946
+ switch (status) {
28947
+ case "disabled": return "bg-black ring-1 ring-foreground-subtle/60";
28948
+ case "offline": return "bg-foreground-subtle";
28949
+ case "recording-continuous": return "bg-danger";
28950
+ case "recording-events": return "bg-info";
28951
+ case "online": return "bg-success";
28952
+ }
28837
28953
  }
28838
28954
  function StatusDot({ status, lastChangedAt }) {
28839
28955
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
28840
- className: cn("h-1.5 w-1.5 rounded-full flex-shrink-0", status === "disabled" ? "bg-warning" : status === "online" ? "bg-success" : "bg-danger"),
28956
+ className: cn("h-1.5 w-1.5 rounded-full flex-shrink-0", colorClass(status), isAlive(status) && "animate-pulse"),
28841
28957
  title: resolveTitle(status, lastChangedAt)
28842
28958
  });
28843
28959
  }
@@ -47081,6 +47197,7 @@ exports.MediaPlayerHeroCard = MediaPlayerHeroCard;
47081
47197
  exports.MediaPlayerInlineControl = MediaPlayerInlineControl;
47082
47198
  exports.MediaPlayerPanel = MediaPlayerPanel;
47083
47199
  exports.MobileDrawer = MobileDrawer;
47200
+ exports.ModelPicker = ModelPicker;
47084
47201
  exports.MotionZonesSettings = MotionZonesSettings;
47085
47202
  exports.NodeMultiSelectField = NodeMultiSelectField;
47086
47203
  exports.NodePicker = NodePicker;
@@ -47182,6 +47299,7 @@ exports.WidgetPanel = WidgetPanel;
47182
47299
  exports.WidgetRegistryProvider = WidgetRegistryProvider;
47183
47300
  exports.WidgetSlot = WidgetSlot;
47184
47301
  exports.ZoneEditingProvider = ZoneEditingProvider;
47302
+ exports.agentColumnKey = agentColumnKey;
47185
47303
  exports.allDeviceTypeFilterOptions = allDeviceTypeFilterOptions;
47186
47304
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
47187
47305
  exports.childEntityId = childEntityId;
@@ -47215,6 +47333,7 @@ exports.formatNumeric = formatNumeric;
47215
47333
  exports.fuzzyMatch = fuzzyMatch;
47216
47334
  exports.getClassColor = getClassColor;
47217
47335
  exports.getPhaseVisual = getPhaseVisual;
47336
+ exports.groupAgentColumns = groupAgentColumns;
47218
47337
  exports.groupChildrenByLayout = groupChildrenByLayout;
47219
47338
  exports.hardwareLabel = hardwareLabel;
47220
47339
  exports.humidifierTint = humidifierTint;
@@ -47820,7 +47939,6 @@ exports.usePipelineExecutorKillEngine = usePipelineExecutorKillEngine;
47820
47939
  exports.usePipelineExecutorListLoadedEngines = usePipelineExecutorListLoadedEngines;
47821
47940
  exports.usePipelineExecutorListReferenceImages = usePipelineExecutorListReferenceImages;
47822
47941
  exports.usePipelineExecutorListTemplates = usePipelineExecutorListTemplates;
47823
- exports.usePipelineExecutorReprobeEngine = usePipelineExecutorReprobeEngine;
47824
47942
  exports.usePipelineExecutorRunAudioTest = usePipelineExecutorRunAudioTest;
47825
47943
  exports.usePipelineExecutorRunPipeline = usePipelineExecutorRunPipeline;
47826
47944
  exports.usePipelineExecutorRunPipelineBatch = usePipelineExecutorRunPipelineBatch;
@@ -47849,8 +47967,10 @@ exports.usePipelineOrchestratorGetDeviceLiveContribution = usePipelineOrchestrat
47849
47967
  exports.usePipelineOrchestratorGetDeviceSettingsContribution = usePipelineOrchestratorGetDeviceSettingsContribution;
47850
47968
  exports.usePipelineOrchestratorGetGlobalMetrics = usePipelineOrchestratorGetGlobalMetrics;
47851
47969
  exports.usePipelineOrchestratorGetIngestOwner = usePipelineOrchestratorGetIngestOwner;
47970
+ exports.usePipelineOrchestratorGetNodeInferenceDevices = usePipelineOrchestratorGetNodeInferenceDevices;
47852
47971
  exports.usePipelineOrchestratorGetPipelineAssignment = usePipelineOrchestratorGetPipelineAssignment;
47853
47972
  exports.usePipelineOrchestratorGetPipelineAssignments = usePipelineOrchestratorGetPipelineAssignments;
47973
+ exports.usePipelineOrchestratorGetPipelineDevicePin = usePipelineOrchestratorGetPipelineDevicePin;
47854
47974
  exports.usePipelineOrchestratorListAgentSettings = usePipelineOrchestratorListAgentSettings;
47855
47975
  exports.usePipelineOrchestratorListTemplates = usePipelineOrchestratorListTemplates;
47856
47976
  exports.usePipelineOrchestratorRebalance = usePipelineOrchestratorRebalance;
@@ -47858,7 +47978,6 @@ exports.usePipelineOrchestratorRemoveAgentSettings = usePipelineOrchestratorRemo
47858
47978
  exports.usePipelineOrchestratorResetNodePipelineDefaults = usePipelineOrchestratorResetNodePipelineDefaults;
47859
47979
  exports.usePipelineOrchestratorResolvePipeline = usePipelineOrchestratorResolvePipeline;
47860
47980
  exports.usePipelineOrchestratorSaveTemplate = usePipelineOrchestratorSaveTemplate;
47861
- exports.usePipelineOrchestratorSetAgentAddonDefaults = usePipelineOrchestratorSetAgentAddonDefaults;
47862
47981
  exports.usePipelineOrchestratorSetAgentCapabilities = usePipelineOrchestratorSetAgentCapabilities;
47863
47982
  exports.usePipelineOrchestratorSetAgentDetectWeight = usePipelineOrchestratorSetAgentDetectWeight;
47864
47983
  exports.usePipelineOrchestratorSetAgentInferenceDevices = usePipelineOrchestratorSetAgentInferenceDevices;
@@ -47868,6 +47987,7 @@ exports.usePipelineOrchestratorSetCameraPipelineForAgent = usePipelineOrchestrat
47868
47987
  exports.usePipelineOrchestratorSetCameraStepOverride = usePipelineOrchestratorSetCameraStepOverride;
47869
47988
  exports.usePipelineOrchestratorSetCameraStepToggle = usePipelineOrchestratorSetCameraStepToggle;
47870
47989
  exports.usePipelineOrchestratorSetCapabilityBinding = usePipelineOrchestratorSetCapabilityBinding;
47990
+ exports.usePipelineOrchestratorSetPipelineDevicePin = usePipelineOrchestratorSetPipelineDevicePin;
47871
47991
  exports.usePipelineOrchestratorUnassignAudio = usePipelineOrchestratorUnassignAudio;
47872
47992
  exports.usePipelineOrchestratorUnassignPipeline = usePipelineOrchestratorUnassignPipeline;
47873
47993
  exports.usePipelineOrchestratorUpdateTemplate = usePipelineOrchestratorUpdateTemplate;