@camstack/ui-library 1.1.43 → 1.1.45

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
@@ -14426,7 +14426,7 @@ function buildDisplayStep(addon, cfg) {
14426
14426
  function computeEffective(mode, agentDefault, patch) {
14427
14427
  if (mode !== "device" || !patch) return agentDefault;
14428
14428
  return {
14429
- enabled: patch.enabled ?? agentDefault.enabled,
14429
+ enabled: agentDefault.enabled,
14430
14430
  modelId: patch.modelId ?? agentDefault.modelId,
14431
14431
  settings: patch.settings ?? agentDefault.settings
14432
14432
  };
@@ -14440,97 +14440,62 @@ function AgentModeEditor({ addon, agentDefault, modelsForFormat, onChangeAgentCo
14440
14440
  };
14441
14441
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14442
14442
  className: "space-y-4 text-xs",
14443
- children: [
14444
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
14445
- className: "flex items-center gap-2",
14446
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14447
- type: "checkbox",
14448
- checked: agentDefault.enabled,
14449
- onChange: (e) => update({ enabled: e.target.checked })
14450
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
14451
- "Enabled on this agent (",
14452
- addon.name,
14453
- ")"
14454
- ] })]
14455
- }),
14456
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14457
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
14458
- children: "Model"
14459
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
14460
- models: modelsForFormat,
14461
- value: agentDefault.modelId ?? "",
14462
- onChange: (modelId) => update({ modelId })
14463
- })] }),
14464
- (addon.configSchema?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14465
- className: "pt-2 border-t border-border/50 space-y-3",
14466
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14467
- className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
14468
- children: "Settings"
14469
- }), addon.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
14470
- field,
14471
- allFields: addon.configSchema ?? [],
14472
- step: buildDisplayStep(addon, agentDefault),
14473
- disabled: false,
14474
- onChange: (next) => update({ settings: { ...next.settings } })
14475
- }, field.key))]
14476
- })
14477
- ]
14443
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14444
+ className: "text-[10px] uppercase tracking-widest text-foreground-subtle mb-1",
14445
+ children: "Model"
14446
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
14447
+ models: modelsForFormat,
14448
+ value: agentDefault.modelId ?? "",
14449
+ onChange: (modelId) => update({ modelId })
14450
+ })] }), (addon.configSchema?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14451
+ className: "pt-2 border-t border-border/50 space-y-3",
14452
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14453
+ className: "text-[10px] uppercase tracking-widest text-foreground-subtle",
14454
+ children: "Settings"
14455
+ }), addon.configSchema?.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigSchemaField, {
14456
+ field,
14457
+ allFields: addon.configSchema ?? [],
14458
+ step: buildDisplayStep(addon, agentDefault),
14459
+ disabled: false,
14460
+ onChange: (next) => update({ settings: { ...next.settings } })
14461
+ }, field.key))]
14462
+ })]
14478
14463
  });
14479
14464
  }
14480
14465
  function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, modelsForFormat, onChangePatch }) {
14481
14466
  const effective = computeEffective("device", agentDefault, currentPatch);
14482
14467
  const commitPatch = (next) => {
14483
- const hasAnyField = next.enabled !== void 0 || next.modelId !== void 0 || next.settings !== void 0;
14468
+ const hasAnyField = next.modelId !== void 0 || next.settings !== void 0;
14484
14469
  onChangePatch?.(hasAnyField ? next : null);
14485
14470
  };
14486
- const setEnabled = (state) => {
14487
- const { enabled: _prev, ...rest } = currentPatch ?? {};
14488
- commitPatch(state === "inherit" ? rest : {
14489
- ...rest,
14490
- enabled: state === "on"
14491
- });
14492
- };
14493
14471
  const setModel = (modelId) => {
14494
- const { modelId: _prev, ...rest } = currentPatch ?? {};
14472
+ const { enabled: _enabled, modelId: _prev, ...rest } = currentPatch ?? {};
14495
14473
  commitPatch(modelId === void 0 ? rest : {
14496
14474
  ...rest,
14497
14475
  modelId
14498
14476
  });
14499
14477
  };
14500
14478
  const setSettings = (settings) => {
14501
- const { settings: _prev, ...rest } = currentPatch ?? {};
14479
+ const { enabled: _enabled, settings: _prev, ...rest } = currentPatch ?? {};
14502
14480
  commitPatch(settings === void 0 ? rest : {
14503
14481
  ...rest,
14504
14482
  settings
14505
14483
  });
14506
14484
  };
14507
- const enabledTri = currentPatch?.enabled === void 0 ? "inherit" : currentPatch.enabled ? "on" : "off";
14508
14485
  const modelOverridden = currentPatch?.modelId !== void 0;
14509
14486
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14510
14487
  className: "space-y-4 text-xs",
14511
14488
  children: [
14512
14489
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14513
- className: "flex items-center gap-1",
14514
- children: [[
14515
- "inherit",
14516
- "on",
14517
- "off"
14518
- ].map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14519
- type: "button",
14520
- onClick: () => setEnabled(s),
14521
- className: cn("px-3 py-1 border border-border rounded text-xs capitalize", enabledTri === s ? "bg-primary text-primary-foreground border-primary" : "bg-surface text-foreground hover:bg-muted"),
14522
- children: s
14523
- }, s)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14524
- className: "ml-2 text-[10px] text-foreground-subtle",
14525
- children: [
14526
- "agent default:",
14527
- " ",
14528
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14529
- className: "font-semibold",
14530
- children: agentDefault.enabled ? "on" : "off"
14531
- })
14532
- ]
14533
- })]
14490
+ className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
14491
+ children: [
14492
+ "Provisioning for ",
14493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14494
+ className: "font-semibold text-foreground",
14495
+ children: agentNodeId
14496
+ }),
14497
+ " model + settings only. Whether this camera runs the step is set by its per-camera gate above."
14498
+ ]
14534
14499
  }),
14535
14500
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
14536
14501
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -14601,15 +14566,13 @@ function DeviceModeEditor({ addon, agentDefault, agentNodeId, currentPatch, mode
14601
14566
  className: "rounded bg-muted/40 px-3 py-2 text-[11px] text-foreground-subtle",
14602
14567
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14603
14568
  className: "font-semibold text-foreground mb-0.5",
14604
- children: "Effective config preview"
14569
+ children: "Effective provisioning preview"
14605
14570
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14606
14571
  className: "font-mono truncate",
14607
14572
  children: [
14608
14573
  addon.id,
14609
14574
  " · model: ",
14610
- effective.modelId,
14611
- " · ",
14612
- effective.enabled ? "on" : "off"
14575
+ effective.modelId ?? "Auto"
14613
14576
  ]
14614
14577
  })]
14615
14578
  })
@@ -14634,19 +14597,6 @@ function AgentStepEditor(props) {
14634
14597
  onChangePatch
14635
14598
  });
14636
14599
  }
14637
- //#endregion
14638
- //#region src/composites/pipeline-tree-matrix.tsx
14639
- /**
14640
- * Fixed column geometry (rem). The matrix uses a CSS grid with STATIC track
14641
- * widths so a cell's content (a status chip, a long model id) can never resize
14642
- * a column or shift its neighbours — cell content is truncated WITHIN the fixed
14643
- * track instead. This kills the "table dances when the last column's status
14644
- * changes" jitter. See `min-w-0` on every grid child, which lets the child
14645
- * shrink to the track (a grid item's automatic minimum is its content size, so
14646
- * without this a non-wrapping model id would blow the track wider than its max).
14647
- */
14648
- var STEP_COL_REM = 18;
14649
- var AGENT_COL_REM = 13;
14650
14600
  function flattenTree(nodes) {
14651
14601
  const out = [];
14652
14602
  const walk = (list, depth) => {
@@ -14661,6 +14611,20 @@ function flattenTree(nodes) {
14661
14611
  walk(nodes, 0);
14662
14612
  return out;
14663
14613
  }
14614
+ /**
14615
+ * Layout decision for the responsive A↔B switch. Pure so it can be unit-tested
14616
+ * in isolation from the DOM.
14617
+ *
14618
+ * Returns `true` (single-node fallback) when there is MORE than one node but
14619
+ * the container is too narrow to show the step column plus at least TWO node
14620
+ * columns side by side. An unmeasured container (`width <= 0`) defaults to the
14621
+ * wide matrix so there is no first-paint flash into the fallback.
14622
+ */
14623
+ function shouldUseSingleNode(width, nodeCount, stepColPx, agentColPx = 208) {
14624
+ if (nodeCount <= 1) return false;
14625
+ if (width <= 0) return false;
14626
+ return width < stepColPx + agentColPx * 2;
14627
+ }
14664
14628
  var SLOT_LABEL = {
14665
14629
  detector: {
14666
14630
  label: "detector",
@@ -14711,25 +14675,26 @@ function ClassChips({ inputs, outputs }) {
14711
14675
  ]
14712
14676
  });
14713
14677
  }
14714
- function QuickToggle({ enabled, onChange, disabled }) {
14715
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14716
- type: "button",
14717
- role: "switch",
14718
- "aria-checked": enabled,
14719
- disabled,
14720
- onClick: (e) => {
14721
- e.stopPropagation();
14722
- onChange(!enabled);
14723
- },
14724
- className: cn("relative inline-flex h-3.5 w-7 shrink-0 items-center rounded-full transition-colors", enabled ? "bg-emerald-500" : "bg-foreground-subtle/30", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
14725
- title: enabled ? "Click to disable" : "Click to enable",
14726
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-3 w-3 rounded-full bg-white shadow transition-transform", enabled ? "translate-x-3.5" : "translate-x-0.5") })
14678
+ /** Step label (slot heading + addon name + class chips), shared by both views. */
14679
+ function StepLabel({ node }) {
14680
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14681
+ className: "flex flex-col gap-1 min-w-0 flex-1",
14682
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotHeading, { slot: node.slot }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14683
+ className: "flex items-center gap-2 flex-wrap",
14684
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14685
+ className: "font-semibold truncate",
14686
+ children: node.addonName
14687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClassChips, {
14688
+ inputs: node.inputClasses,
14689
+ outputs: node.outputClasses
14690
+ })]
14691
+ })]
14727
14692
  });
14728
14693
  }
14729
14694
  /**
14730
- * Cell content. Kept width-stable: the status dot is `shrink-0` (reserved
14731
- * space) and the model id truncates inside a `min-w-0` flex row, so a longer
14732
- * model id never widens the cell — it ellipsises instead.
14695
+ * Cell content. Width-stable: the status dot is `shrink-0` (reserved space) and
14696
+ * the model id truncates inside a `min-w-0` flex row, so a longer model id never
14697
+ * widens the cell — it ellipsises instead.
14733
14698
  */
14734
14699
  function renderCellContent(state) {
14735
14700
  if (state.kind === "enabled") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -14762,31 +14727,41 @@ function renderCellContent(state) {
14762
14727
  })
14763
14728
  ]
14764
14729
  });
14730
+ if (state.kind === "skip") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14731
+ className: "flex min-w-0 items-center gap-1 text-amber-500",
14732
+ title: "Gated on for this camera, but no compatible model on this node — click to provision",
14733
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14734
+ className: "truncate font-medium",
14735
+ children: "⚠ skip"
14736
+ })]
14737
+ });
14765
14738
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
14766
14739
  className: "flex min-w-0 items-center gap-1 text-foreground-subtle/70",
14767
14740
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-muted shrink-0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14768
14741
  className: "truncate",
14769
- children: "n/a"
14742
+ children: ""
14770
14743
  })]
14771
14744
  });
14772
14745
  }
14773
14746
  function cellButtonClass(state, isSelected, extra) {
14774
- return cn("text-left text-xs hover:bg-muted/40", state.kind === "enabled" && "bg-emerald-500/5", state.kind === "na" && "bg-muted/30", isSelected && "ring-2 ring-primary ring-inset", extra);
14747
+ return cn("text-left text-xs hover:bg-muted/40", state.kind === "enabled" && "bg-emerald-500/5", state.kind === "skip" && "bg-amber-500/5", state.kind === "na" && "bg-muted/30", isSelected && "ring-2 ring-primary ring-inset", extra);
14775
14748
  }
14776
- /** Step label (slot heading + addon name + class chips), shared by both views. */
14777
- function StepLabel({ node }) {
14778
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14779
- className: "flex flex-col gap-1 min-w-0 flex-1",
14780
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlotHeading, { slot: node.slot }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14781
- className: "flex items-center gap-2 flex-wrap",
14782
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14783
- className: "font-semibold truncate",
14784
- children: node.addonName
14785
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClassChips, {
14786
- inputs: node.inputClasses,
14787
- outputs: node.outputClasses
14788
- })]
14789
- })]
14749
+ //#endregion
14750
+ //#region src/composites/pipeline-tree-matrix.tsx
14751
+ var STEP_COL_REM$1 = 18;
14752
+ function QuickToggle({ enabled, onChange, disabled }) {
14753
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14754
+ type: "button",
14755
+ role: "switch",
14756
+ "aria-checked": enabled,
14757
+ disabled,
14758
+ onClick: (e) => {
14759
+ e.stopPropagation();
14760
+ onChange(!enabled);
14761
+ },
14762
+ className: cn("relative inline-flex h-3.5 w-7 shrink-0 items-center rounded-full transition-colors", enabled ? "bg-emerald-500" : "bg-foreground-subtle/30", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"),
14763
+ title: enabled ? "Click to disable" : "Click to enable",
14764
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cn("inline-block h-3 w-3 rounded-full bg-white shadow transition-transform", enabled ? "translate-x-3.5" : "translate-x-0.5") })
14790
14765
  });
14791
14766
  }
14792
14767
  function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell, toggleProps }) {
@@ -14814,11 +14789,11 @@ function GridRow({ node, depth, agents, getCellState, onCellClick, selectedCell,
14814
14789
  /**
14815
14790
  * Wide layout: horizontally self-scrolling matrix. The scroll lives INSIDE
14816
14791
  * this `overflow-auto` box (sticky first column + sticky header row) so the
14817
- * page body never scrolls sideways. Column tracks are fixed px widths see
14818
- * STEP_COL_REM / AGENT_COL_REM — so the grid is static regardless of content.
14792
+ * page body never scrolls sideways. Column tracks are fixed px widths so the
14793
+ * grid is static regardless of content.
14819
14794
  */
14820
- function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
14821
- const gridTemplate = `${STEP_COL_REM}rem repeat(${agents.length}, ${AGENT_COL_REM}rem)`;
14795
+ function MatrixGrid$1({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
14796
+ const gridTemplate = `${STEP_COL_REM$1}rem repeat(${agents.length}, 13rem)`;
14822
14797
  const showToggle = onToggleEnabled !== void 0 && agents.length === 1;
14823
14798
  const onlyAgent = agents[0]?.agentNodeId;
14824
14799
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -14863,9 +14838,7 @@ function MatrixGrid({ rows, agents, getCellState, onCellClick, selectedCell, onT
14863
14838
  }
14864
14839
  /**
14865
14840
  * Narrow layout: one card per agent column, each listing every step stacked
14866
- * vertically. A wide fixed matrix is unusable on a phone; here the same cells
14867
- * (click-to-edit, status, toggle) reflow into full-width rows so nothing scrolls
14868
- * horizontally. Feature-parity with the matrix — same `onCellClick`, same
14841
+ * vertically. Feature-parity with the matrix same `onCellClick`, same
14869
14842
  * `renderCellContent`, same single-agent `onToggleEnabled`.
14870
14843
  */
14871
14844
  function StackedCards({ rows, agents, getCellState, onCellClick, selectedCell, onToggleEnabled }) {
@@ -14934,7 +14907,7 @@ function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedC
14934
14907
  })
14935
14908
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14936
14909
  className: "hidden @2xl/matrix:block",
14937
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
14910
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid$1, {
14938
14911
  rows,
14939
14912
  agents,
14940
14913
  getCellState,
@@ -14946,6 +14919,226 @@ function PipelineTreeMatrix({ tree, agents, getCellState, onCellClick, selectedC
14946
14919
  });
14947
14920
  }
14948
14921
  //#endregion
14922
+ //#region src/composites/device-step-matrix.tsx
14923
+ /**
14924
+ * DeviceStepMatrix — the per-camera "Detection pipeline → Steps" surface (#33)
14925
+ * rendered as ONE hierarchical steps×nodes matrix.
14926
+ *
14927
+ * Column 1 (STICKY): the indented step tree. Each step carries its slot +
14928
+ * name + class-flow chips PLUS a per-camera, node-independent GATE control
14929
+ * with THREE states — On / Off / Default (inherit). `package-detection` is
14930
+ * zone-driven, so it shows a "Zones" badge instead of a gate.
14931
+ * Columns 2..N (one per online node): the provisioned model chip (click →
14932
+ * the model/config picker), a clickable "⚠ skip" when the step is gated ON
14933
+ * but the node has no compatible model, or "—" when N/A.
14934
+ *
14935
+ * Responsive, container-measured (never a global media query, so it also works
14936
+ * embedded):
14937
+ * A) wide — sticky step column + node columns in an `overflow-x:auto` box, so
14938
+ * narrow viewports scroll the NODES sideways while col-1 stays pinned and
14939
+ * the page body never scrolls horizontally.
14940
+ * B) narrow — below the width needed for ≥2 node columns, collapse to a
14941
+ * single-node view: a node selector picks ONE node and the matrix renders
14942
+ * `step-tree × that-one-node`.
14943
+ */
14944
+ /** Wider than the cluster matrix's 18rem: col-1 also hosts the gate control. */
14945
+ var STEP_COL_REM = 21;
14946
+ var STEP_COL_PX = STEP_COL_REM * 16;
14947
+ function activeChoice(gate) {
14948
+ if (!gate.explicit) return "default";
14949
+ return gate.effectiveOn ? "on" : "off";
14950
+ }
14951
+ /**
14952
+ * Tri-state segmented control. `Default` inherits the catalog value (shown in
14953
+ * the caption); `On`/`Off` pin an explicit per-camera override. Touch-friendly
14954
+ * 28px segments.
14955
+ */
14956
+ function GateControl({ gate, busy, onSet }) {
14957
+ const active = activeChoice(gate);
14958
+ const seg = (choice, label, value, activeClass) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14959
+ type: "button",
14960
+ "aria-pressed": active === choice,
14961
+ disabled: busy,
14962
+ onClick: () => onSet(value),
14963
+ className: cn("flex-1 px-2 py-1 text-[11px] font-medium leading-none transition-colors", "min-h-[26px]", active === choice ? activeClass : "text-foreground-subtle hover:bg-muted/60", busy && "opacity-50 cursor-not-allowed"),
14964
+ children: label
14965
+ });
14966
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14967
+ className: "flex flex-col gap-1",
14968
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14969
+ className: "inline-flex w-fit max-w-full self-start overflow-hidden rounded-md border border-border",
14970
+ children: [
14971
+ seg("default", "Auto", null, "bg-primary/15 text-primary"),
14972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
14973
+ seg("on", "On", true, "bg-emerald-500 text-white"),
14974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-px self-stretch bg-border" }),
14975
+ seg("off", "Off", false, "bg-foreground-subtle/70 text-white")
14976
+ ]
14977
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14978
+ className: "text-[10px] leading-none text-foreground-subtle",
14979
+ children: active === "default" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
14980
+ "= default (",
14981
+ gate.defaultOn ? "on" : "off",
14982
+ ")"
14983
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: ["overridden · default ", gate.defaultOn ? "on" : "off"] })
14984
+ })]
14985
+ });
14986
+ }
14987
+ function ZoneBadge() {
14988
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14989
+ className: "flex flex-col gap-1",
14990
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14991
+ className: "inline-flex w-fit items-center gap-1 rounded-md border border-violet-500/40 bg-violet-500/10 px-2 py-1 text-[11px] font-medium text-violet-400",
14992
+ children: "Zones"
14993
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14994
+ className: "text-[10px] leading-none text-foreground-subtle",
14995
+ children: "driven by package zones"
14996
+ })]
14997
+ });
14998
+ }
14999
+ function StepGateCell({ node, depth, gate, busy, onSet }) {
15000
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15001
+ 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
+ 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, {
15004
+ gate,
15005
+ busy,
15006
+ onSet
15007
+ })]
15008
+ });
15009
+ }
15010
+ /**
15011
+ * 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
15013
+ * 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.
15015
+ */
15016
+ function NodeSelectorBar({ agents, selected, onSelect }) {
15017
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15018
+ className: "flex flex-wrap items-center gap-1.5",
15019
+ role: "tablist",
15020
+ "aria-label": "Node",
15021
+ children: [
15022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Server, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
15023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
15024
+ className: "text-[10px] uppercase tracking-wider text-foreground-subtle",
15025
+ children: "Node"
15026
+ }),
15027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15028
+ className: "flex overflow-hidden rounded-md border border-border text-[11px] font-medium",
15029
+ children: agents.map((a, idx) => {
15030
+ const isSelected = a.agentNodeId === selected;
15031
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15032
+ type: "button",
15033
+ role: "tab",
15034
+ "aria-selected": isSelected,
15035
+ onClick: () => onSelect(a.agentNodeId),
15036
+ title: `${a.agentNodeId} · ${a.engineLabel}`,
15037
+ className: cn("px-2.5 py-1 transition-colors", isSelected ? "bg-primary/15 text-primary" : "bg-transparent text-foreground-subtle hover:bg-surface-hover hover:text-foreground", idx > 0 && "border-l border-border"),
15038
+ children: a.agentNodeId
15039
+ }, a.agentNodeId);
15040
+ })
15041
+ })
15042
+ ]
15043
+ });
15044
+ }
15045
+ function MatrixGrid({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, single }) {
15046
+ const rows = (0, react$1.useMemo)(() => flattenTree(tree), [tree]);
15047
+ const gridTemplate = single ? `minmax(0, 1.35fr) repeat(${agents.length}, minmax(0, 1fr))` : `${STEP_COL_REM}rem repeat(${agents.length}, 13rem)`;
15048
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15049
+ className: cn("rounded border border-border", single ? "overflow-hidden" : "overflow-auto"),
15050
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15051
+ className: cn("grid", single ? "w-full" : "w-max"),
15052
+ style: { gridTemplateColumns: gridTemplate },
15053
+ children: [
15054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15055
+ 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"
15057
+ }),
15058
+ agents.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15059
+ className: "sticky top-0 z-10 min-w-0 border-b border-l border-border bg-muted/60 px-3 py-2",
15060
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15061
+ className: "truncate text-xs font-semibold text-foreground",
15062
+ children: a.agentNodeId
15063
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
15064
+ className: "truncate text-[10px] text-foreground-subtle",
15065
+ children: a.engineLabel
15066
+ })]
15067
+ }, a.agentNodeId)),
15068
+ rows.map(({ node, depth }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixRow, {
15069
+ node,
15070
+ depth,
15071
+ agents,
15072
+ gate: gateFor(node.addonId),
15073
+ getCellState,
15074
+ onCellClick,
15075
+ selectedCell,
15076
+ onSetGate,
15077
+ gateBusy
15078
+ }, node.addonId))
15079
+ ]
15080
+ })
15081
+ });
15082
+ }
15083
+ function MatrixRow({ node, depth, agents, gate, getCellState, onCellClick, selectedCell, onSetGate, gateBusy }) {
15084
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepGateCell, {
15085
+ node,
15086
+ depth,
15087
+ gate,
15088
+ busy: gateBusy,
15089
+ onSet: (next) => onSetGate(node.addonId, next)
15090
+ }), agents.map((a) => {
15091
+ const state = getCellState(node.addonId, a.agentNodeId);
15092
+ const isSelected = selectedCell?.addonId === node.addonId && selectedCell.agentNodeId === a.agentNodeId;
15093
+ const actionable = state.kind === "enabled" || state.kind === "skip";
15094
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
15095
+ type: "button",
15096
+ disabled: !actionable,
15097
+ onClick: () => onCellClick(node.addonId, a.agentNodeId),
15098
+ 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
+ children: renderCellContent(state)
15100
+ }, a.agentNodeId);
15101
+ })] });
15102
+ }
15103
+ function DeviceStepMatrix({ tree, agents, gateFor, getCellState, onCellClick, selectedCell, onSetGate, gateBusy, fallbackNodeId, onFallbackNodeChange }) {
15104
+ const containerRef = (0, react$1.useRef)(null);
15105
+ const [width, setWidth] = (0, react$1.useState)(0);
15106
+ (0, react$1.useLayoutEffect)(() => {
15107
+ const el = containerRef.current;
15108
+ if (!el) return;
15109
+ const measure = () => setWidth(el.getBoundingClientRect().width);
15110
+ measure();
15111
+ const RO = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
15112
+ RO?.observe(el);
15113
+ return () => RO?.disconnect();
15114
+ }, []);
15115
+ const single = shouldUseSingleNode(width, agents.length, STEP_COL_PX);
15116
+ 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;
15121
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
15122
+ ref: containerRef,
15123
+ className: "w-full space-y-2",
15124
+ children: [single && selectedNode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeSelectorBar, {
15125
+ agents,
15126
+ selected: selectedNode.agentNodeId,
15127
+ onSelect: onFallbackNodeChange
15128
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MatrixGrid, {
15129
+ tree,
15130
+ agents: shownAgents,
15131
+ gateFor,
15132
+ getCellState,
15133
+ onCellClick,
15134
+ selectedCell,
15135
+ onSetGate,
15136
+ gateBusy,
15137
+ single: single && selectedNode !== null
15138
+ })]
15139
+ });
15140
+ }
15141
+ //#endregion
14949
15142
  //#region src/trpc-react.ts
14950
15143
  /**
14951
15144
  * Shared typed React Query proxy for the backend AppRouter.
@@ -18773,6 +18966,8 @@ var usePipelineOrchestratorSetAgentDetectWeight = trpc.pipelineOrchestrator.setA
18773
18966
  var usePipelineOrchestratorSetAgentCapabilities = trpc.pipelineOrchestrator.setAgentCapabilities.useMutation;
18774
18967
  /** Generated alias around `trpc.pipelineOrchestrator.setAgentReachableHost.useMutation`. */
18775
18968
  var usePipelineOrchestratorSetAgentReachableHost = trpc.pipelineOrchestrator.setAgentReachableHost.useMutation;
18969
+ /** Generated alias around `trpc.pipelineOrchestrator.setAgentInferenceDevices.useMutation`. */
18970
+ var usePipelineOrchestratorSetAgentInferenceDevices = trpc.pipelineOrchestrator.setAgentInferenceDevices.useMutation;
18776
18971
  /** Generated alias around `trpc.pipelineOrchestrator.resetNodePipelineDefaults.useMutation`. */
18777
18972
  var usePipelineOrchestratorResetNodePipelineDefaults = trpc.pipelineOrchestrator.resetNodePipelineDefaults.useMutation;
18778
18973
  /** Generated alias around `trpc.pipelineOrchestrator.getCameraSettings.useQuery`. */
@@ -26533,131 +26728,117 @@ function ControlHeroCard({ trpc, deviceId, features, optimistic }) {
26533
26728
  }
26534
26729
  //#endregion
26535
26730
  //#region src/composites/device-controls/control-registry.tsx
26536
- var COVER_CONTROL = {
26537
- InlineControl: CoverInlineControl,
26538
- HeroCard: CoverHeroCard
26539
- };
26540
- var DUMMY_CONTROL = {
26541
- InlineControl: DummyInline,
26542
- HeroCard: DummyHeroCard
26543
- };
26544
- var SENSOR_CONTROL = {
26545
- InlineControl: SensorInlineControl,
26546
- HeroCard: SensorHeroCard
26547
- };
26548
- var UPDATE_CONTROL = {
26549
- InlineControl: UpdateInlineControl,
26550
- HeroCard: UpdateHeroCard
26551
- };
26552
- var SWITCH_CONTROL = {
26553
- InlineControl: SwitchInlineControl,
26554
- HeroCard: SwitchHeroCard
26555
- };
26556
- var LOCK_CONTROL = {
26557
- InlineControl: LockInlineControl,
26558
- HeroCard: LockHeroCard
26559
- };
26560
- var LIGHT_CONTROL = {
26561
- InlineControl: LightInlineControl,
26562
- HeroCard: LightHeroCard
26563
- };
26564
- var FAN_CONTROL = {
26565
- InlineControl: FanInlineControl,
26566
- HeroCard: FanHeroCard
26567
- };
26568
- var THERMOSTAT_CONTROL = {
26569
- InlineControl: ThermostatInlineControl,
26570
- HeroCard: ThermostatHeroCard
26571
- };
26572
- var CLIMATE_CONTROL = {
26573
- InlineControl: AcInlineControl,
26574
- HeroCard: AcHeroCard
26575
- };
26576
- var MEDIA_PLAYER_CONTROL = {
26577
- InlineControl: MediaPlayerInlineControl,
26578
- HeroCard: MediaPlayerHeroCard
26579
- };
26580
- var ALARM_PANEL_CONTROL = {
26581
- InlineControl: AlarmInlineControl,
26582
- HeroCard: AlarmHeroCard
26583
- };
26584
- var CONTROL_CONTROL = {
26585
- InlineControl: ControlInlineControl,
26586
- HeroCard: ControlHeroCard
26587
- };
26588
- var VACUUM_CONTROL = {
26589
- InlineControl: VacuumInlineControl,
26590
- HeroCard: VacuumHeroCard
26591
- };
26592
- var LAWN_MOWER_CONTROL = {
26593
- InlineControl: LawnMowerInlineControl,
26594
- HeroCard: LawnMowerHeroCard
26595
- };
26596
- var VALVE_CONTROL = {
26597
- InlineControl: ValveInlineControl,
26598
- HeroCard: ValveHeroCard
26599
- };
26600
- var HUMIDIFIER_CONTROL = {
26601
- InlineControl: HumidifierInlineControl,
26602
- HeroCard: HumidifierHeroCard
26603
- };
26604
- var WATER_HEATER_CONTROL = {
26605
- InlineControl: WaterHeaterInlineControl,
26606
- HeroCard: WaterHeaterHeroCard
26607
- };
26608
- var WEATHER_CONTROL = {
26609
- InlineControl: WeatherInlineControl,
26610
- HeroCard: WeatherHeroCard
26611
- };
26612
- var IMAGE_CONTROL = {
26613
- InlineControl: ImageInlineControl,
26614
- HeroCard: ImageHeroCard
26615
- };
26616
- var PET_FEEDER_CONTROL = {
26617
- InlineControl: PetFeederInlineControl,
26618
- HeroCard: PetFeederHeroCard
26619
- };
26620
26731
  /**
26621
- * Exhaustive record over DeviceType. Omitting a member causes a
26622
- * TypeScript build error (same pattern as DEVICE_TYPE_ACTIONS).
26623
- */
26624
- var DEVICE_TYPE_CONTROL = {
26625
- [_camstack_types.DeviceType.Cover]: COVER_CONTROL,
26626
- [_camstack_types.DeviceType.Valve]: VALVE_CONTROL,
26627
- [_camstack_types.DeviceType.Humidifier]: HUMIDIFIER_CONTROL,
26628
- [_camstack_types.DeviceType.WaterHeater]: WATER_HEATER_CONTROL,
26629
- [_camstack_types.DeviceType.Camera]: DUMMY_CONTROL,
26630
- [_camstack_types.DeviceType.Hub]: DUMMY_CONTROL,
26631
- [_camstack_types.DeviceType.Switch]: SWITCH_CONTROL,
26632
- [_camstack_types.DeviceType.Siren]: SWITCH_CONTROL,
26633
- [_camstack_types.DeviceType.Light]: LIGHT_CONTROL,
26634
- [_camstack_types.DeviceType.Fan]: FAN_CONTROL,
26635
- [_camstack_types.DeviceType.Sensor]: SENSOR_CONTROL,
26636
- [_camstack_types.DeviceType.Thermostat]: THERMOSTAT_CONTROL,
26637
- [_camstack_types.DeviceType.Climate]: CLIMATE_CONTROL,
26638
- [_camstack_types.DeviceType.Button]: {
26732
+ * Presentation binding: the shared `DeviceControlKind` (decided in
26733
+ * `@camstack/types` `resolveDeviceControlKind`) this platform's web
26734
+ * components. Exhaustive over `DeviceControlKind` — omitting a kind fails the
26735
+ * build here. The type→kind DECISION is single-sourced in `@camstack/types`;
26736
+ * this map only binds a kind to its web widgets.
26737
+ */
26738
+ var CONTROL_KIND_COMPONENTS = {
26739
+ cover: {
26740
+ InlineControl: CoverInlineControl,
26741
+ HeroCard: CoverHeroCard
26742
+ },
26743
+ valve: {
26744
+ InlineControl: ValveInlineControl,
26745
+ HeroCard: ValveHeroCard
26746
+ },
26747
+ humidifier: {
26748
+ InlineControl: HumidifierInlineControl,
26749
+ HeroCard: HumidifierHeroCard
26750
+ },
26751
+ "water-heater": {
26752
+ InlineControl: WaterHeaterInlineControl,
26753
+ HeroCard: WaterHeaterHeroCard
26754
+ },
26755
+ switch: {
26756
+ InlineControl: SwitchInlineControl,
26757
+ HeroCard: SwitchHeroCard
26758
+ },
26759
+ light: {
26760
+ InlineControl: LightInlineControl,
26761
+ HeroCard: LightHeroCard
26762
+ },
26763
+ fan: {
26764
+ InlineControl: FanInlineControl,
26765
+ HeroCard: FanHeroCard
26766
+ },
26767
+ sensor: {
26768
+ InlineControl: SensorInlineControl,
26769
+ HeroCard: SensorHeroCard
26770
+ },
26771
+ thermostat: {
26772
+ InlineControl: ThermostatInlineControl,
26773
+ HeroCard: ThermostatHeroCard
26774
+ },
26775
+ climate: {
26776
+ InlineControl: AcInlineControl,
26777
+ HeroCard: AcHeroCard
26778
+ },
26779
+ button: {
26639
26780
  InlineControl: ButtonControl,
26640
26781
  HeroCard: ButtonHeroCard
26641
26782
  },
26642
- [_camstack_types.DeviceType.EventEmitter]: DUMMY_CONTROL,
26643
- [_camstack_types.DeviceType.Update]: UPDATE_CONTROL,
26644
- [_camstack_types.DeviceType.Generic]: DUMMY_CONTROL,
26645
- [_camstack_types.DeviceType.Notifier]: DUMMY_CONTROL,
26646
- [_camstack_types.DeviceType.Script]: DUMMY_CONTROL,
26647
- [_camstack_types.DeviceType.Automation]: DUMMY_CONTROL,
26648
- [_camstack_types.DeviceType.Lock]: LOCK_CONTROL,
26649
- [_camstack_types.DeviceType.MediaPlayer]: MEDIA_PLAYER_CONTROL,
26650
- [_camstack_types.DeviceType.AlarmPanel]: ALARM_PANEL_CONTROL,
26651
- [_camstack_types.DeviceType.Control]: CONTROL_CONTROL,
26652
- [_camstack_types.DeviceType.Presence]: SENSOR_CONTROL,
26653
- [_camstack_types.DeviceType.Weather]: WEATHER_CONTROL,
26654
- [_camstack_types.DeviceType.Vacuum]: VACUUM_CONTROL,
26655
- [_camstack_types.DeviceType.LawnMower]: LAWN_MOWER_CONTROL,
26656
- [_camstack_types.DeviceType.Container]: DUMMY_CONTROL,
26657
- [_camstack_types.DeviceType.Image]: IMAGE_CONTROL,
26658
- [_camstack_types.DeviceType.PetFeeder]: PET_FEEDER_CONTROL
26783
+ update: {
26784
+ InlineControl: UpdateInlineControl,
26785
+ HeroCard: UpdateHeroCard
26786
+ },
26787
+ lock: {
26788
+ InlineControl: LockInlineControl,
26789
+ HeroCard: LockHeroCard
26790
+ },
26791
+ "media-player": {
26792
+ InlineControl: MediaPlayerInlineControl,
26793
+ HeroCard: MediaPlayerHeroCard
26794
+ },
26795
+ alarm: {
26796
+ InlineControl: AlarmInlineControl,
26797
+ HeroCard: AlarmHeroCard
26798
+ },
26799
+ control: {
26800
+ InlineControl: ControlInlineControl,
26801
+ HeroCard: ControlHeroCard
26802
+ },
26803
+ weather: {
26804
+ InlineControl: WeatherInlineControl,
26805
+ HeroCard: WeatherHeroCard
26806
+ },
26807
+ vacuum: {
26808
+ InlineControl: VacuumInlineControl,
26809
+ HeroCard: VacuumHeroCard
26810
+ },
26811
+ "lawn-mower": {
26812
+ InlineControl: LawnMowerInlineControl,
26813
+ HeroCard: LawnMowerHeroCard
26814
+ },
26815
+ image: {
26816
+ InlineControl: ImageInlineControl,
26817
+ HeroCard: ImageHeroCard
26818
+ },
26819
+ "pet-feeder": {
26820
+ InlineControl: PetFeederInlineControl,
26821
+ HeroCard: PetFeederHeroCard
26822
+ },
26823
+ dummy: {
26824
+ InlineControl: DummyInline,
26825
+ HeroCard: DummyHeroCard
26826
+ }
26659
26827
  };
26660
26828
  /**
26829
+ * Exhaustive record over DeviceType, DERIVED from the shared decision
26830
+ * (`DEVICE_TYPE_CONTROL_KIND`) composed with the web `CONTROL_KIND_COMPONENTS`
26831
+ * binding — so the type→control mapping can never diverge from the single
26832
+ * source in `@camstack/types`. Preserved as a public export for consumers/tests
26833
+ * that index it by `DeviceType`.
26834
+ *
26835
+ * The `Object.fromEntries` result is a `{ [k: string]: DeviceTypeControl }`;
26836
+ * `Object.values(DeviceType)` (a string enum) yields exactly the enum's members,
26837
+ * so the record is fully populated — the assertion narrows that documented
26838
+ * enum-record projection boundary.
26839
+ */
26840
+ var DEVICE_TYPE_CONTROL = Object.fromEntries(Object.values(_camstack_types.DeviceType).map((type) => [type, CONTROL_KIND_COMPONENTS[_camstack_types.DEVICE_TYPE_CONTROL_KIND[type]]]));
26841
+ /**
26661
26842
  * Per-type FILL flag.
26662
26843
  *
26663
26844
  * true — range / choice / button controls (cover ▲■▼ triad, thermostat
@@ -26711,12 +26892,14 @@ var CONTROL_FILLS = {
26711
26892
  };
26712
26893
  var DEVICE_TYPE_VALUES$1 = new Set(Object.values(_camstack_types.DeviceType));
26713
26894
  /**
26714
- * Runtime-string-safe lookup: a device row's `type` crosses the wire
26715
- * as a plain string, so an unrecognised value resolves to null instead
26716
- * of indexing the registry with an unverified key.
26895
+ * Runtime-string-safe lookup: a device row's `type` crosses the wire as a plain
26896
+ * string. The type→control-kind DECISION is single-sourced in `@camstack/types`
26897
+ * (`resolveDeviceControlKind`, null for an unrecognised type); this maps the
26898
+ * resolved kind onto the web components.
26717
26899
  */
26718
26900
  function resolveDeviceControl(type) {
26719
- return DEVICE_TYPE_VALUES$1.has(type) ? DEVICE_TYPE_CONTROL[type] : null;
26901
+ const kind = (0, _camstack_types.resolveDeviceControlKind)(type);
26902
+ return kind ? CONTROL_KIND_COMPONENTS[kind] : null;
26720
26903
  }
26721
26904
  /**
26722
26905
  * Runtime-string-safe alignment lookup for the fixed-width control column.
@@ -40488,8 +40671,30 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40488
40671
  const pc = new RTCPeerConnection({ iceServers: [...iceServers ?? [], { urls: "stun:stun.l.google.com:19302" }] });
40489
40672
  const newStream = new MediaStream();
40490
40673
  let swappedIn = false;
40491
- const swapInOnce = () => {
40492
- if (swappedIn || !mountedRef.current) return;
40674
+ let warmupStarted = false;
40675
+ let warmupVideo = null;
40676
+ let warmupRvfc;
40677
+ let warmupSafety;
40678
+ const teardownWarmup = () => {
40679
+ if (warmupSafety !== void 0) {
40680
+ clearTimeout(warmupSafety);
40681
+ warmupSafety = void 0;
40682
+ }
40683
+ const v = warmupVideo;
40684
+ warmupVideo = null;
40685
+ if (!v) return;
40686
+ if (warmupRvfc !== void 0 && typeof v.cancelVideoFrameCallback === "function") v.cancelVideoFrameCallback(warmupRvfc);
40687
+ v.removeEventListener("loadeddata", commitSwap);
40688
+ try {
40689
+ v.srcObject = null;
40690
+ } catch {}
40691
+ v.remove();
40692
+ };
40693
+ function commitSwap() {
40694
+ if (swappedIn || !mountedRef.current) {
40695
+ teardownWarmup();
40696
+ return;
40697
+ }
40493
40698
  swappedIn = true;
40494
40699
  pcRef.current = pc;
40495
40700
  if (videoRef.current) {
@@ -40497,11 +40702,32 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40497
40702
  videoRef.current.play().catch(() => {});
40498
40703
  }
40499
40704
  if (oldPc && oldPc !== pc) oldPc.close();
40705
+ teardownWarmup();
40706
+ }
40707
+ const armSwap = () => {
40708
+ if (warmupStarted || swappedIn || !mountedRef.current) return;
40709
+ warmupStarted = true;
40710
+ if (typeof document === "undefined") {
40711
+ commitSwap();
40712
+ return;
40713
+ }
40714
+ const v = document.createElement("video");
40715
+ v.muted = true;
40716
+ v.playsInline = true;
40717
+ v.autoplay = true;
40718
+ v.style.cssText = "position:absolute;left:-9999px;top:0;width:1px;height:1px;opacity:0;pointer-events:none;";
40719
+ v.srcObject = newStream;
40720
+ warmupVideo = v;
40721
+ (containerRef.current ?? document.body).appendChild(v);
40722
+ v.play().catch(() => {});
40723
+ if (typeof v.requestVideoFrameCallback === "function") warmupRvfc = v.requestVideoFrameCallback(() => commitSwap());
40724
+ v.addEventListener("loadeddata", commitSwap, { once: true });
40725
+ warmupSafety = setTimeout(() => commitSwap(), 1800);
40500
40726
  };
40501
40727
  pc.ontrack = (event) => {
40502
40728
  console.debug("[WebRTC] ontrack (reoffer)", event.track.kind, event.track.readyState);
40503
40729
  newStream.addTrack(event.track);
40504
- swapInOnce();
40730
+ armSwap();
40505
40731
  };
40506
40732
  pc.oniceconnectionstatechange = () => {
40507
40733
  if (!mountedRef.current) return;
@@ -40509,10 +40735,11 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40509
40735
  console.debug("[WebRTC] ICE state (reoffer):", iceState);
40510
40736
  if (iceState === "connected" || iceState === "completed") {
40511
40737
  reconnectAttemptsRef.current = 0;
40512
- swapInOnce();
40738
+ armSwap();
40513
40739
  updateState("playing");
40514
40740
  if (videoRef.current?.paused) videoRef.current.play().catch(() => {});
40515
40741
  } else if (iceState === "failed" || iceState === "closed") {
40742
+ teardownWarmup();
40516
40743
  if (!swappedIn && iceState === "failed") pc.close();
40517
40744
  updateState("disconnected");
40518
40745
  scheduleReconnect();
@@ -44590,6 +44817,33 @@ function buildRawTree(data, fetchedAt) {
44590
44817
  * State). Computed once at module load.
44591
44818
  */
44592
44819
  var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(_camstack_types.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].toSorted());
44820
+ /**
44821
+ * The keys of a cap's `runtimeState` object schema, or `[]` when the cap
44822
+ * declares none / it is not an object schema. Read structurally (no Zod
44823
+ * import, no cast) so a self-contained addon's schema instance still works
44824
+ * across the bundle boundary — `ZodObject.shape` is a public getter.
44825
+ */
44826
+ function runtimeStateKeys(capName) {
44827
+ const rs = _camstack_types.ALL_CAPABILITY_DEFINITIONS.find((c) => c.name === capName)?.runtimeState;
44828
+ if (!rs || typeof rs !== "object") return [];
44829
+ const shape = Reflect.get(rs, "shape");
44830
+ if (!shape || typeof shape !== "object") return [];
44831
+ return Object.keys(shape);
44832
+ }
44833
+ /**
44834
+ * Cold placeholder slice for a bound cap that declares `runtimeState` but
44835
+ * has never written it (e.g. a virtual-doorbell camera that has never rung).
44836
+ * Every declared field is seeded `null` so the tree surfaces the cap's
44837
+ * presence and reads as "never" instead of the cap being OMITTED entirely.
44838
+ * Returns `null` when the cap has no runtime-state schema (nothing to seed).
44839
+ */
44840
+ function coldRuntimeStateSlice(capName) {
44841
+ const keys = runtimeStateKeys(capName);
44842
+ if (keys.length === 0) return null;
44843
+ const slice = {};
44844
+ for (const key of keys) slice[key] = null;
44845
+ return slice;
44846
+ }
44593
44847
  /** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
44594
44848
  * their direct slice keys (depth 1) render expanded; anything nested
44595
44849
  * deeper opens on demand. */
@@ -44636,6 +44890,27 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onCl
44636
44890
  (0, react$1.useEffect)(() => {
44637
44891
  seededRef.current = false;
44638
44892
  }, [deviceId]);
44893
+ const { data: bindings } = trpc.deviceManager.getBindings.useQuery({ deviceId }, { staleTime: 3e4 });
44894
+ (0, react$1.useEffect)(() => {
44895
+ const entries = bindings?.entries;
44896
+ if (!entries || entries.length === 0) return;
44897
+ const now = Date.now();
44898
+ setCapStates((prev) => {
44899
+ let mutated = false;
44900
+ const next = new Map(prev);
44901
+ for (const entry of entries) {
44902
+ if (next.has(entry.capName)) continue;
44903
+ const slice = coldRuntimeStateSlice(entry.capName);
44904
+ if (!slice) continue;
44905
+ next.set(entry.capName, {
44906
+ slice,
44907
+ updatedAt: now
44908
+ });
44909
+ mutated = true;
44910
+ }
44911
+ return mutated ? next : prev;
44912
+ });
44913
+ }, [bindings, deviceId]);
44639
44914
  const patchCapState = (0, react$1.useCallback)((capName, slice) => {
44640
44915
  setCapStates((prev) => {
44641
44916
  const next = new Map(prev);
@@ -46738,6 +47013,7 @@ exports.DeviceItem = DeviceItem;
46738
47013
  exports.DeviceList = DeviceList;
46739
47014
  exports.DeviceMultiSelectField = DeviceMultiSelectField;
46740
47015
  exports.DeviceSelectField = DeviceSelectField;
47016
+ exports.DeviceStepMatrix = DeviceStepMatrix;
46741
47017
  exports.Dialog = Dialog;
46742
47018
  exports.DialogContent = DialogContent;
46743
47019
  exports.DialogDescription = DialogDescription;
@@ -46971,6 +47247,7 @@ exports.selectedDeviceOptions = selectedDeviceOptions;
46971
47247
  exports.serializeRecordedCommand = serializeRecordedCommand;
46972
47248
  exports.shouldCommit = shouldCommit;
46973
47249
  exports.shouldEmit = shouldEmit;
47250
+ exports.shouldUseSingleNode = shouldUseSingleNode;
46974
47251
  exports.sortRows = sortRows;
46975
47252
  exports.statusIcons = statusIcons;
46976
47253
  exports.stripParentNamePrefix = stripParentNamePrefix;
@@ -47584,6 +47861,7 @@ exports.usePipelineOrchestratorSaveTemplate = usePipelineOrchestratorSaveTemplat
47584
47861
  exports.usePipelineOrchestratorSetAgentAddonDefaults = usePipelineOrchestratorSetAgentAddonDefaults;
47585
47862
  exports.usePipelineOrchestratorSetAgentCapabilities = usePipelineOrchestratorSetAgentCapabilities;
47586
47863
  exports.usePipelineOrchestratorSetAgentDetectWeight = usePipelineOrchestratorSetAgentDetectWeight;
47864
+ exports.usePipelineOrchestratorSetAgentInferenceDevices = usePipelineOrchestratorSetAgentInferenceDevices;
47587
47865
  exports.usePipelineOrchestratorSetAgentMaxCameras = usePipelineOrchestratorSetAgentMaxCameras;
47588
47866
  exports.usePipelineOrchestratorSetAgentReachableHost = usePipelineOrchestratorSetAgentReachableHost;
47589
47867
  exports.usePipelineOrchestratorSetCameraPipelineForAgent = usePipelineOrchestratorSetCameraPipelineForAgent;