@xenosystem/blocks 0.13.11 → 0.13.13

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/ops/index.js CHANGED
@@ -7,11 +7,8 @@ import {
7
7
  terminalThemeVar
8
8
  } from "../chunk-WE6A2DTY.js";
9
9
  import {
10
- CancelGlyph,
11
- ClearGlyph,
12
- CopyGlyph,
13
- RetryGlyph
14
- } from "../chunk-K7YLLV2W.js";
10
+ ICONS
11
+ } from "../chunk-6XVFYPVH.js";
15
12
  import "../chunk-2KG3PWR4.js";
16
13
 
17
14
  // src/ops/console/panel.ts
@@ -63,10 +60,10 @@ function spliceRedactedAppend(existing, addition, redact, carry = REDACTION_CARR
63
60
  const tail = existing.slice(cut);
64
61
  return head + redact(tail + addition);
65
62
  }
66
- function truncateLogMessage(text, max) {
67
- if (max <= 0 || text.length <= max) return text;
68
- if (max <= MESSAGE_ELISION.length) return text.slice(text.length - max);
69
- return MESSAGE_ELISION + text.slice(text.length - (max - MESSAGE_ELISION.length));
63
+ function truncateLogMessage(text3, max) {
64
+ if (max <= 0 || text3.length <= max) return text3;
65
+ if (max <= MESSAGE_ELISION.length) return text3.slice(text3.length - max);
66
+ return MESSAGE_ELISION + text3.slice(text3.length - (max - MESSAGE_ELISION.length));
70
67
  }
71
68
  function formatRecord(record) {
72
69
  const time = new Date(record.ts).toISOString();
@@ -243,7 +240,7 @@ var ConsoleController = class {
243
240
  */
244
241
  merge(existing, patch) {
245
242
  const secrets = [...this.host.getSecrets?.() ?? []];
246
- const redact = (text) => redactForLog(text, secrets);
243
+ const redact = (text3) => redactForLog(text3, secrets);
247
244
  let message = existing.message;
248
245
  if (typeof patch.message === "string") message = redact(patch.message);
249
246
  if (typeof patch.appendMessage === "string" && patch.appendMessage.length > 0) {
@@ -388,6 +385,29 @@ var ConsoleController = class {
388
385
  }
389
386
  /* ── Interaction ───────────────────────────────────────────────────────── */
390
387
  /** Follow the tail. Auto-disabled by the view when the user scrolls up. */
388
+ /* ── View state the declaration cannot hold ────────────────────────────── */
389
+ expandedId = null;
390
+ viewport = { height: 0, scrollTop: 0 };
391
+ /** Which record's data is expanded under its row, if any. */
392
+ getExpanded() {
393
+ return this.expandedId;
394
+ }
395
+ /** Toggle a record's expanded detail (only records carrying `data` expand). */
396
+ toggleExpanded(recordId) {
397
+ const record = this.records.find((r) => r.id === recordId);
398
+ if (!record || record.data === void 0) return;
399
+ this.expandedId = this.expandedId === recordId ? null : recordId;
400
+ this.notify();
401
+ }
402
+ /** The scroller reports its viewport; the view state windows the rows from it. */
403
+ setViewport(height, scrollTop) {
404
+ if (this.viewport.height === height && this.viewport.scrollTop === scrollTop) return;
405
+ this.viewport = { height, scrollTop };
406
+ this.notify();
407
+ }
408
+ getViewport() {
409
+ return this.viewport;
410
+ }
391
411
  setFollow(follow) {
392
412
  if (this.follow === follow) return;
393
413
  this.follow = follow;
@@ -607,203 +627,289 @@ var consoleManifest = {
607
627
  };
608
628
 
609
629
  // src/ops/console/react/ConsolePanelView.tsx
610
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
630
+ import { useEffect, useRef, useSyncExternalStore } from "react";
611
631
  import {
612
632
  Badge,
633
+ Code,
634
+ Column,
613
635
  EmptyState,
636
+ FilterChip,
614
637
  IconButton,
615
638
  Row,
616
639
  RowList,
617
640
  ScrollArea,
618
641
  SearchField,
619
642
  SegmentedControl,
643
+ Stack,
620
644
  StatusBar,
645
+ Text,
621
646
  Toolbar,
622
- ToolbarGroup
647
+ ToolbarGroup,
648
+ VirtualWindow
623
649
  } from "@xenosystem/components/chrome";
650
+
651
+ // src/ops/console/declaration.ts
624
652
  import { computeWindow } from "@xenosystem/tree-core";
625
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
626
- var TONE = {
627
- trace: "neutral",
628
- debug: "neutral",
629
- info: "info",
630
- warn: "warning",
631
- error: "error"
653
+ var TONE = { trace: "neutral", debug: "neutral", info: "info", warn: "warning", error: "error" };
654
+ function consoleViewState(c, options = {}) {
655
+ const s = c.getState();
656
+ const rowHeight = options.rowHeight ?? 20;
657
+ const showTimestamps = options.showTimestamps ?? true;
658
+ const viewport = c.getViewport();
659
+ const window_ = computeWindow({ scrollTop: viewport.scrollTop, viewportHeight: viewport.height || rowHeight * 20, rowHeight, rowCount: s.rows.length });
660
+ const expanded = c.getExpanded();
661
+ return {
662
+ search: s.filter.search ?? "",
663
+ levels: LEVEL_ORDER.map((l) => ({ value: l, label: l.slice(0, 1).toUpperCase() })),
664
+ level: s.filter.minLevel ?? "trace",
665
+ errors: s.counts.error > 0 ? String(s.counts.error) : void 0,
666
+ warns: s.counts.warn > 0 ? String(s.counts.warn) : void 0,
667
+ multiSource: s.sources.length > 1,
668
+ sources: s.sources.map((src) => ({ id: src.id, label: src.label ?? src.id, on: src.enabled, title: `${src.count} records` })),
669
+ empty: s.rows.length === 0,
670
+ emptyTitle: s.total > 0 ? "No matches" : "No output",
671
+ emptyHint: s.total > 0 ? "Nothing matches the current filter." : options.emptyHint ?? "Run something to see its output here.",
672
+ listed: s.rows.length > 0,
673
+ rowHeight,
674
+ totalHeight: window_.totalHeight,
675
+ offsetTop: window_.offsetTop,
676
+ rows: s.rows.slice(window_.startIndex, window_.endIndex).map((record) => ({
677
+ id: record.id,
678
+ time: showTimestamps ? new Date(record.ts).toISOString().slice(11, 23) : void 0,
679
+ level: record.level,
680
+ tone: TONE[record.level],
681
+ source: record.source,
682
+ message: record.message,
683
+ repeat: (record.count ?? 1) > 1 ? `x${record.count}` : void 0,
684
+ expanded: expanded === record.id && record.data !== void 0,
685
+ // Already redacted — the buffer never held the raw value.
686
+ detail: expanded === record.id && record.data !== void 0 ? safeJson(record.data) : void 0
687
+ })),
688
+ countLabel: `${s.rows.length} of ${s.total}` + (s.dropped > 0 ? ` \xB7 ${s.dropped} dropped` : "") + (s.droppedPatches > 0 ? ` \xB7 ${s.droppedPatches} unmatched` : ""),
689
+ followLabel: s.follow ? "following" : void 0
690
+ };
691
+ }
692
+ function applyConsoleUi(c, intent) {
693
+ switch (intent.kind) {
694
+ case "search":
695
+ c.setSearch(intent.value);
696
+ break;
697
+ case "level":
698
+ c.setLevel(intent.level === "trace" ? void 0 : intent.level);
699
+ break;
700
+ case "copyVisible":
701
+ void globalThis.navigator?.clipboard?.writeText(c.copyVisible());
702
+ break;
703
+ case "clear":
704
+ c.clear();
705
+ break;
706
+ case "source":
707
+ c.toggleSource(intent.id, intent.on);
708
+ break;
709
+ case "row":
710
+ c.toggleExpanded(intent.id);
711
+ c.activate(intent.id);
712
+ break;
713
+ case "contextMenu":
714
+ c.requestContextMenu(intent.id, intent.x, intent.y);
715
+ break;
716
+ }
717
+ }
718
+ var consoleViewManifest = {
719
+ id: consoleManifest.id,
720
+ version: "1.0.0",
721
+ title: "Console",
722
+ category: "ops",
723
+ layout: { slot: "bottom" },
724
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The ConsoleViewModel the runtime derives from the controller on every draw." }],
725
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, \u2026 } \u2014 applied by the runtime to the controller." }],
726
+ commands: []
632
727
  };
633
- function ConsolePanelView({
634
- controller,
635
- rowHeight = 20,
636
- showTimestamps = true,
637
- emptyHint
638
- }) {
728
+ var EMPTY = { search: "", levels: [], level: "trace", multiSource: false, sources: [], empty: true, emptyTitle: "No output", emptyHint: "Run something to see its output here.", listed: false, rowHeight: 20, totalHeight: 0, offsetTop: 0, rows: [], countLabel: "0 of 0" };
729
+ var BEHAVIOURS = {
730
+ "ops/consolePanelView": (inputs) => inputs.view ?? EMPTY
731
+ };
732
+ var CONSOLE_RUNTIME_UNITS = ["console/Scroller"];
733
+ var text = (b) => ({ component: "text", props: { text: b } });
734
+ var ui = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
735
+ var CONSOLE_DECLARATION = {
736
+ format: "xeno.block@1",
737
+ manifest: consoleViewManifest,
738
+ behaviour: "ops/consolePanelView",
739
+ composition: {
740
+ component: "chrome/Column",
741
+ children: [
742
+ {
743
+ component: "chrome/Toolbar",
744
+ props: {
745
+ left: {
746
+ node: {
747
+ component: "fragment",
748
+ children: [
749
+ { component: "chrome/SearchField", props: { value: { input: "view.search" }, placeholder: { value: "Filter\u2026" } }, on: { onChange: ui("search", { value: { arg: "" } }) } },
750
+ { component: "chrome/SegmentedControl", props: { size: { value: "sm" }, label: { value: "Minimum level" }, options: { input: "view.levels" }, value: { input: "view.level" } }, on: { onChange: ui("level", { level: { arg: "" } }) } }
751
+ ]
752
+ }
753
+ },
754
+ right: {
755
+ node: {
756
+ component: "chrome/ToolbarGroup",
757
+ props: { end: { value: true } },
758
+ children: [
759
+ { component: "chrome/Badge", when: { input: "view.errors" }, props: { tone: { value: "error" } }, children: [text({ input: "view.errors" })] },
760
+ { component: "chrome/Badge", when: { input: "view.warns" }, props: { tone: { value: "warning" } }, children: [text({ input: "view.warns" })] },
761
+ { component: "chrome/IconButton", props: { icon: { node: { component: "icons/Copy" } }, label: { value: "Copy visible" } }, on: { onClick: ui("copyVisible") } },
762
+ { component: "chrome/IconButton", props: { icon: { node: { component: "icons/Clear" } }, label: { value: "Clear" } }, on: { onClick: ui("clear") } }
763
+ ]
764
+ }
765
+ }
766
+ }
767
+ },
768
+ {
769
+ component: "chrome/Stack",
770
+ when: { input: "view.multiSource" },
771
+ props: { direction: { value: "row" }, gap: { value: 4 }, pad: { value: "inset" } },
772
+ children: [
773
+ { component: "chrome/FilterChip", each: { input: "view.sources", key: "id" }, props: { on: { item: "on" }, title: { item: "title" } }, on: { onChange: ui("source", { id: { item: "id" }, on: { arg: "" } }) }, children: [text({ item: "label" })] }
774
+ ]
775
+ },
776
+ { component: "chrome/EmptyState", when: { input: "view.empty" }, props: { title: { input: "view.emptyTitle" }, hint: { input: "view.emptyHint" } } },
777
+ {
778
+ component: "console/Scroller",
779
+ when: { input: "view.listed" },
780
+ children: [
781
+ {
782
+ component: "chrome/VirtualWindow",
783
+ props: { totalHeight: { input: "view.totalHeight" }, offsetTop: { input: "view.offsetTop" } },
784
+ children: [
785
+ {
786
+ component: "chrome/RowList",
787
+ props: { role: { value: "log" } },
788
+ children: [
789
+ {
790
+ component: "fragment",
791
+ each: { input: "view.rows", key: "id" },
792
+ children: [
793
+ {
794
+ component: "chrome/Row",
795
+ props: {
796
+ height: { input: "view.rowHeight" },
797
+ noIcon: { value: true },
798
+ label: {
799
+ node: {
800
+ component: "chrome/Stack",
801
+ props: { direction: { value: "row" }, pad: { value: "none" } },
802
+ children: [
803
+ { component: "chrome/Text", when: { item: "time" }, props: { role: { value: "number" } }, children: [text({ item: "time" })] },
804
+ { component: "chrome/Badge", props: { tone: { item: "tone" } }, children: [text({ item: "level" })] },
805
+ { component: "chrome/Text", when: { item: "source" }, props: { role: { value: "meta" } }, children: [text({ item: "source" })] },
806
+ { component: "chrome/Text", children: [text({ item: "message" })] }
807
+ ]
808
+ }
809
+ },
810
+ trailing: { node: { component: "chrome/Badge", when: { item: "repeat" }, props: { title: { value: "Repeated consecutively" } }, children: [text({ item: "repeat" })] } }
811
+ },
812
+ on: { onClick: ui("row", { id: { item: "id" } }), onContextMenuAt: ui("contextMenu", { id: { item: "id" }, x: { arg: "", index: 0 }, y: { arg: "", index: 1 } }) }
813
+ },
814
+ { component: "chrome/Code", when: { item: "expanded" }, children: [text({ item: "detail" })] }
815
+ ]
816
+ }
817
+ ]
818
+ }
819
+ ]
820
+ }
821
+ ]
822
+ },
823
+ { component: "chrome/StatusBar", props: { left: { input: "view.countLabel" }, right: { input: "view.followLabel" } } }
824
+ ]
825
+ }
826
+ };
827
+
828
+ // src/ops/console/react/ConsolePanelView.tsx
829
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
830
+ var CopyGlyph = ICONS["icons/Copy"];
831
+ var ClearGlyph = ICONS["icons/Clear"];
832
+ function ConsoleScroller({ controller, rowHeight = 20, children }) {
639
833
  const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
640
- const scrollRef = useRef(null);
641
- const [viewport, setViewport] = useState({ height: 0, scrollTop: 0 });
642
- const [expanded, setExpanded] = useState(null);
834
+ const ref = useRef(null);
643
835
  useEffect(() => {
644
- const element = scrollRef.current;
645
- if (!element || typeof ResizeObserver === "undefined") return;
646
- const observer = new ResizeObserver(() => setViewport((v) => ({ ...v, height: element.clientHeight })));
647
- observer.observe(element);
648
- setViewport((v) => ({ ...v, height: element.clientHeight }));
649
- return () => observer.disconnect();
650
- }, []);
836
+ const el = ref.current;
837
+ if (!el) return;
838
+ controller.setViewport(el.clientHeight, el.scrollTop);
839
+ const RO = globalThis.ResizeObserver;
840
+ if (!RO) return;
841
+ const ro = new RO(() => controller.setViewport(el.clientHeight, el.scrollTop));
842
+ ro.observe(el);
843
+ return () => ro.disconnect();
844
+ }, [controller]);
651
845
  useEffect(() => {
652
- const element = scrollRef.current;
653
- if (!element || !state.follow) return;
654
- element.scrollTop = element.scrollHeight;
846
+ const el = ref.current;
847
+ if (!el || !state.follow) return;
848
+ el.scrollTop = el.scrollHeight;
655
849
  }, [state.rows.length, state.follow]);
656
- const onScroll = useCallback(
657
- (event) => {
658
- const el = event.currentTarget;
659
- setViewport((v) => ({ ...v, scrollTop: el.scrollTop }));
660
- const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < rowHeight;
661
- if (state.follow && !atBottom) controller.setFollow(false);
662
- else if (!state.follow && atBottom) controller.setFollow(true);
663
- },
664
- [controller, rowHeight, state.follow]
850
+ return /* @__PURE__ */ jsx(
851
+ ScrollArea,
852
+ {
853
+ ref,
854
+ onScroll: (e) => {
855
+ const el = e.currentTarget;
856
+ controller.setViewport(el.clientHeight, el.scrollTop);
857
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < rowHeight;
858
+ if (state.follow && !atBottom) controller.setFollow(false);
859
+ else if (!state.follow && atBottom) controller.setFollow(true);
860
+ },
861
+ children
862
+ }
665
863
  );
666
- const window = computeWindow({
667
- scrollTop: viewport.scrollTop,
668
- viewportHeight: viewport.height || rowHeight * 20,
669
- rowHeight,
670
- rowCount: state.rows.length
671
- });
672
- const slice = state.rows.slice(window.startIndex, window.endIndex);
673
- return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }, children: [
864
+ }
865
+ function consoleRuntimeComponents(controller, rowHeight) {
866
+ return {
867
+ "console/Scroller": ({ children }) => /* @__PURE__ */ jsx(ConsoleScroller, { controller, rowHeight, children })
868
+ };
869
+ }
870
+ function ConsolePanelView({ controller, ...options }) {
871
+ useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
872
+ const view = consoleViewState(controller, options);
873
+ const ui3 = (intent) => applyConsoleUi(controller, intent);
874
+ return /* @__PURE__ */ jsxs(Column, { children: [
674
875
  /* @__PURE__ */ jsx(
675
876
  Toolbar,
676
877
  {
677
878
  left: /* @__PURE__ */ jsxs(Fragment, { children: [
678
- /* @__PURE__ */ jsx(
679
- SearchField,
680
- {
681
- value: state.filter.search ?? "",
682
- onChange: (term) => controller.setSearch(term),
683
- placeholder: "Filter\u2026"
684
- }
685
- ),
686
- /* @__PURE__ */ jsx(
687
- SegmentedControl,
688
- {
689
- size: "sm",
690
- label: "Minimum level",
691
- options: LEVEL_ORDER.map((l) => ({ value: l, label: l.slice(0, 1).toUpperCase() })),
692
- value: state.filter.minLevel ?? "trace",
693
- onChange: (level) => controller.setLevel(level === "trace" ? void 0 : level)
694
- }
695
- )
879
+ /* @__PURE__ */ jsx(SearchField, { value: view.search, placeholder: "Filter\u2026", onChange: (value) => ui3({ kind: "search", value }) }),
880
+ /* @__PURE__ */ jsx(SegmentedControl, { size: "sm", label: "Minimum level", options: view.levels, value: view.level, onChange: (level) => ui3({ kind: "level", level }) })
696
881
  ] }),
697
882
  right: /* @__PURE__ */ jsxs(ToolbarGroup, { end: true, children: [
698
- state.counts.error > 0 ? /* @__PURE__ */ jsx(Badge, { tone: "error", children: String(state.counts.error) }) : null,
699
- state.counts.warn > 0 ? /* @__PURE__ */ jsx(Badge, { tone: "warning", children: String(state.counts.warn) }) : null,
700
- /* @__PURE__ */ jsx(
701
- IconButton,
702
- {
703
- icon: /* @__PURE__ */ jsx(CopyGlyph, {}),
704
- label: "Copy visible",
705
- onClick: () => {
706
- void navigator?.clipboard?.writeText(controller.copyVisible());
707
- }
708
- }
709
- ),
710
- /* @__PURE__ */ jsx(IconButton, { icon: /* @__PURE__ */ jsx(ClearGlyph, {}), label: "Clear", onClick: () => controller.clear() })
883
+ view.errors ? /* @__PURE__ */ jsx(Badge, { tone: "error", children: view.errors }) : null,
884
+ view.warns ? /* @__PURE__ */ jsx(Badge, { tone: "warning", children: view.warns }) : null,
885
+ /* @__PURE__ */ jsx(IconButton, { icon: /* @__PURE__ */ jsx(CopyGlyph, {}), label: "Copy visible", onClick: () => ui3({ kind: "copyVisible" }) }),
886
+ /* @__PURE__ */ jsx(IconButton, { icon: /* @__PURE__ */ jsx(ClearGlyph, {}), label: "Clear", onClick: () => ui3({ kind: "clear" }) })
711
887
  ] })
712
888
  }
713
889
  ),
714
- state.sources.length > 1 ? /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4, padding: "2px 6px", flexWrap: "wrap" }, children: state.sources.map((source) => /* @__PURE__ */ jsx(
715
- "span",
716
- {
717
- onClick: () => controller.toggleSource(source.id),
718
- style: { cursor: "pointer", opacity: source.enabled ? 1 : 0.4 },
719
- children: /* @__PURE__ */ jsx(Badge, { title: `${source.count} records`, children: source.label ?? source.id })
720
- },
721
- source.id
722
- )) }) : null,
723
- state.rows.length === 0 ? /* @__PURE__ */ jsx(
724
- EmptyState,
725
- {
726
- title: state.total > 0 ? "No matches" : "No output",
727
- hint: state.total > 0 ? "Nothing matches the current filter." : emptyHint ?? "Run something to see its output here."
728
- }
729
- ) : /* @__PURE__ */ jsx(ScrollArea, { children: /* @__PURE__ */ jsx("div", { ref: scrollRef, onScroll, style: { height: "100%", overflow: "auto" }, children: /* @__PURE__ */ jsx("div", { style: { height: window.totalHeight, position: "relative" }, children: /* @__PURE__ */ jsx("div", { style: { transform: `translateY(${window.offsetTop}px)` }, children: /* @__PURE__ */ jsx(RowList, { role: "log", children: slice.map((record) => /* @__PURE__ */ jsx(
730
- LogRow,
731
- {
732
- record,
733
- controller,
734
- rowHeight,
735
- showTimestamps,
736
- expanded: expanded === record.id,
737
- onToggle: () => setExpanded(expanded === record.id ? null : record.id)
738
- },
739
- record.id
740
- )) }) }) }) }) }),
741
- /* @__PURE__ */ jsx(
742
- StatusBar,
743
- {
744
- left: `${state.rows.length} of ${state.total}` + (state.dropped > 0 ? ` \xB7 ${state.dropped} dropped` : "") + // A patch that addressed nothing is the one streaming failure with no visible symptom:
745
- // the producer sees no line grow, and a mis-keyed wire looks exactly like a dead one.
746
- (state.droppedPatches > 0 ? ` \xB7 ${state.droppedPatches} unmatched` : ""),
747
- right: state.follow ? "following" : void 0
748
- }
749
- )
750
- ] });
751
- }
752
- function LogRow({
753
- record,
754
- controller,
755
- rowHeight,
756
- showTimestamps,
757
- expanded,
758
- onToggle
759
- }) {
760
- return /* @__PURE__ */ jsxs(Fragment, { children: [
761
- /* @__PURE__ */ jsx(
762
- Row,
763
- {
764
- style: { height: rowHeight, alignItems: "flex-start" },
765
- noIcon: true,
766
- onClick: () => {
767
- if (record.data !== void 0) onToggle();
768
- controller.activate(record.id);
769
- },
770
- onContextMenu: (e) => {
771
- e.preventDefault();
772
- controller.requestContextMenu(record.id, e.clientX, e.clientY);
773
- },
774
- label: /* @__PURE__ */ jsxs("span", { style: { display: "flex", gap: 6, alignItems: "center", minWidth: 0 }, children: [
775
- showTimestamps ? /* @__PURE__ */ jsx("span", { style: { opacity: 0.45, fontVariantNumeric: "tabular-nums", flex: "0 0 auto" }, children: new Date(record.ts).toISOString().slice(11, 23) }) : null,
776
- /* @__PURE__ */ jsx(Badge, { tone: TONE[record.level], children: record.level }),
777
- record.source ? /* @__PURE__ */ jsx("span", { style: { opacity: 0.55 }, children: record.source }) : null,
778
- /* @__PURE__ */ jsx(
779
- "span",
780
- {
781
- style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 },
782
- children: record.message
783
- }
784
- )
785
- ] }),
786
- trailing: (record.count ?? 1) > 1 ? (
787
- // The best feature in any incumbent: a run of identical lines is one row with a count.
788
- /* @__PURE__ */ jsx(Badge, { title: "Repeated consecutively", children: `x${record.count}` })
789
- ) : null
790
- }
791
- ),
792
- expanded && record.data !== void 0 ? /* @__PURE__ */ jsx(
793
- "pre",
794
- {
795
- style: {
796
- margin: 0,
797
- padding: "2px 10px 6px 24px",
798
- fontSize: 10,
799
- lineHeight: 1.4,
800
- whiteSpace: "pre-wrap",
801
- wordBreak: "break-all",
802
- opacity: 0.8
890
+ view.multiSource ? /* @__PURE__ */ jsx(Stack, { direction: "row", gap: 4, pad: "inset", children: view.sources.map((source) => /* @__PURE__ */ jsx(FilterChip, { on: source.on, title: source.title, onChange: (on) => ui3({ kind: "source", id: source.id, on }), children: source.label }, source.id)) }) : null,
891
+ view.empty ? /* @__PURE__ */ jsx(EmptyState, { title: view.emptyTitle, hint: view.emptyHint }) : null,
892
+ view.listed ? /* @__PURE__ */ jsx(ConsoleScroller, { controller, rowHeight: view.rowHeight, children: /* @__PURE__ */ jsx(VirtualWindow, { totalHeight: view.totalHeight, offsetTop: view.offsetTop, children: /* @__PURE__ */ jsx(RowList, { role: "log", children: view.rows.map((row) => /* @__PURE__ */ jsxs(Fragment, { children: [
893
+ /* @__PURE__ */ jsx(
894
+ Row,
895
+ {
896
+ height: view.rowHeight,
897
+ noIcon: true,
898
+ label: /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "none", children: [
899
+ row.time ? /* @__PURE__ */ jsx(Text, { role: "number", children: row.time }) : null,
900
+ /* @__PURE__ */ jsx(Badge, { tone: row.tone, children: row.level }),
901
+ row.source ? /* @__PURE__ */ jsx(Text, { role: "meta", children: row.source }) : null,
902
+ /* @__PURE__ */ jsx(Text, { children: row.message })
903
+ ] }),
904
+ trailing: row.repeat ? /* @__PURE__ */ jsx(Badge, { title: "Repeated consecutively", children: row.repeat }) : null,
905
+ onClick: () => ui3({ kind: "row", id: row.id }),
906
+ onContextMenuAt: (x, y) => ui3({ kind: "contextMenu", id: row.id, x, y })
803
907
  },
804
- children: safeJson(record.data)
805
- }
806
- ) : null
908
+ row.id
909
+ ),
910
+ row.expanded ? /* @__PURE__ */ jsx(Code, { children: row.detail }, `${row.id}:detail`) : null
911
+ ] })) }) }) }) : null,
912
+ /* @__PURE__ */ jsx(StatusBar, { left: view.countLabel, right: view.followLabel })
807
913
  ] });
808
914
  }
809
915
 
@@ -1708,19 +1814,25 @@ var runsManifest = {
1708
1814
  import { useSyncExternalStore as useSyncExternalStore2 } from "react";
1709
1815
  import {
1710
1816
  Badge as Badge2,
1817
+ Column as Column2,
1711
1818
  EmptyState as EmptyState2,
1819
+ FilterChip as FilterChip2,
1712
1820
  IconButton as IconButton2,
1713
- ProportionBar,
1821
+ Note,
1822
+ Progress,
1714
1823
  Row as Row2,
1715
1824
  RowList as RowList2,
1716
1825
  ScrollArea as ScrollArea2,
1717
1826
  SearchField as SearchField2,
1827
+ Stack as Stack2,
1718
1828
  StatusBar as StatusBar2,
1829
+ Text as Text2,
1719
1830
  TextButton,
1720
1831
  Toolbar as Toolbar2,
1721
1832
  ToolbarGroup as ToolbarGroup2
1722
1833
  } from "@xenosystem/components/chrome";
1723
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1834
+
1835
+ // src/ops/runs/declaration.ts
1724
1836
  var TONE2 = {
1725
1837
  requested: "neutral",
1726
1838
  queued: "neutral",
@@ -1733,205 +1845,296 @@ var TONE2 = {
1733
1845
  failed: "error",
1734
1846
  cancelled: "neutral"
1735
1847
  };
1736
- function toneOf(status) {
1737
- return TONE2[status] ?? "neutral";
1738
- }
1739
- function RunsPanelView({
1740
- controller,
1741
- showStats = true,
1742
- showProgress = true,
1743
- emptyHint,
1744
- now = () => Date.now()
1745
- }) {
1746
- const state = useSyncExternalStore2(controller.subscribe, controller.getState, controller.getState);
1747
- const active = new Set(state.filter.statuses ?? []);
1748
- return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }, children: [
1749
- /* @__PURE__ */ jsx2(
1750
- Toolbar2,
1751
- {
1752
- left: /* @__PURE__ */ jsx2(
1753
- SearchField2,
1754
- {
1755
- value: state.filter.search ?? "",
1756
- onChange: (term) => controller.setSearch(term),
1757
- placeholder: "Filter runs\u2026"
1758
- }
1759
- ),
1760
- right: /* @__PURE__ */ jsx2(ToolbarGroup2, { end: true, children: /* @__PURE__ */ jsx2(TextButton, { onClick: () => controller.clearTerminal(), children: "Clear finished" }) })
1761
- }
1762
- ),
1763
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 4, padding: "2px 6px", flexWrap: "wrap" }, children: [
1764
- ALL_STATUSES.filter((s) => state.counts[s] > 0).map((status) => /* @__PURE__ */ jsx2(
1765
- "span",
1766
- {
1767
- onClick: () => controller.setStatuses(
1768
- active.has(status) ? [...active].filter((s) => s !== status) : [...active, status]
1769
- ),
1770
- style: { cursor: "pointer", opacity: active.size === 0 || active.has(status) ? 1 : 0.4 },
1771
- children: /* @__PURE__ */ jsx2(Badge2, { tone: TONE2[status], children: `${status} ${state.counts[status]}` })
1772
- },
1773
- status
1774
- )),
1775
- state.sources.length > 1 ? state.sources.map((source) => /* @__PURE__ */ jsx2(
1776
- "span",
1777
- {
1778
- onClick: () => controller.toggleSource(source.id),
1779
- style: { cursor: "pointer", opacity: source.enabled ? 1 : 0.4 },
1780
- children: /* @__PURE__ */ jsx2(Badge2, { title: `${source.count} runs`, children: source.label })
1781
- },
1782
- source.id
1783
- )) : null
1784
- ] }),
1785
- state.rows.length === 0 ? /* @__PURE__ */ jsx2(
1786
- EmptyState2,
1787
- {
1788
- title: state.total > 0 ? "No matching runs" : "No runs yet",
1789
- hint: state.total > 0 ? "Nothing matches the current filter." : emptyHint ?? "Run something to see it here."
1790
- }
1791
- ) : /* @__PURE__ */ jsx2(ScrollArea2, { children: /* @__PURE__ */ jsx2(RowList2, { role: "list", children: state.rows.map(
1792
- (row) => row.kind === "run" ? /* @__PURE__ */ jsx2(RunRow, { row, controller, showProgress, now }, row.key) : /* @__PURE__ */ jsx2(StepRow, { row, controller, showProgress }, row.key)
1793
- ) }) }),
1794
- /* @__PURE__ */ jsx2(
1795
- StatusBar2,
1796
- {
1797
- left: `${state.runs.length} of ${state.total}${state.dropped > 0 ? ` \xB7 ${state.dropped} dropped` : ""}`,
1798
- right: showStats && state.stats.terminal > 0 ? `${Math.round((state.stats.successRate ?? 0) * 100)}% ok${state.stats.meanDurationMs !== null ? ` \xB7 ~${formatDuration(state.stats.meanDurationMs)}` : ""}` : (
1799
- // `null` success rate is NOT 0% — nothing has finished, which is a different fact.
1800
- state.stats.active > 0 ? `${state.stats.active} active` : void 0
1801
- )
1848
+ var toneOf = (status) => TONE2[status] ?? "neutral";
1849
+ var truncationHint = (reason) => reason === "cycle" ? "This step is nested inside itself \u2014 the panel stopped here rather than repeat it." : "Nesting is deeper than this panel will render.";
1850
+ function runsViewState(c, options = {}) {
1851
+ const s = c.getState();
1852
+ const showStats = options.showStats ?? true;
1853
+ const showProgress = options.showProgress ?? true;
1854
+ const now = options.now ?? (() => Date.now());
1855
+ const active = new Set(s.filter.statuses ?? []);
1856
+ const statuses = ALL_STATUSES.filter((st) => s.counts[st] > 0).map((st) => ({ id: st, label: `${st} ${s.counts[st]}`, on: active.size === 0 || active.has(st), tone: toneOf(st) }));
1857
+ const sources = s.sources.length > 1 ? s.sources.map((src) => ({ id: src.id, label: src.label, on: src.enabled, tone: "neutral", title: `${src.count} runs` })) : [];
1858
+ const stats = showStats && s.stats.terminal > 0 ? `${Math.round((s.stats.successRate ?? 0) * 100)}% ok${s.stats.meanDurationMs !== null ? ` \xB7 ~${formatDuration(s.stats.meanDurationMs)}` : ""}` : s.stats.active > 0 ? `${s.stats.active} active` : void 0;
1859
+ return {
1860
+ search: s.filter.search ?? "",
1861
+ statuses,
1862
+ multiSource: s.sources.length > 1,
1863
+ sources,
1864
+ hasChips: statuses.length > 0 || sources.length > 0,
1865
+ empty: s.rows.length === 0,
1866
+ emptyTitle: s.total > 0 ? "No matching runs" : "No runs yet",
1867
+ emptyHint: s.total > 0 ? "Nothing matches the current filter." : options.emptyHint ?? "Run something to see it here.",
1868
+ listed: s.rows.length > 0,
1869
+ rows: s.rows.map((row) => {
1870
+ const run = row.run;
1871
+ if (row.kind === "run") {
1872
+ const duration = durationOf(run, now());
1873
+ const hasProgress2 = showProgress && run.progress !== void 0;
1874
+ return {
1875
+ key: row.key,
1876
+ runId: run.id,
1877
+ stepPath: [],
1878
+ depth: 0,
1879
+ step: false,
1880
+ status: run.status,
1881
+ tone: toneOf(run.status),
1882
+ label: run.label,
1883
+ trigger: run.trigger,
1884
+ retries: (run.retryCount ?? 0) > 0 ? `retry x${run.retryCount}` : void 0,
1885
+ truncated: row.truncated ? row.truncated === "cycle" ? "cycle" : "deeper\u2026" : void 0,
1886
+ truncatedHint: row.truncated ? truncationHint(row.truncated) : void 0,
1887
+ hasProgress: hasProgress2,
1888
+ progress: run.progress ?? 0,
1889
+ progressLabel: `${Math.round((run.progress ?? 0) * 100)}%`,
1890
+ duration: !hasProgress2 && duration !== null ? formatDuration(duration) : void 0,
1891
+ canCancel: canCancel(run),
1892
+ cancelLabel: `Cancel ${run.label}`,
1893
+ canRetry: canRetry(run),
1894
+ retryLabel: `Retry ${run.label}`,
1895
+ hasError: !!run.error,
1896
+ errorCode: run.error?.code,
1897
+ errorMessage: run.error?.message
1898
+ };
1802
1899
  }
1803
- )
1804
- ] });
1900
+ const step = row.step;
1901
+ const elapsed = step.startedAt !== void 0 && step.endedAt !== void 0 ? step.endedAt - step.startedAt : null;
1902
+ const hasProgress = showProgress && step.progress !== void 0;
1903
+ return {
1904
+ key: row.key,
1905
+ runId: row.runId,
1906
+ stepPath: row.stepPath,
1907
+ depth: row.depth,
1908
+ step: true,
1909
+ status: step.status,
1910
+ tone: toneOf(step.status),
1911
+ label: step.label,
1912
+ truncated: row.truncated ? row.truncated === "cycle" ? "cycle" : "deeper\u2026" : void 0,
1913
+ truncatedHint: row.truncated ? truncationHint(row.truncated) : void 0,
1914
+ hasProgress,
1915
+ progress: step.progress ?? 0,
1916
+ progressLabel: `${Math.round((step.progress ?? 0) * 100)}%`,
1917
+ duration: !hasProgress && elapsed !== null ? formatDuration(elapsed) : void 0,
1918
+ canCancel: false,
1919
+ cancelLabel: "",
1920
+ canRetry: false,
1921
+ retryLabel: "",
1922
+ hasError: false
1923
+ };
1924
+ }),
1925
+ countLabel: `${s.runs.length} of ${s.total}${s.dropped > 0 ? ` \xB7 ${s.dropped} dropped` : ""}`,
1926
+ statsLabel: stats
1927
+ };
1805
1928
  }
1806
- function TruncationBadge({ reason }) {
1807
- return /* @__PURE__ */ jsx2(
1808
- Badge2,
1809
- {
1810
- tone: "warning",
1811
- title: reason === "cycle" ? "This step is nested inside itself \u2014 the panel stopped here rather than repeat it." : "Nesting is deeper than this panel will render.",
1812
- children: reason === "cycle" ? "cycle" : "deeper\u2026"
1929
+ function applyRunsUi(c, intent) {
1930
+ switch (intent.kind) {
1931
+ case "search":
1932
+ c.setSearch(intent.value);
1933
+ break;
1934
+ case "clearTerminal":
1935
+ c.clearTerminal();
1936
+ break;
1937
+ case "status": {
1938
+ const active = new Set(c.getState().filter.statuses ?? []);
1939
+ const st = intent.status;
1940
+ if (active.has(st)) active.delete(st);
1941
+ else active.add(st);
1942
+ c.setStatuses([...active]);
1943
+ break;
1813
1944
  }
1814
- );
1815
- }
1816
- function RowLabel({
1817
- status,
1818
- text,
1819
- trailing
1820
- }) {
1821
- return /* @__PURE__ */ jsxs2("span", { style: { display: "flex", gap: 6, alignItems: "center", minWidth: 0 }, children: [
1822
- /* @__PURE__ */ jsx2(Badge2, { tone: toneOf(status), children: status }),
1823
- /* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: text }),
1824
- trailing
1825
- ] });
1945
+ case "source":
1946
+ c.toggleSource(intent.id, intent.on);
1947
+ break;
1948
+ case "row": {
1949
+ const row = c.getState().rows.find((r) => r.runId === intent.runId && r.stepPath.join("/") === intent.stepPath.join("/"));
1950
+ if (row?.hasChildren) c.toggleExpanded(intent.runId, intent.stepPath.length ? intent.stepPath : void 0);
1951
+ c.activate(intent.runId, intent.stepPath.length ? intent.stepPath : void 0);
1952
+ break;
1953
+ }
1954
+ case "contextMenu":
1955
+ c.requestContextMenu(intent.runId, intent.x, intent.y);
1956
+ break;
1957
+ case "cancel":
1958
+ c.cancel(intent.runId);
1959
+ break;
1960
+ case "retry":
1961
+ c.retry(intent.runId);
1962
+ break;
1963
+ }
1826
1964
  }
1827
- function RunRow({
1828
- row,
1829
- controller,
1830
- showProgress,
1831
- now
1832
- }) {
1833
- const run = row.run;
1834
- const duration = durationOf(run, now());
1835
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1836
- /* @__PURE__ */ jsx2(
1837
- Row2,
1965
+ var runsViewManifest = {
1966
+ id: runsManifest.id,
1967
+ version: "1.0.0",
1968
+ title: "Runs",
1969
+ category: "ops",
1970
+ layout: { slot: "side" },
1971
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The RunsViewModel the runtime derives from the controller on every draw." }],
1972
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, \u2026 } \u2014 applied by the runtime to the controller." }],
1973
+ commands: []
1974
+ };
1975
+ var EMPTY2 = { search: "", statuses: [], multiSource: false, sources: [], hasChips: false, empty: true, emptyTitle: "No runs yet", emptyHint: "Run something to see it here.", listed: false, rows: [], countLabel: "0 of 0" };
1976
+ var BEHAVIOURS2 = {
1977
+ "ops/runsPanelView": (inputs) => inputs.view ?? EMPTY2
1978
+ };
1979
+ var RUNS_RUNTIME_UNITS = [];
1980
+ var text2 = (b) => ({ component: "text", props: { text: b } });
1981
+ var ui2 = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
1982
+ var chip = (intent) => ({
1983
+ component: "chrome/FilterChip",
1984
+ props: { on: { item: "on" }, title: { item: "title" } },
1985
+ on: { onChange: intent },
1986
+ children: [{ component: "chrome/Badge", props: { tone: { item: "tone" } }, children: [text2({ item: "label" })] }]
1987
+ });
1988
+ var RUNS_DECLARATION = {
1989
+ format: "xeno.block@1",
1990
+ manifest: runsViewManifest,
1991
+ behaviour: "ops/runsPanelView",
1992
+ composition: {
1993
+ component: "chrome/Column",
1994
+ children: [
1838
1995
  {
1839
- noIcon: true,
1840
- onClick: () => {
1841
- if (row.hasChildren) controller.toggleExpanded(run.id);
1842
- controller.activate(run.id);
1843
- },
1844
- onContextMenu: (e) => {
1845
- e.preventDefault();
1846
- controller.requestContextMenu(run.id, e.clientX, e.clientY);
1847
- },
1848
- label: /* @__PURE__ */ jsx2(
1849
- RowLabel,
1850
- {
1851
- status: run.status,
1852
- text: run.label,
1853
- trailing: /* @__PURE__ */ jsxs2(Fragment2, { children: [
1854
- run.trigger ? /* @__PURE__ */ jsx2("span", { style: { opacity: 0.5, fontSize: 9 }, children: run.trigger }) : null,
1855
- (run.retryCount ?? 0) > 0 ? /* @__PURE__ */ jsx2(Badge2, { title: "Retries so far", children: `retry x${run.retryCount}` }) : null,
1856
- row.truncated ? /* @__PURE__ */ jsx2(TruncationBadge, { reason: row.truncated }) : null
1857
- ] })
1858
- }
1859
- ),
1860
- meta: showProgress && run.progress !== void 0 ? /* @__PURE__ */ jsx2("span", { style: { width: 48, display: "inline-flex" }, children: /* @__PURE__ */ jsx2(
1861
- ProportionBar,
1996
+ component: "chrome/Toolbar",
1997
+ props: {
1998
+ left: { node: { component: "chrome/SearchField", props: { value: { input: "view.search" }, placeholder: { value: "Filter runs\u2026" } }, on: { onChange: ui2("search", { value: { arg: "" } }) } } },
1999
+ right: { node: { component: "chrome/ToolbarGroup", props: { end: { value: true } }, children: [{ component: "chrome/TextButton", on: { onClick: ui2("clearTerminal") }, children: [text2({ value: "Clear finished" })] }] } }
2000
+ }
2001
+ },
2002
+ {
2003
+ component: "chrome/Stack",
2004
+ when: { input: "view.hasChips" },
2005
+ props: { direction: { value: "row" }, gap: { value: 4 }, pad: { value: "inset" } },
2006
+ children: [
2007
+ { ...chip(ui2("status", { status: { item: "id" }, on: { arg: "" } })), each: { input: "view.statuses", key: "id" } },
2008
+ { ...chip(ui2("source", { id: { item: "id" }, on: { arg: "" } })), each: { input: "view.sources", key: "id" } }
2009
+ ]
2010
+ },
2011
+ { component: "chrome/EmptyState", when: { input: "view.empty" }, props: { title: { input: "view.emptyTitle" }, hint: { input: "view.emptyHint" } } },
2012
+ {
2013
+ component: "chrome/ScrollArea",
2014
+ when: { input: "view.listed" },
2015
+ children: [
1862
2016
  {
1863
- label: `${Math.round(run.progress * 100)}%`,
1864
- segments: [
1865
- { value: run.progress, opacity: 0.45 },
1866
- { value: Math.max(0, 1 - run.progress), opacity: 0.06 }
2017
+ component: "chrome/RowList",
2018
+ children: [
2019
+ {
2020
+ component: "fragment",
2021
+ each: { input: "view.rows", key: "key" },
2022
+ children: [
2023
+ {
2024
+ component: "chrome/Row",
2025
+ props: {
2026
+ noIcon: { value: true },
2027
+ depth: { item: "depth" },
2028
+ dimmed: { item: "step" },
2029
+ label: {
2030
+ node: {
2031
+ component: "chrome/Stack",
2032
+ props: { direction: { value: "row" }, pad: { value: "none" } },
2033
+ children: [
2034
+ { component: "chrome/Badge", props: { tone: { item: "tone" } }, children: [text2({ item: "status" })] },
2035
+ { component: "chrome/Text", children: [text2({ item: "label" })] },
2036
+ { component: "chrome/Text", when: { item: "trigger" }, props: { role: { value: "caption" } }, children: [text2({ item: "trigger" })] },
2037
+ { component: "chrome/Badge", when: { item: "retries" }, props: { title: { value: "Retries so far" } }, children: [text2({ item: "retries" })] },
2038
+ { component: "chrome/Badge", when: { item: "truncated" }, props: { tone: { value: "warning" }, title: { item: "truncatedHint" } }, children: [text2({ item: "truncated" })] }
2039
+ ]
2040
+ }
2041
+ },
2042
+ meta: {
2043
+ node: {
2044
+ component: "fragment",
2045
+ children: [
2046
+ { component: "chrome/Progress", when: { item: "hasProgress" }, props: { value: { item: "progress" }, label: { item: "progressLabel" } } },
2047
+ { component: "chrome/Text", when: { item: "duration" }, props: { role: { value: "number" } }, children: [text2({ item: "duration" })] }
2048
+ ]
2049
+ }
2050
+ },
2051
+ trailing: {
2052
+ node: {
2053
+ component: "fragment",
2054
+ children: [
2055
+ { component: "chrome/IconButton", when: { item: "canCancel" }, props: { size: { value: "sm" }, icon: { node: { component: "icons/Cancel" } }, label: { item: "cancelLabel" } }, on: { onClick: ui2("cancel", { runId: { item: "runId" } }) } },
2056
+ { component: "chrome/IconButton", when: { item: "canRetry" }, props: { size: { value: "sm" }, icon: { node: { component: "icons/Retry" } }, label: { item: "retryLabel" } }, on: { onClick: ui2("retry", { runId: { item: "runId" } }) } }
2057
+ ]
2058
+ }
2059
+ }
2060
+ },
2061
+ on: { onClick: ui2("row", { runId: { item: "runId" }, stepPath: { item: "stepPath" } }), onContextMenuAt: ui2("contextMenu", { runId: { item: "runId" }, x: { arg: "", index: 0 }, y: { arg: "", index: 1 } }) }
2062
+ },
2063
+ {
2064
+ component: "chrome/Note",
2065
+ when: { item: "hasError" },
2066
+ props: { variant: { value: "inset" } },
2067
+ // The host's sentence, verbatim. The panel never composes error prose.
2068
+ children: [{ component: "chrome/Badge", props: { tone: { value: "error" } }, children: [text2({ item: "errorCode" })] }, text2({ value: " " }), text2({ item: "errorMessage" })]
2069
+ }
2070
+ ]
2071
+ }
1867
2072
  ]
1868
2073
  }
1869
- ) }) : duration !== null ? /* @__PURE__ */ jsx2("span", { style: { fontVariantNumeric: "tabular-nums", opacity: 0.6 }, children: formatDuration(duration) }) : null,
1870
- trailing: /* @__PURE__ */ jsxs2(Fragment2, { children: [
1871
- canCancel(run) ? /* @__PURE__ */ jsx2(
1872
- IconButton2,
1873
- {
1874
- size: "sm",
1875
- icon: /* @__PURE__ */ jsx2(CancelGlyph, {}),
1876
- label: `Cancel ${run.label}`,
1877
- onClick: () => controller.cancel(run.id)
1878
- }
1879
- ) : null,
1880
- canRetry(run) ? /* @__PURE__ */ jsx2(
1881
- IconButton2,
1882
- {
1883
- size: "sm",
1884
- icon: /* @__PURE__ */ jsx2(RetryGlyph, {}),
1885
- label: `Retry ${run.label}`,
1886
- onClick: () => controller.retry(run.id)
1887
- }
1888
- ) : null
1889
- ] })
2074
+ ]
2075
+ },
2076
+ { component: "chrome/StatusBar", props: { left: { input: "view.countLabel" }, right: { input: "view.statsLabel" } } }
2077
+ ]
2078
+ }
2079
+ };
2080
+
2081
+ // src/ops/runs/react/RunsPanelView.tsx
2082
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2083
+ var CancelGlyph = ICONS["icons/Cancel"];
2084
+ var RetryGlyph = ICONS["icons/Retry"];
2085
+ function RunsPanelView({ controller, ...options }) {
2086
+ useSyncExternalStore2(controller.subscribe, controller.getState, controller.getState);
2087
+ const view = runsViewState(controller, options);
2088
+ const ui3 = (intent) => applyRunsUi(controller, intent);
2089
+ const chip2 = (c, onChange) => /* @__PURE__ */ jsx2(FilterChip2, { on: c.on, title: c.title, onChange, children: /* @__PURE__ */ jsx2(Badge2, { tone: c.tone, children: c.label }) }, c.id);
2090
+ return /* @__PURE__ */ jsxs2(Column2, { children: [
2091
+ /* @__PURE__ */ jsx2(
2092
+ Toolbar2,
2093
+ {
2094
+ left: /* @__PURE__ */ jsx2(SearchField2, { value: view.search, placeholder: "Filter runs\u2026", onChange: (value) => ui3({ kind: "search", value }) }),
2095
+ right: /* @__PURE__ */ jsx2(ToolbarGroup2, { end: true, children: /* @__PURE__ */ jsx2(TextButton, { onClick: () => ui3({ kind: "clearTerminal" }), children: "Clear finished" }) })
1890
2096
  }
1891
2097
  ),
1892
- run.error ? /* @__PURE__ */ jsxs2("div", { style: { padding: "0 10px 3px 24px", fontSize: 10, opacity: 0.75 }, children: [
1893
- /* @__PURE__ */ jsx2(Badge2, { tone: "error", children: run.error.code }),
1894
- " ",
1895
- run.error.message
1896
- ] }) : null
1897
- ] });
1898
- }
1899
- function StepRow({
1900
- row,
1901
- controller,
1902
- showProgress
1903
- }) {
1904
- const step = row.step;
1905
- const elapsed = step.startedAt !== void 0 && step.endedAt !== void 0 ? step.endedAt - step.startedAt : null;
1906
- return /* @__PURE__ */ jsx2(
1907
- Row2,
1908
- {
1909
- noIcon: true,
1910
- style: { paddingLeft: 12 + row.depth * 12, opacity: 0.85 },
1911
- onClick: () => {
1912
- if (row.hasChildren) controller.toggleExpanded(row.runId, row.stepPath);
1913
- controller.activate(row.runId, row.stepPath);
1914
- },
1915
- label: /* @__PURE__ */ jsx2(
1916
- RowLabel,
2098
+ view.hasChips ? /* @__PURE__ */ jsxs2(Stack2, { direction: "row", gap: 4, pad: "inset", children: [
2099
+ view.statuses.map((c) => chip2(c, (on) => ui3({ kind: "status", status: c.id, on }))),
2100
+ view.sources.map((c) => chip2(c, (on) => ui3({ kind: "source", id: c.id, on })))
2101
+ ] }) : null,
2102
+ view.empty ? /* @__PURE__ */ jsx2(EmptyState2, { title: view.emptyTitle, hint: view.emptyHint }) : null,
2103
+ view.listed ? /* @__PURE__ */ jsx2(ScrollArea2, { children: /* @__PURE__ */ jsx2(RowList2, { children: view.rows.map((row) => /* @__PURE__ */ jsxs2(Fragment2, { children: [
2104
+ /* @__PURE__ */ jsx2(
2105
+ Row2,
1917
2106
  {
1918
- status: step.status,
1919
- text: step.label,
1920
- trailing: row.truncated ? /* @__PURE__ */ jsx2(TruncationBadge, { reason: row.truncated }) : null
1921
- }
2107
+ noIcon: true,
2108
+ depth: row.depth,
2109
+ dimmed: row.step,
2110
+ label: /* @__PURE__ */ jsxs2(Stack2, { direction: "row", pad: "none", children: [
2111
+ /* @__PURE__ */ jsx2(Badge2, { tone: row.tone, children: row.status }),
2112
+ /* @__PURE__ */ jsx2(Text2, { children: row.label }),
2113
+ row.trigger ? /* @__PURE__ */ jsx2(Text2, { role: "caption", children: row.trigger }) : null,
2114
+ row.retries ? /* @__PURE__ */ jsx2(Badge2, { title: "Retries so far", children: row.retries }) : null,
2115
+ row.truncated ? /* @__PURE__ */ jsx2(Badge2, { tone: "warning", title: row.truncatedHint, children: row.truncated }) : null
2116
+ ] }),
2117
+ meta: /* @__PURE__ */ jsxs2(Fragment2, { children: [
2118
+ row.hasProgress ? /* @__PURE__ */ jsx2(Progress, { value: row.progress, label: row.progressLabel }) : null,
2119
+ row.duration ? /* @__PURE__ */ jsx2(Text2, { role: "number", children: row.duration }) : null
2120
+ ] }),
2121
+ trailing: /* @__PURE__ */ jsxs2(Fragment2, { children: [
2122
+ row.canCancel ? /* @__PURE__ */ jsx2(IconButton2, { size: "sm", icon: /* @__PURE__ */ jsx2(CancelGlyph, {}), label: row.cancelLabel, onClick: () => ui3({ kind: "cancel", runId: row.runId }) }) : null,
2123
+ row.canRetry ? /* @__PURE__ */ jsx2(IconButton2, { size: "sm", icon: /* @__PURE__ */ jsx2(RetryGlyph, {}), label: row.retryLabel, onClick: () => ui3({ kind: "retry", runId: row.runId }) }) : null
2124
+ ] }),
2125
+ onClick: () => ui3({ kind: "row", runId: row.runId, stepPath: row.stepPath }),
2126
+ onContextMenuAt: (x, y) => ui3({ kind: "contextMenu", runId: row.runId, x, y })
2127
+ },
2128
+ row.key
1922
2129
  ),
1923
- meta: showProgress && step.progress !== void 0 ? /* @__PURE__ */ jsx2("span", { style: { width: 48, display: "inline-flex" }, children: /* @__PURE__ */ jsx2(
1924
- ProportionBar,
1925
- {
1926
- label: `${Math.round(step.progress * 100)}%`,
1927
- segments: [
1928
- { value: step.progress, opacity: 0.45 },
1929
- { value: Math.max(0, 1 - step.progress), opacity: 0.06 }
1930
- ]
1931
- }
1932
- ) }) : elapsed !== null ? /* @__PURE__ */ jsx2("span", { style: { fontVariantNumeric: "tabular-nums", opacity: 0.6 }, children: formatDuration(elapsed) }) : null
1933
- }
1934
- );
2130
+ row.hasError ? /* @__PURE__ */ jsxs2(Note, { variant: "inset", children: [
2131
+ /* @__PURE__ */ jsx2(Badge2, { tone: "error", children: row.errorCode }),
2132
+ " ",
2133
+ row.errorMessage
2134
+ ] }, `${row.key}:error`) : null
2135
+ ] })) }) }) : null,
2136
+ /* @__PURE__ */ jsx2(StatusBar2, { left: view.countLabel, right: view.statsLabel })
2137
+ ] });
1935
2138
  }
1936
2139
 
1937
2140
  // src/ops/runs/panel.ts
@@ -2822,11 +3025,11 @@ function omissionLabel(omission) {
2822
3025
  }
2823
3026
 
2824
3027
  // src/ops/diff/controller.ts
2825
- var EMPTY = { files: [], status: "idle" };
3028
+ var EMPTY3 = { files: [], status: "idle" };
2826
3029
  var DiffController = class {
2827
3030
  host;
2828
3031
  reviewEnabled;
2829
- model = EMPTY;
3032
+ model = EMPTY3;
2830
3033
  rev = -1;
2831
3034
  decisions = /* @__PURE__ */ new Map();
2832
3035
  view;
@@ -3089,7 +3292,7 @@ var DiffController = class {
3089
3292
  /** Tear down. */
3090
3293
  dispose() {
3091
3294
  this.listeners.clear();
3092
- this.model = EMPTY;
3295
+ this.model = EMPTY3;
3093
3296
  this.decisions.clear();
3094
3297
  }
3095
3298
  };
@@ -3245,8 +3448,8 @@ var MARK = {
3245
3448
  context: {}
3246
3449
  };
3247
3450
  var SIGIL = { added: "+", removed: "-", context: " " };
3248
- function visible(text, show) {
3249
- return show ? text.replace(/\t/g, "\u2192 ").replace(/ /g, "\xB7") : text;
3451
+ function visible(text3, show) {
3452
+ return show ? text3.replace(/\t/g, "\u2192 ").replace(/ /g, "\xB7") : text3;
3250
3453
  }
3251
3454
  function LineText({
3252
3455
  line,
@@ -3629,9 +3832,13 @@ function createDiffPanel(options = {}) {
3629
3832
  var diffPanel = createDiffPanel();
3630
3833
  export {
3631
3834
  ALL_STATUSES,
3835
+ BEHAVIOURS as CONSOLE_BEHAVIOURS,
3836
+ CONSOLE_DECLARATION,
3632
3837
  CONSOLE_PANEL_ID,
3838
+ CONSOLE_RUNTIME_UNITS,
3633
3839
  ConsoleController,
3634
3840
  ConsolePanelView,
3841
+ ConsoleScroller,
3635
3842
  DEFAULT_MAX_STEP_DEPTH,
3636
3843
  DIFF_PANEL_ID,
3637
3844
  DiffController,
@@ -3639,7 +3846,10 @@ export {
3639
3846
  LEVEL_ORDER,
3640
3847
  MESSAGE_ELISION,
3641
3848
  REDACTION_CARRY,
3849
+ BEHAVIOURS2 as RUNS_BEHAVIOURS,
3850
+ RUNS_DECLARATION,
3642
3851
  RUNS_PANEL_ID,
3852
+ RUNS_RUNTIME_UNITS,
3643
3853
  RUN_PATH_SEPARATOR,
3644
3854
  RUN_STATUSES,
3645
3855
  RunsController,
@@ -3650,12 +3860,17 @@ export {
3650
3860
  TerminalPanelView,
3651
3861
  XENO_TERMINAL_THEME_KEYS,
3652
3862
  appendStepsAtPath,
3863
+ applyConsoleUi,
3864
+ applyRunsUi,
3653
3865
  canCancel,
3654
3866
  canRetry,
3655
3867
  collapseAdjacent,
3656
3868
  computeStats,
3657
3869
  consoleManifest,
3658
3870
  consolePanel,
3871
+ consoleRuntimeComponents,
3872
+ consoleViewManifest,
3873
+ consoleViewState,
3659
3874
  countChanges,
3660
3875
  createConsolePanel,
3661
3876
  createDiffPanel,
@@ -3688,6 +3903,8 @@ export {
3688
3903
  resolveStepPath,
3689
3904
  runsManifest,
3690
3905
  runsPanel,
3906
+ runsViewManifest,
3907
+ runsViewState,
3691
3908
  safeJson,
3692
3909
  spliceRedactedAppend,
3693
3910
  splitRows,
@@ -3695,5 +3912,6 @@ export {
3695
3912
  terminalManifest,
3696
3913
  terminalPanel,
3697
3914
  terminalThemeVar,
3915
+ toneOf,
3698
3916
  truncateLogMessage
3699
3917
  };