@camstack/ui-library 1.1.45 → 1.1.76

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 }) {
@@ -14996,11 +15088,11 @@ function ZoneBadge() {
14996
15088
  })]
14997
15089
  });
14998
15090
  }
14999
- function StepGateCell({ node, depth, gate, busy, onSet }) {
15091
+ function StepGateCell({ node, depth, gate, busy, onSet, hideGate }) {
15000
15092
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15001
15093
  className: "sticky left-0 z-10 flex flex-col gap-2 border-b border-border bg-surface px-3 py-2.5 text-xs min-w-0",
15002
15094
  style: { paddingLeft: `${12 + depth * 14}px` },
15003
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), gate.kind === "zone-driven" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoneBadge, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GateControl, {
15095
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepLabel, { node }), hideGate ? null : gate.kind === "zone-driven" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoneBadge, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GateControl, {
15004
15096
  gate,
15005
15097
  busy,
15006
15098
  onSet
@@ -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",
@@ -15042,8 +15135,9 @@ function NodeSelectorBar({ agents, selected, onSelect }) {
15042
15135
  ]
15043
15136
  });
15044
15137
  }
15045
- function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single }) {
15138
+ function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single, hideGate }) {
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"),
@@ -15053,18 +15147,18 @@ function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selected
15053
15147
  children: [
15054
15148
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
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
- children: "Step · gate"
15150
+ children: hideGate ? "Step" : "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,
@@ -15074,33 +15168,35 @@ function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selected
15074
15168
  onCellClick,
15075
15169
  selectedCell,
15076
15170
  onSetGate,
15077
- gateBusy
15171
+ gateBusy,
15172
+ hideGate
15078
15173
  }, node.addonId))
15079
15174
  ]
15080
15175
  })
15081
15176
  });
15082
15177
  }
15083
- function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy }) {
15178
+ function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, hideGate }) {
15084
15179
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepGateCell, {
15085
15180
  node,
15086
15181
  depth,
15087
15182
  gate,
15088
15183
  busy: gateBusy,
15089
- onSet: (next) => onSetGate(node.addonId, next)
15184
+ onSet: (next) => onSetGate(node.addonId, next),
15185
+ hideGate
15090
15186
  }), agents.map((a) => {
15091
- const state = getCellState(node.addonId, a.agentNodeId);
15092
- const isSelected = selectedCell?.addonId === node.addonId && selectedCell.agentNodeId === a.agentNodeId;
15187
+ const state = getCellState(node.addonId, a);
15188
+ const isSelected = selectedCell !== null && selectedCell.addonId === node.addonId && agentColumnKey(selectedCell) === agentColumnKey(a);
15093
15189
  const actionable = state.kind === "enabled" || state.kind === "skip";
15094
15190
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15095
15191
  type: "button",
15096
15192
  disabled: !actionable,
15097
- onClick: () => onCellClick(node.addonId, a.agentNodeId),
15193
+ onClick: () => onCellClick(node.addonId, a),
15098
15194
  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
15195
  children: renderCellContent(state)
15100
- }, a.agentNodeId);
15196
+ }, agentColumnKey(a));
15101
15197
  })] });
15102
15198
  }
15103
- function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange }) {
15199
+ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange, hideGate }) {
15104
15200
  const containerRef = (0, react$1.useRef)(null);
15105
15201
  const [width, setWidth] = (0, react$1.useState)(0);
15106
15202
  (0, react$1.useLayoutEffect)(() => {
@@ -15112,17 +15208,18 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15112
15208
  RO?.observe(el);
15113
15209
  return () => RO?.disconnect();
15114
15210
  }, []);
15115
- const single = shouldUseSingleNode(width, agents.length, STEP_COL_PX);
15211
+ const nodes = (0, react$1.useMemo)(() => groupAgentColumns(agents), [agents]);
15212
+ const single = shouldUseSingleNode(width, nodes.length, STEP_COL_PX);
15116
15213
  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;
15214
+ if (nodes.length === 0) return null;
15215
+ return nodes.find((n) => n.agentNodeId === fallbackNodeId) ?? nodes[0] ?? null;
15216
+ }, [nodes, fallbackNodeId]);
15217
+ const shownAgents = single && selectedNode ? selectedNode.columns : agents;
15121
15218
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15122
15219
  ref: containerRef,
15123
15220
  className: "w-full space-y-2",
15124
15221
  children: [single && selectedNode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectorBar, {
15125
- agents,
15222
+ nodes,
15126
15223
  selected: selectedNode.agentNodeId,
15127
15224
  onSelect: onFallbackNodeChange
15128
15225
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
@@ -15134,7 +15231,8 @@ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, se
15134
15231
  selectedCell,
15135
15232
  onSetGate,
15136
15233
  gateBusy,
15137
- single: single && selectedNode !== null
15234
+ single: single && selectedNode !== null,
15235
+ hideGate
15138
15236
  })]
15139
15237
  });
15140
15238
  }
@@ -18850,8 +18948,6 @@ var usePipelineExecutorGetAvailableEngines = trpc.pipelineExecutor.getAvailableE
18850
18948
  var usePipelineExecutorGetSelectedEngine = trpc.pipelineExecutor.getSelectedEngine.useQuery;
18851
18949
  /** Generated alias around `trpc.pipelineExecutor.getDefaultSteps.useQuery`. */
18852
18950
  var usePipelineExecutorGetDefaultSteps = trpc.pipelineExecutor.getDefaultSteps.useQuery;
18853
- /** Generated alias around `trpc.pipelineExecutor.reprobeEngine.useMutation`. */
18854
- var usePipelineExecutorReprobeEngine = trpc.pipelineExecutor.reprobeEngine.useMutation;
18855
18951
  /** Generated alias around `trpc.pipelineExecutor.getEngineProvisioning.useQuery`. */
18856
18952
  var usePipelineExecutorGetEngineProvisioning = trpc.pipelineExecutor.getEngineProvisioning.useQuery;
18857
18953
  /** Generated alias around `trpc.pipelineExecutor.getVideoPipelineSteps.useQuery`. */
@@ -18922,6 +19018,10 @@ var usePipelineExecutorGetDetectionConfigSchema = trpc.pipelineExecutor.getDetec
18922
19018
  var usePipelineOrchestratorAssignPipeline = trpc.pipelineOrchestrator.assignPipeline.useMutation;
18923
19019
  /** Generated alias around `trpc.pipelineOrchestrator.unassignPipeline.useMutation`. */
18924
19020
  var usePipelineOrchestratorUnassignPipeline = trpc.pipelineOrchestrator.unassignPipeline.useMutation;
19021
+ /** Generated alias around `trpc.pipelineOrchestrator.setPipelineDevicePin.useMutation`. */
19022
+ var usePipelineOrchestratorSetPipelineDevicePin = trpc.pipelineOrchestrator.setPipelineDevicePin.useMutation;
19023
+ /** Generated alias around `trpc.pipelineOrchestrator.getPipelineDevicePin.useQuery`. */
19024
+ var usePipelineOrchestratorGetPipelineDevicePin = trpc.pipelineOrchestrator.getPipelineDevicePin.useQuery;
18925
19025
  /** Generated alias around `trpc.pipelineOrchestrator.rebalance.useMutation`. */
18926
19026
  var usePipelineOrchestratorRebalance = trpc.pipelineOrchestrator.rebalance.useMutation;
18927
19027
  /** Generated alias around `trpc.pipelineOrchestrator.getPipelineAssignments.useQuery`. */
@@ -18954,8 +19054,6 @@ var usePipelineOrchestratorGetAudioAssignments = trpc.pipelineOrchestrator.getAu
18954
19054
  var usePipelineOrchestratorGetAgentSettings = trpc.pipelineOrchestrator.getAgentSettings.useQuery;
18955
19055
  /** Generated alias around `trpc.pipelineOrchestrator.listAgentSettings.useQuery`. */
18956
19056
  var usePipelineOrchestratorListAgentSettings = trpc.pipelineOrchestrator.listAgentSettings.useQuery;
18957
- /** Generated alias around `trpc.pipelineOrchestrator.setAgentAddonDefaults.useMutation`. */
18958
- var usePipelineOrchestratorSetAgentAddonDefaults = trpc.pipelineOrchestrator.setAgentAddonDefaults.useMutation;
18959
19057
  /** Generated alias around `trpc.pipelineOrchestrator.removeAgentSettings.useMutation`. */
18960
19058
  var usePipelineOrchestratorRemoveAgentSettings = trpc.pipelineOrchestrator.removeAgentSettings.useMutation;
18961
19059
  /** Generated alias around `trpc.pipelineOrchestrator.setAgentMaxCameras.useMutation`. */
@@ -18968,6 +19066,8 @@ var usePipelineOrchestratorSetAgentCapabilities = trpc.pipelineOrchestrator.setA
18968
19066
  var usePipelineOrchestratorSetAgentReachableHost = trpc.pipelineOrchestrator.setAgentReachableHost.useMutation;
18969
19067
  /** Generated alias around `trpc.pipelineOrchestrator.setAgentInferenceDevices.useMutation`. */
18970
19068
  var usePipelineOrchestratorSetAgentInferenceDevices = trpc.pipelineOrchestrator.setAgentInferenceDevices.useMutation;
19069
+ /** Generated alias around `trpc.pipelineOrchestrator.getNodeInferenceDevices.useQuery`. */
19070
+ var usePipelineOrchestratorGetNodeInferenceDevices = trpc.pipelineOrchestrator.getNodeInferenceDevices.useQuery;
18971
19071
  /** Generated alias around `trpc.pipelineOrchestrator.resetNodePipelineDefaults.useMutation`. */
18972
19072
  var usePipelineOrchestratorResetNodePipelineDefaults = trpc.pipelineOrchestrator.resetNodePipelineDefaults.useMutation;
18973
19073
  /** Generated alias around `trpc.pipelineOrchestrator.getCameraSettings.useQuery`. */
@@ -28829,15 +28929,34 @@ function DeviceItemPreview({ trpc, device, status, enabled, showStatusPills = fa
28829
28929
  }
28830
28930
  //#endregion
28831
28931
  //#region src/composites/device-item/status-dot.tsx
28932
+ /** The states that breathe (are "alive"). Offline + disabled are static. */
28933
+ function isAlive(status) {
28934
+ return status === "online" || status === "recording-continuous" || status === "recording-events";
28935
+ }
28832
28936
  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}`;
28937
+ switch (status) {
28938
+ case "disabled": return "Disabled";
28939
+ case "online": return "Online";
28940
+ case "recording-continuous": return "Recording · always";
28941
+ case "recording-events": return "Recording · on motion";
28942
+ case "offline": {
28943
+ const lastSeen = formatLastSeen(lastChangedAt);
28944
+ return lastSeen === null ? "Offline" : `Offline · last seen ${lastSeen}`;
28945
+ }
28946
+ }
28947
+ }
28948
+ function colorClass(status) {
28949
+ switch (status) {
28950
+ case "disabled": return "bg-black ring-1 ring-foreground-subtle/60";
28951
+ case "offline": return "bg-foreground-subtle";
28952
+ case "recording-continuous": return "bg-danger";
28953
+ case "recording-events": return "bg-info";
28954
+ case "online": return "bg-success";
28955
+ }
28837
28956
  }
28838
28957
  function StatusDot({ status, lastChangedAt }) {
28839
28958
  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"),
28959
+ className: cn("h-1.5 w-1.5 rounded-full flex-shrink-0", colorClass(status), isAlive(status) && "animate-pulse"),
28841
28960
  title: resolveTitle(status, lastChangedAt)
28842
28961
  });
28843
28962
  }
@@ -47081,6 +47200,7 @@ exports.MediaPlayerHeroCard = MediaPlayerHeroCard;
47081
47200
  exports.MediaPlayerInlineControl = MediaPlayerInlineControl;
47082
47201
  exports.MediaPlayerPanel = MediaPlayerPanel;
47083
47202
  exports.MobileDrawer = MobileDrawer;
47203
+ exports.ModelPicker = ModelPicker;
47084
47204
  exports.MotionZonesSettings = MotionZonesSettings;
47085
47205
  exports.NodeMultiSelectField = NodeMultiSelectField;
47086
47206
  exports.NodePicker = NodePicker;
@@ -47182,6 +47302,7 @@ exports.WidgetPanel = WidgetPanel;
47182
47302
  exports.WidgetRegistryProvider = WidgetRegistryProvider;
47183
47303
  exports.WidgetSlot = WidgetSlot;
47184
47304
  exports.ZoneEditingProvider = ZoneEditingProvider;
47305
+ exports.agentColumnKey = agentColumnKey;
47185
47306
  exports.allDeviceTypeFilterOptions = allDeviceTypeFilterOptions;
47186
47307
  exports.buildStepTreeFromSchema = buildStepTreeFromSchema;
47187
47308
  exports.childEntityId = childEntityId;
@@ -47215,6 +47336,7 @@ exports.formatNumeric = formatNumeric;
47215
47336
  exports.fuzzyMatch = fuzzyMatch;
47216
47337
  exports.getClassColor = getClassColor;
47217
47338
  exports.getPhaseVisual = getPhaseVisual;
47339
+ exports.groupAgentColumns = groupAgentColumns;
47218
47340
  exports.groupChildrenByLayout = groupChildrenByLayout;
47219
47341
  exports.hardwareLabel = hardwareLabel;
47220
47342
  exports.humidifierTint = humidifierTint;
@@ -47820,7 +47942,6 @@ exports.usePipelineExecutorKillEngine = usePipelineExecutorKillEngine;
47820
47942
  exports.usePipelineExecutorListLoadedEngines = usePipelineExecutorListLoadedEngines;
47821
47943
  exports.usePipelineExecutorListReferenceImages = usePipelineExecutorListReferenceImages;
47822
47944
  exports.usePipelineExecutorListTemplates = usePipelineExecutorListTemplates;
47823
- exports.usePipelineExecutorReprobeEngine = usePipelineExecutorReprobeEngine;
47824
47945
  exports.usePipelineExecutorRunAudioTest = usePipelineExecutorRunAudioTest;
47825
47946
  exports.usePipelineExecutorRunPipeline = usePipelineExecutorRunPipeline;
47826
47947
  exports.usePipelineExecutorRunPipelineBatch = usePipelineExecutorRunPipelineBatch;
@@ -47849,8 +47970,10 @@ exports.usePipelineOrchestratorGetDeviceLiveContribution = usePipelineOrchestrat
47849
47970
  exports.usePipelineOrchestratorGetDeviceSettingsContribution = usePipelineOrchestratorGetDeviceSettingsContribution;
47850
47971
  exports.usePipelineOrchestratorGetGlobalMetrics = usePipelineOrchestratorGetGlobalMetrics;
47851
47972
  exports.usePipelineOrchestratorGetIngestOwner = usePipelineOrchestratorGetIngestOwner;
47973
+ exports.usePipelineOrchestratorGetNodeInferenceDevices = usePipelineOrchestratorGetNodeInferenceDevices;
47852
47974
  exports.usePipelineOrchestratorGetPipelineAssignment = usePipelineOrchestratorGetPipelineAssignment;
47853
47975
  exports.usePipelineOrchestratorGetPipelineAssignments = usePipelineOrchestratorGetPipelineAssignments;
47976
+ exports.usePipelineOrchestratorGetPipelineDevicePin = usePipelineOrchestratorGetPipelineDevicePin;
47854
47977
  exports.usePipelineOrchestratorListAgentSettings = usePipelineOrchestratorListAgentSettings;
47855
47978
  exports.usePipelineOrchestratorListTemplates = usePipelineOrchestratorListTemplates;
47856
47979
  exports.usePipelineOrchestratorRebalance = usePipelineOrchestratorRebalance;
@@ -47858,7 +47981,6 @@ exports.usePipelineOrchestratorRemoveAgentSettings = usePipelineOrchestratorRemo
47858
47981
  exports.usePipelineOrchestratorResetNodePipelineDefaults = usePipelineOrchestratorResetNodePipelineDefaults;
47859
47982
  exports.usePipelineOrchestratorResolvePipeline = usePipelineOrchestratorResolvePipeline;
47860
47983
  exports.usePipelineOrchestratorSaveTemplate = usePipelineOrchestratorSaveTemplate;
47861
- exports.usePipelineOrchestratorSetAgentAddonDefaults = usePipelineOrchestratorSetAgentAddonDefaults;
47862
47984
  exports.usePipelineOrchestratorSetAgentCapabilities = usePipelineOrchestratorSetAgentCapabilities;
47863
47985
  exports.usePipelineOrchestratorSetAgentDetectWeight = usePipelineOrchestratorSetAgentDetectWeight;
47864
47986
  exports.usePipelineOrchestratorSetAgentInferenceDevices = usePipelineOrchestratorSetAgentInferenceDevices;
@@ -47868,6 +47990,7 @@ exports.usePipelineOrchestratorSetCameraPipelineForAgent = usePipelineOrchestrat
47868
47990
  exports.usePipelineOrchestratorSetCameraStepOverride = usePipelineOrchestratorSetCameraStepOverride;
47869
47991
  exports.usePipelineOrchestratorSetCameraStepToggle = usePipelineOrchestratorSetCameraStepToggle;
47870
47992
  exports.usePipelineOrchestratorSetCapabilityBinding = usePipelineOrchestratorSetCapabilityBinding;
47993
+ exports.usePipelineOrchestratorSetPipelineDevicePin = usePipelineOrchestratorSetPipelineDevicePin;
47871
47994
  exports.usePipelineOrchestratorUnassignAudio = usePipelineOrchestratorUnassignAudio;
47872
47995
  exports.usePipelineOrchestratorUnassignPipeline = usePipelineOrchestratorUnassignPipeline;
47873
47996
  exports.usePipelineOrchestratorUpdateTemplate = usePipelineOrchestratorUpdateTemplate;