@xenosystem/blocks 0.13.10 → 0.13.12

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
@@ -8,10 +8,9 @@ import {
8
8
  } from "../chunk-WE6A2DTY.js";
9
9
  import {
10
10
  CancelGlyph,
11
- ClearGlyph,
12
- CopyGlyph,
11
+ ICONS,
13
12
  RetryGlyph
14
- } from "../chunk-K7YLLV2W.js";
13
+ } from "../chunk-5BVZMAD2.js";
15
14
  import "../chunk-2KG3PWR4.js";
16
15
 
17
16
  // src/ops/console/panel.ts
@@ -63,10 +62,10 @@ function spliceRedactedAppend(existing, addition, redact, carry = REDACTION_CARR
63
62
  const tail = existing.slice(cut);
64
63
  return head + redact(tail + addition);
65
64
  }
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));
65
+ function truncateLogMessage(text2, max) {
66
+ if (max <= 0 || text2.length <= max) return text2;
67
+ if (max <= MESSAGE_ELISION.length) return text2.slice(text2.length - max);
68
+ return MESSAGE_ELISION + text2.slice(text2.length - (max - MESSAGE_ELISION.length));
70
69
  }
71
70
  function formatRecord(record) {
72
71
  const time = new Date(record.ts).toISOString();
@@ -243,7 +242,7 @@ var ConsoleController = class {
243
242
  */
244
243
  merge(existing, patch) {
245
244
  const secrets = [...this.host.getSecrets?.() ?? []];
246
- const redact = (text) => redactForLog(text, secrets);
245
+ const redact = (text2) => redactForLog(text2, secrets);
247
246
  let message = existing.message;
248
247
  if (typeof patch.message === "string") message = redact(patch.message);
249
248
  if (typeof patch.appendMessage === "string" && patch.appendMessage.length > 0) {
@@ -388,6 +387,29 @@ var ConsoleController = class {
388
387
  }
389
388
  /* ── Interaction ───────────────────────────────────────────────────────── */
390
389
  /** Follow the tail. Auto-disabled by the view when the user scrolls up. */
390
+ /* ── View state the declaration cannot hold ────────────────────────────── */
391
+ expandedId = null;
392
+ viewport = { height: 0, scrollTop: 0 };
393
+ /** Which record's data is expanded under its row, if any. */
394
+ getExpanded() {
395
+ return this.expandedId;
396
+ }
397
+ /** Toggle a record's expanded detail (only records carrying `data` expand). */
398
+ toggleExpanded(recordId) {
399
+ const record = this.records.find((r) => r.id === recordId);
400
+ if (!record || record.data === void 0) return;
401
+ this.expandedId = this.expandedId === recordId ? null : recordId;
402
+ this.notify();
403
+ }
404
+ /** The scroller reports its viewport; the view state windows the rows from it. */
405
+ setViewport(height, scrollTop) {
406
+ if (this.viewport.height === height && this.viewport.scrollTop === scrollTop) return;
407
+ this.viewport = { height, scrollTop };
408
+ this.notify();
409
+ }
410
+ getViewport() {
411
+ return this.viewport;
412
+ }
391
413
  setFollow(follow) {
392
414
  if (this.follow === follow) return;
393
415
  this.follow = follow;
@@ -607,203 +629,289 @@ var consoleManifest = {
607
629
  };
608
630
 
609
631
  // src/ops/console/react/ConsolePanelView.tsx
610
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
632
+ import { useEffect, useRef, useSyncExternalStore } from "react";
611
633
  import {
612
634
  Badge,
635
+ Code,
636
+ Column,
613
637
  EmptyState,
638
+ FilterChip,
614
639
  IconButton,
615
640
  Row,
616
641
  RowList,
617
642
  ScrollArea,
618
643
  SearchField,
619
644
  SegmentedControl,
645
+ Stack,
620
646
  StatusBar,
647
+ Text,
621
648
  Toolbar,
622
- ToolbarGroup
649
+ ToolbarGroup,
650
+ VirtualWindow
623
651
  } from "@xenosystem/components/chrome";
652
+
653
+ // src/ops/console/declaration.ts
624
654
  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"
655
+ var TONE = { trace: "neutral", debug: "neutral", info: "info", warn: "warning", error: "error" };
656
+ function consoleViewState(c, options = {}) {
657
+ const s = c.getState();
658
+ const rowHeight = options.rowHeight ?? 20;
659
+ const showTimestamps = options.showTimestamps ?? true;
660
+ const viewport = c.getViewport();
661
+ const window_ = computeWindow({ scrollTop: viewport.scrollTop, viewportHeight: viewport.height || rowHeight * 20, rowHeight, rowCount: s.rows.length });
662
+ const expanded = c.getExpanded();
663
+ return {
664
+ search: s.filter.search ?? "",
665
+ levels: LEVEL_ORDER.map((l) => ({ value: l, label: l.slice(0, 1).toUpperCase() })),
666
+ level: s.filter.minLevel ?? "trace",
667
+ errors: s.counts.error > 0 ? String(s.counts.error) : void 0,
668
+ warns: s.counts.warn > 0 ? String(s.counts.warn) : void 0,
669
+ multiSource: s.sources.length > 1,
670
+ sources: s.sources.map((src) => ({ id: src.id, label: src.label ?? src.id, on: src.enabled, title: `${src.count} records` })),
671
+ empty: s.rows.length === 0,
672
+ emptyTitle: s.total > 0 ? "No matches" : "No output",
673
+ emptyHint: s.total > 0 ? "Nothing matches the current filter." : options.emptyHint ?? "Run something to see its output here.",
674
+ listed: s.rows.length > 0,
675
+ rowHeight,
676
+ totalHeight: window_.totalHeight,
677
+ offsetTop: window_.offsetTop,
678
+ rows: s.rows.slice(window_.startIndex, window_.endIndex).map((record) => ({
679
+ id: record.id,
680
+ time: showTimestamps ? new Date(record.ts).toISOString().slice(11, 23) : void 0,
681
+ level: record.level,
682
+ tone: TONE[record.level],
683
+ source: record.source,
684
+ message: record.message,
685
+ repeat: (record.count ?? 1) > 1 ? `x${record.count}` : void 0,
686
+ expanded: expanded === record.id && record.data !== void 0,
687
+ // Already redacted — the buffer never held the raw value.
688
+ detail: expanded === record.id && record.data !== void 0 ? safeJson(record.data) : void 0
689
+ })),
690
+ countLabel: `${s.rows.length} of ${s.total}` + (s.dropped > 0 ? ` \xB7 ${s.dropped} dropped` : "") + (s.droppedPatches > 0 ? ` \xB7 ${s.droppedPatches} unmatched` : ""),
691
+ followLabel: s.follow ? "following" : void 0
692
+ };
693
+ }
694
+ function applyConsoleUi(c, intent) {
695
+ switch (intent.kind) {
696
+ case "search":
697
+ c.setSearch(intent.value);
698
+ break;
699
+ case "level":
700
+ c.setLevel(intent.level === "trace" ? void 0 : intent.level);
701
+ break;
702
+ case "copyVisible":
703
+ void globalThis.navigator?.clipboard?.writeText(c.copyVisible());
704
+ break;
705
+ case "clear":
706
+ c.clear();
707
+ break;
708
+ case "source":
709
+ c.toggleSource(intent.id, intent.on);
710
+ break;
711
+ case "row":
712
+ c.toggleExpanded(intent.id);
713
+ c.activate(intent.id);
714
+ break;
715
+ case "contextMenu":
716
+ c.requestContextMenu(intent.id, intent.x, intent.y);
717
+ break;
718
+ }
719
+ }
720
+ var consoleViewManifest = {
721
+ id: consoleManifest.id,
722
+ version: "1.0.0",
723
+ title: "Console",
724
+ category: "ops",
725
+ layout: { slot: "bottom" },
726
+ inputs: [{ id: "view", type: "object", label: "View state", description: "The ConsoleViewModel the runtime derives from the controller on every draw." }],
727
+ outputs: [{ id: "ui", type: "object", label: "UI intent", description: "{ kind, \u2026 } \u2014 applied by the runtime to the controller." }],
728
+ commands: []
632
729
  };
633
- function ConsolePanelView({
634
- controller,
635
- rowHeight = 20,
636
- showTimestamps = true,
637
- emptyHint
638
- }) {
730
+ 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" };
731
+ var BEHAVIOURS = {
732
+ "ops/consolePanelView": (inputs) => inputs.view ?? EMPTY
733
+ };
734
+ var CONSOLE_RUNTIME_UNITS = ["console/Scroller"];
735
+ var text = (b) => ({ component: "text", props: { text: b } });
736
+ var ui = (kind, fields = {}) => ({ emit: "ui", value: { object: { kind: { value: kind }, ...fields } } });
737
+ var CONSOLE_DECLARATION = {
738
+ format: "xeno.block@1",
739
+ manifest: consoleViewManifest,
740
+ behaviour: "ops/consolePanelView",
741
+ composition: {
742
+ component: "chrome/Column",
743
+ children: [
744
+ {
745
+ component: "chrome/Toolbar",
746
+ props: {
747
+ left: {
748
+ node: {
749
+ component: "fragment",
750
+ children: [
751
+ { component: "chrome/SearchField", props: { value: { input: "view.search" }, placeholder: { value: "Filter\u2026" } }, on: { onChange: ui("search", { value: { arg: "" } }) } },
752
+ { 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: "" } }) } }
753
+ ]
754
+ }
755
+ },
756
+ right: {
757
+ node: {
758
+ component: "chrome/ToolbarGroup",
759
+ props: { end: { value: true } },
760
+ children: [
761
+ { component: "chrome/Badge", when: { input: "view.errors" }, props: { tone: { value: "error" } }, children: [text({ input: "view.errors" })] },
762
+ { component: "chrome/Badge", when: { input: "view.warns" }, props: { tone: { value: "warning" } }, children: [text({ input: "view.warns" })] },
763
+ { component: "chrome/IconButton", props: { icon: { node: { component: "icons/Copy" } }, label: { value: "Copy visible" } }, on: { onClick: ui("copyVisible") } },
764
+ { component: "chrome/IconButton", props: { icon: { node: { component: "icons/Clear" } }, label: { value: "Clear" } }, on: { onClick: ui("clear") } }
765
+ ]
766
+ }
767
+ }
768
+ }
769
+ },
770
+ {
771
+ component: "chrome/Stack",
772
+ when: { input: "view.multiSource" },
773
+ props: { direction: { value: "row" }, gap: { value: 4 }, pad: { value: "inset" } },
774
+ children: [
775
+ { 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" })] }
776
+ ]
777
+ },
778
+ { component: "chrome/EmptyState", when: { input: "view.empty" }, props: { title: { input: "view.emptyTitle" }, hint: { input: "view.emptyHint" } } },
779
+ {
780
+ component: "console/Scroller",
781
+ when: { input: "view.listed" },
782
+ children: [
783
+ {
784
+ component: "chrome/VirtualWindow",
785
+ props: { totalHeight: { input: "view.totalHeight" }, offsetTop: { input: "view.offsetTop" } },
786
+ children: [
787
+ {
788
+ component: "chrome/RowList",
789
+ props: { role: { value: "log" } },
790
+ children: [
791
+ {
792
+ component: "fragment",
793
+ each: { input: "view.rows", key: "id" },
794
+ children: [
795
+ {
796
+ component: "chrome/Row",
797
+ props: {
798
+ height: { input: "view.rowHeight" },
799
+ noIcon: { value: true },
800
+ label: {
801
+ node: {
802
+ component: "chrome/Stack",
803
+ props: { direction: { value: "row" }, pad: { value: "none" } },
804
+ children: [
805
+ { component: "chrome/Text", when: { item: "time" }, props: { role: { value: "number" } }, children: [text({ item: "time" })] },
806
+ { component: "chrome/Badge", props: { tone: { item: "tone" } }, children: [text({ item: "level" })] },
807
+ { component: "chrome/Text", when: { item: "source" }, props: { role: { value: "meta" } }, children: [text({ item: "source" })] },
808
+ { component: "chrome/Text", children: [text({ item: "message" })] }
809
+ ]
810
+ }
811
+ },
812
+ trailing: { node: { component: "chrome/Badge", when: { item: "repeat" }, props: { title: { value: "Repeated consecutively" } }, children: [text({ item: "repeat" })] } }
813
+ },
814
+ on: { onClick: ui("row", { id: { item: "id" } }), onContextMenuAt: ui("contextMenu", { id: { item: "id" }, x: { arg: "", index: 0 }, y: { arg: "", index: 1 } }) }
815
+ },
816
+ { component: "chrome/Code", when: { item: "expanded" }, children: [text({ item: "detail" })] }
817
+ ]
818
+ }
819
+ ]
820
+ }
821
+ ]
822
+ }
823
+ ]
824
+ },
825
+ { component: "chrome/StatusBar", props: { left: { input: "view.countLabel" }, right: { input: "view.followLabel" } } }
826
+ ]
827
+ }
828
+ };
829
+
830
+ // src/ops/console/react/ConsolePanelView.tsx
831
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
832
+ var CopyGlyph = ICONS["icons/Copy"];
833
+ var ClearGlyph = ICONS["icons/Clear"];
834
+ function ConsoleScroller({ controller, rowHeight = 20, children }) {
639
835
  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);
836
+ const ref = useRef(null);
643
837
  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
- }, []);
838
+ const el = ref.current;
839
+ if (!el) return;
840
+ controller.setViewport(el.clientHeight, el.scrollTop);
841
+ const RO = globalThis.ResizeObserver;
842
+ if (!RO) return;
843
+ const ro = new RO(() => controller.setViewport(el.clientHeight, el.scrollTop));
844
+ ro.observe(el);
845
+ return () => ro.disconnect();
846
+ }, [controller]);
651
847
  useEffect(() => {
652
- const element = scrollRef.current;
653
- if (!element || !state.follow) return;
654
- element.scrollTop = element.scrollHeight;
848
+ const el = ref.current;
849
+ if (!el || !state.follow) return;
850
+ el.scrollTop = el.scrollHeight;
655
851
  }, [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]
852
+ return /* @__PURE__ */ jsx(
853
+ ScrollArea,
854
+ {
855
+ ref,
856
+ onScroll: (e) => {
857
+ const el = e.currentTarget;
858
+ controller.setViewport(el.clientHeight, el.scrollTop);
859
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < rowHeight;
860
+ if (state.follow && !atBottom) controller.setFollow(false);
861
+ else if (!state.follow && atBottom) controller.setFollow(true);
862
+ },
863
+ children
864
+ }
665
865
  );
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: [
866
+ }
867
+ function consoleRuntimeComponents(controller, rowHeight) {
868
+ return {
869
+ "console/Scroller": ({ children }) => /* @__PURE__ */ jsx(ConsoleScroller, { controller, rowHeight, children })
870
+ };
871
+ }
872
+ function ConsolePanelView({ controller, ...options }) {
873
+ useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
874
+ const view = consoleViewState(controller, options);
875
+ const ui2 = (intent) => applyConsoleUi(controller, intent);
876
+ return /* @__PURE__ */ jsxs(Column, { children: [
674
877
  /* @__PURE__ */ jsx(
675
878
  Toolbar,
676
879
  {
677
880
  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
- )
881
+ /* @__PURE__ */ jsx(SearchField, { value: view.search, placeholder: "Filter\u2026", onChange: (value) => ui2({ kind: "search", value }) }),
882
+ /* @__PURE__ */ jsx(SegmentedControl, { size: "sm", label: "Minimum level", options: view.levels, value: view.level, onChange: (level) => ui2({ kind: "level", level }) })
696
883
  ] }),
697
884
  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() })
885
+ view.errors ? /* @__PURE__ */ jsx(Badge, { tone: "error", children: view.errors }) : null,
886
+ view.warns ? /* @__PURE__ */ jsx(Badge, { tone: "warning", children: view.warns }) : null,
887
+ /* @__PURE__ */ jsx(IconButton, { icon: /* @__PURE__ */ jsx(CopyGlyph, {}), label: "Copy visible", onClick: () => ui2({ kind: "copyVisible" }) }),
888
+ /* @__PURE__ */ jsx(IconButton, { icon: /* @__PURE__ */ jsx(ClearGlyph, {}), label: "Clear", onClick: () => ui2({ kind: "clear" }) })
711
889
  ] })
712
890
  }
713
891
  ),
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
892
+ 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) => ui2({ kind: "source", id: source.id, on }), children: source.label }, source.id)) }) : null,
893
+ view.empty ? /* @__PURE__ */ jsx(EmptyState, { title: view.emptyTitle, hint: view.emptyHint }) : null,
894
+ 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: [
895
+ /* @__PURE__ */ jsx(
896
+ Row,
897
+ {
898
+ height: view.rowHeight,
899
+ noIcon: true,
900
+ label: /* @__PURE__ */ jsxs(Stack, { direction: "row", pad: "none", children: [
901
+ row.time ? /* @__PURE__ */ jsx(Text, { role: "number", children: row.time }) : null,
902
+ /* @__PURE__ */ jsx(Badge, { tone: row.tone, children: row.level }),
903
+ row.source ? /* @__PURE__ */ jsx(Text, { role: "meta", children: row.source }) : null,
904
+ /* @__PURE__ */ jsx(Text, { children: row.message })
905
+ ] }),
906
+ trailing: row.repeat ? /* @__PURE__ */ jsx(Badge, { title: "Repeated consecutively", children: row.repeat }) : null,
907
+ onClick: () => ui2({ kind: "row", id: row.id }),
908
+ onContextMenuAt: (x, y) => ui2({ kind: "contextMenu", id: row.id, x, y })
803
909
  },
804
- children: safeJson(record.data)
805
- }
806
- ) : null
910
+ row.id
911
+ ),
912
+ row.expanded ? /* @__PURE__ */ jsx(Code, { children: row.detail }, `${row.id}:detail`) : null
913
+ ] })) }) }) }) : null,
914
+ /* @__PURE__ */ jsx(StatusBar, { left: view.countLabel, right: view.followLabel })
807
915
  ] });
808
916
  }
809
917
 
@@ -1815,12 +1923,12 @@ function TruncationBadge({ reason }) {
1815
1923
  }
1816
1924
  function RowLabel({
1817
1925
  status,
1818
- text,
1926
+ text: text2,
1819
1927
  trailing
1820
1928
  }) {
1821
1929
  return /* @__PURE__ */ jsxs2("span", { style: { display: "flex", gap: 6, alignItems: "center", minWidth: 0 }, children: [
1822
1930
  /* @__PURE__ */ jsx2(Badge2, { tone: toneOf(status), children: status }),
1823
- /* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: text }),
1931
+ /* @__PURE__ */ jsx2("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: text2 }),
1824
1932
  trailing
1825
1933
  ] });
1826
1934
  }
@@ -2822,11 +2930,11 @@ function omissionLabel(omission) {
2822
2930
  }
2823
2931
 
2824
2932
  // src/ops/diff/controller.ts
2825
- var EMPTY = { files: [], status: "idle" };
2933
+ var EMPTY2 = { files: [], status: "idle" };
2826
2934
  var DiffController = class {
2827
2935
  host;
2828
2936
  reviewEnabled;
2829
- model = EMPTY;
2937
+ model = EMPTY2;
2830
2938
  rev = -1;
2831
2939
  decisions = /* @__PURE__ */ new Map();
2832
2940
  view;
@@ -3089,7 +3197,7 @@ var DiffController = class {
3089
3197
  /** Tear down. */
3090
3198
  dispose() {
3091
3199
  this.listeners.clear();
3092
- this.model = EMPTY;
3200
+ this.model = EMPTY2;
3093
3201
  this.decisions.clear();
3094
3202
  }
3095
3203
  };
@@ -3245,8 +3353,8 @@ var MARK = {
3245
3353
  context: {}
3246
3354
  };
3247
3355
  var SIGIL = { added: "+", removed: "-", context: " " };
3248
- function visible(text, show) {
3249
- return show ? text.replace(/\t/g, "\u2192 ").replace(/ /g, "\xB7") : text;
3356
+ function visible(text2, show) {
3357
+ return show ? text2.replace(/\t/g, "\u2192 ").replace(/ /g, "\xB7") : text2;
3250
3358
  }
3251
3359
  function LineText({
3252
3360
  line,
@@ -3629,9 +3737,13 @@ function createDiffPanel(options = {}) {
3629
3737
  var diffPanel = createDiffPanel();
3630
3738
  export {
3631
3739
  ALL_STATUSES,
3740
+ BEHAVIOURS as CONSOLE_BEHAVIOURS,
3741
+ CONSOLE_DECLARATION,
3632
3742
  CONSOLE_PANEL_ID,
3743
+ CONSOLE_RUNTIME_UNITS,
3633
3744
  ConsoleController,
3634
3745
  ConsolePanelView,
3746
+ ConsoleScroller,
3635
3747
  DEFAULT_MAX_STEP_DEPTH,
3636
3748
  DIFF_PANEL_ID,
3637
3749
  DiffController,
@@ -3650,12 +3762,16 @@ export {
3650
3762
  TerminalPanelView,
3651
3763
  XENO_TERMINAL_THEME_KEYS,
3652
3764
  appendStepsAtPath,
3765
+ applyConsoleUi,
3653
3766
  canCancel,
3654
3767
  canRetry,
3655
3768
  collapseAdjacent,
3656
3769
  computeStats,
3657
3770
  consoleManifest,
3658
3771
  consolePanel,
3772
+ consoleRuntimeComponents,
3773
+ consoleViewManifest,
3774
+ consoleViewState,
3659
3775
  countChanges,
3660
3776
  createConsolePanel,
3661
3777
  createDiffPanel,
@@ -13,8 +13,7 @@ import {
13
13
  StopIcon,
14
14
  TRANSPORT_ICONS,
15
15
  TRANSPORT_RUNTIME_UNITS
16
- } from "../chunk-W4PTX2Y4.js";
17
- import "../chunk-K7YLLV2W.js";
16
+ } from "../chunk-5BVZMAD2.js";
18
17
  import {
19
18
  __reExport
20
19
  } from "../chunk-2KG3PWR4.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenosystem/blocks",
3
- "version": "0.13.10",
3
+ "version": "0.13.12",
4
4
  "description": "XENO Blocks — rung 3 of the front ladder: components composed into a contract-bearing unit (manifest: ports, intents, commands, agent surface), rendered by @xenosystem/workbench. ONE package, one subpath per family (/auth, /edit, /data, /time, /ops, /trust, /agent), one named export per block — XENO PACKAGE NAMING - STANDARD.md.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  "peerDependencies": {
65
65
  "@duckdb/duckdb-wasm": ">=1.28.0",
66
66
  "@xenosystem/block-sdk": ">=0.3.4 <1",
67
- "@xenosystem/components": ">=0.21.0 <1",
67
+ "@xenosystem/components": ">=0.22.0 <1",
68
68
  "@xenosystem/data-core": "^0.1.0",
69
69
  "@xenosystem/elements": ">=0.0.1",
70
70
  "@xenosystem/elements-react": ">=0.0.1",
@@ -81,7 +81,7 @@
81
81
  "@types/react": "^19.0.0",
82
82
  "@types/react-dom": "^19.0.0",
83
83
  "@xenosystem/block-sdk": "^0.3.4",
84
- "@xenosystem/components": "^0.21.0",
84
+ "@xenosystem/components": "^0.22.0",
85
85
  "@xenosystem/data-core": "^0.1.0",
86
86
  "@xenosystem/elements": "0.0.1",
87
87
  "@xenosystem/elements-react": "0.0.1",