@ai-matrx/rich-content 0.2.160 → 0.2.162

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.162
4
+ ### Unreleased
5
+
6
+ - **THE RENDER RATCHET** (`display/chat-markdown/block-registry/render-ratchet.ts`; Arman, 2026-10-14: "once you go forward, you do not go back"). `BlockRenderer` keeps, per mounted block, the furthest render stage it drew (text → loader → generic → component) and a backward frame mid-stream (a bridge declining a half-arrived item, a mid-string parse, an emptied/retracted frame, a late row) keeps the last good decision on screen instead of swapping the component for its loader and back (each swap was an unmount + remount). It lets go when the block settles (the final value is the truth), when a different block takes the slot, or mid-stream only after `RATCHET_RELEASE_MIN_FRAMES` (120) distinct frames AND `RATCHET_RELEASE_MIN_MS` (8 s). Consumer action: none.
7
+ - **One measured loader answer** (SHAPES-BUGS 2.5): `resolveLoadingSlugForKind` answers `origin: "pending"` while the kind's schema is still being read (host port `schemaState`, optional), never a premature "generic". New `useLoadingSlugForKind(kind)` (`kinds/react/loading/use-loading-slug`) demands the schema and re-reads on arrival — every surface reads it. Consumer action: matrx-frontend's Stream-tab facts and Preview loading path use the hook.
8
+ - **A wrapping XML tag is consumed** (SHAPES-BUGS 2.2): `<output>\n{…kind…}\n</output>` no longer leaves `<output>` / `</output>` as two stray code cards around the component, live or reloaded. Consumer action: none.
9
+
10
+ ## 0.2.161
11
+ ### Unreleased
12
+
13
+ - **`KindRenderHarness` / `useInsideKindRenderHarness`** (`kinds/react/render-harness`): a harness (kind-registry Preview, Shape Studio) wraps its renders in it, and `GenericStructuredBlock` files no `generic_floor_render` sighting from inside one. Scoped to the subtree — a generic block elsewhere on the same page still files. Consumer action: matrx-frontend wraps `KindRenderPaths` output in `<KindRenderHarness>`.
14
+
3
15
  ## 0.2.160
4
16
 
5
17
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -2,7 +2,7 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { renderElement } from "@ai-matrx/alchemy/operate/capture";
4
4
  import { downloadFile, downloadUrl } from "@ai-matrx/kit/download";
5
- import { Button, Chip } from "@ai-matrx/design-system/controls";
5
+ import { Button, Chip, Select } from "@ai-matrx/design-system/controls";
6
6
  import {
7
7
  useState,
8
8
  useCallback,
@@ -1408,18 +1408,19 @@ const DiagramFlow = ({
1408
1408
  ] }),
1409
1409
  selectedIsSection ? /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1410
1410
  "Border",
1411
- /* @__PURE__ */ jsxs(
1412
- "select",
1411
+ /* @__PURE__ */ jsx(
1412
+ Select,
1413
1413
  {
1414
+ "aria-label": "Border",
1415
+ className: "mt-1 w-full",
1414
1416
  value: selectedNode.data.groupStyle === "solid" || selectedNode.data.groupStyle === "dotted" ? selectedNode.data.groupStyle : "dashed",
1415
- onChange: (event) => patchSelectedNode({
1416
- groupStyle: event.target.value
1417
+ onValueChange: (v) => patchSelectedNode({
1418
+ groupStyle: v
1417
1419
  }),
1418
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1419
- children: [
1420
- /* @__PURE__ */ jsx("option", { value: "dashed", children: "Dashed" }),
1421
- /* @__PURE__ */ jsx("option", { value: "dotted", children: "Dotted" }),
1422
- /* @__PURE__ */ jsx("option", { value: "solid", children: "Solid" })
1420
+ options: [
1421
+ { value: "dashed", label: "Dashed" },
1422
+ { value: "dotted", label: "Dotted" },
1423
+ { value: "solid", label: "Solid" }
1423
1424
  ]
1424
1425
  }
1425
1426
  )
@@ -1442,36 +1443,38 @@ const DiagramFlow = ({
1442
1443
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1443
1444
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1444
1445
  "Style",
1445
- /* @__PURE__ */ jsxs(
1446
- "select",
1446
+ /* @__PURE__ */ jsx(
1447
+ Select,
1447
1448
  {
1449
+ "aria-label": "Style",
1450
+ className: "mt-1 w-full",
1448
1451
  value: typeof selectedNode.data.nodeType === "string" ? selectedNode.data.nodeType : "default",
1449
- onChange: (event) => patchSelectedNode({ nodeType: event.target.value }),
1450
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1451
- children: [
1452
- /* @__PURE__ */ jsx("option", { value: "default", children: "Plain" }),
1453
- /* @__PURE__ */ jsx("option", { value: "start", children: "Start" }),
1454
- /* @__PURE__ */ jsx("option", { value: "process", children: "Step" }),
1455
- /* @__PURE__ */ jsx("option", { value: "decision", children: "Decision" }),
1456
- /* @__PURE__ */ jsx("option", { value: "data", children: "Information" }),
1457
- /* @__PURE__ */ jsx("option", { value: "end", children: "Finish" })
1452
+ onValueChange: (v) => patchSelectedNode({ nodeType: v }),
1453
+ options: [
1454
+ { value: "default", label: "Plain" },
1455
+ { value: "start", label: "Start" },
1456
+ { value: "process", label: "Step" },
1457
+ { value: "decision", label: "Decision" },
1458
+ { value: "data", label: "Information" },
1459
+ { value: "end", label: "Finish" }
1458
1460
  ]
1459
1461
  }
1460
1462
  )
1461
1463
  ] }),
1462
1464
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1463
1465
  "Section",
1464
- /* @__PURE__ */ jsxs(
1465
- "select",
1466
+ /* @__PURE__ */ jsx(
1467
+ Select,
1466
1468
  {
1469
+ "aria-label": "Section",
1470
+ className: "mt-1 w-full",
1467
1471
  value: selectedNode.parentId ?? "none",
1468
- onChange: (event) => moveSelectedBoxToSection(
1469
- event.target.value === "none" ? null : event.target.value
1472
+ onValueChange: (v) => moveSelectedBoxToSection(
1473
+ v === "none" ? null : v
1470
1474
  ),
1471
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1472
- children: [
1473
- /* @__PURE__ */ jsx("option", { value: "none", children: "No section" }),
1474
- sections.map((section) => /* @__PURE__ */ jsx("option", { value: section.id, children: String(section.data.label ?? "Section") }, section.id))
1475
+ options: [
1476
+ { value: "none", label: "No section" },
1477
+ ...sections.map((section) => ({ value: section.id, label: String(section.data.label ?? "Section") }))
1475
1478
  ]
1476
1479
  }
1477
1480
  )
@@ -1533,39 +1536,41 @@ const DiagramFlow = ({
1533
1536
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1534
1537
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1535
1538
  "Shape",
1536
- /* @__PURE__ */ jsxs(
1537
- "select",
1539
+ /* @__PURE__ */ jsx(
1540
+ Select,
1538
1541
  {
1542
+ "aria-label": "Shape",
1543
+ className: "mt-1 w-full",
1539
1544
  value: String(selectedNode.data.shape ?? "rounded"),
1540
- onChange: (event) => patchSelectedNode({
1541
- shape: event.target.value
1545
+ onValueChange: (v) => patchSelectedNode({
1546
+ shape: v
1542
1547
  }),
1543
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1544
- children: [
1545
- /* @__PURE__ */ jsx("option", { value: "rounded", children: "Rounded" }),
1546
- /* @__PURE__ */ jsx("option", { value: "rectangle", children: "Rectangle" }),
1547
- /* @__PURE__ */ jsx("option", { value: "pill", children: "Pill" }),
1548
- /* @__PURE__ */ jsx("option", { value: "circle", children: "Circle" }),
1549
- /* @__PURE__ */ jsx("option", { value: "diamond", children: "Diamond" }),
1550
- /* @__PURE__ */ jsx("option", { value: "hexagon", children: "Hexagon" })
1548
+ options: [
1549
+ { value: "rounded", label: "Rounded" },
1550
+ { value: "rectangle", label: "Rectangle" },
1551
+ { value: "pill", label: "Pill" },
1552
+ { value: "circle", label: "Circle" },
1553
+ { value: "diamond", label: "Diamond" },
1554
+ { value: "hexagon", label: "Hexagon" }
1551
1555
  ]
1552
1556
  }
1553
1557
  )
1554
1558
  ] }),
1555
1559
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1556
1560
  "Border",
1557
- /* @__PURE__ */ jsxs(
1558
- "select",
1561
+ /* @__PURE__ */ jsx(
1562
+ Select,
1559
1563
  {
1564
+ "aria-label": "Border",
1565
+ className: "mt-1 w-full",
1560
1566
  value: String(selectedNode.data.borderStyle ?? "solid"),
1561
- onChange: (event) => patchSelectedNode({
1562
- borderStyle: event.target.value
1567
+ onValueChange: (v) => patchSelectedNode({
1568
+ borderStyle: v
1563
1569
  }),
1564
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1565
- children: [
1566
- /* @__PURE__ */ jsx("option", { value: "solid", children: "Solid" }),
1567
- /* @__PURE__ */ jsx("option", { value: "dashed", children: "Dashed" }),
1568
- /* @__PURE__ */ jsx("option", { value: "dotted", children: "Dotted" })
1570
+ options: [
1571
+ { value: "solid", label: "Solid" },
1572
+ { value: "dashed", label: "Dashed" },
1573
+ { value: "dotted", label: "Dotted" }
1569
1574
  ]
1570
1575
  }
1571
1576
  )
@@ -1573,17 +1578,18 @@ const DiagramFlow = ({
1573
1578
  ] }),
1574
1579
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1575
1580
  "Text",
1576
- /* @__PURE__ */ jsxs(
1577
- "select",
1581
+ /* @__PURE__ */ jsx(
1582
+ Select,
1578
1583
  {
1584
+ "aria-label": "Text",
1585
+ className: "mt-1 w-full",
1579
1586
  value: String(selectedNode.data.textAlign ?? "left"),
1580
- onChange: (event) => patchSelectedNode({
1581
- textAlign: event.target.value
1587
+ onValueChange: (v) => patchSelectedNode({
1588
+ textAlign: v
1582
1589
  }),
1583
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1584
- children: [
1585
- /* @__PURE__ */ jsx("option", { value: "left", children: "Left" }),
1586
- /* @__PURE__ */ jsx("option", { value: "center", children: "Centered" })
1590
+ options: [
1591
+ { value: "left", label: "Left" },
1592
+ { value: "center", label: "Centered" }
1587
1593
  ]
1588
1594
  }
1589
1595
  )
@@ -1609,37 +1615,39 @@ const DiagramFlow = ({
1609
1615
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1610
1616
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1611
1617
  "Path",
1612
- /* @__PURE__ */ jsxs(
1613
- "select",
1618
+ /* @__PURE__ */ jsx(
1619
+ Select,
1614
1620
  {
1621
+ "aria-label": "Path",
1622
+ className: "mt-1 w-full",
1615
1623
  value: selectedEdge.data?.edgeType === "smoothstep" || selectedEdge.data?.edgeType === "step" || selectedEdge.data?.edgeType === "straight" ? selectedEdge.data.edgeType : "bezier",
1616
- onChange: (event) => patchSelectedEdge({
1617
- edgeType: event.target.value
1624
+ onValueChange: (v) => patchSelectedEdge({
1625
+ edgeType: v
1618
1626
  }),
1619
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1620
- children: [
1621
- /* @__PURE__ */ jsx("option", { value: "bezier", children: "Curved" }),
1622
- /* @__PURE__ */ jsx("option", { value: "smoothstep", children: "Rounded" }),
1623
- /* @__PURE__ */ jsx("option", { value: "step", children: "Stepped" }),
1624
- /* @__PURE__ */ jsx("option", { value: "straight", children: "Straight" })
1627
+ options: [
1628
+ { value: "bezier", label: "Curved" },
1629
+ { value: "smoothstep", label: "Rounded" },
1630
+ { value: "step", label: "Stepped" },
1631
+ { value: "straight", label: "Straight" }
1625
1632
  ]
1626
1633
  }
1627
1634
  )
1628
1635
  ] }),
1629
1636
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1630
1637
  "Line",
1631
- /* @__PURE__ */ jsxs(
1632
- "select",
1638
+ /* @__PURE__ */ jsx(
1639
+ Select,
1633
1640
  {
1641
+ "aria-label": "Line",
1642
+ className: "mt-1 w-full",
1634
1643
  value: selectedEdge.data?.lineStyle === "dashed" || selectedEdge.data?.lineStyle === "dotted" ? selectedEdge.data.lineStyle : "solid",
1635
- onChange: (event) => patchSelectedEdge({
1636
- lineStyle: event.target.value
1644
+ onValueChange: (v) => patchSelectedEdge({
1645
+ lineStyle: v
1637
1646
  }),
1638
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1639
- children: [
1640
- /* @__PURE__ */ jsx("option", { value: "solid", children: "Solid" }),
1641
- /* @__PURE__ */ jsx("option", { value: "dashed", children: "Dashed" }),
1642
- /* @__PURE__ */ jsx("option", { value: "dotted", children: "Dotted" })
1647
+ options: [
1648
+ { value: "solid", label: "Solid" },
1649
+ { value: "dashed", label: "Dashed" },
1650
+ { value: "dotted", label: "Dotted" }
1643
1651
  ]
1644
1652
  }
1645
1653
  )
@@ -1648,38 +1656,40 @@ const DiagramFlow = ({
1648
1656
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1649
1657
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1650
1658
  "Direction",
1651
- /* @__PURE__ */ jsxs(
1652
- "select",
1659
+ /* @__PURE__ */ jsx(
1660
+ Select,
1653
1661
  {
1662
+ "aria-label": "Direction",
1663
+ className: "mt-1 w-full",
1654
1664
  value: String(selectedEdge.data?.marker ?? "end"),
1655
- onChange: (event) => patchSelectedEdge({
1656
- marker: event.target.value
1665
+ onValueChange: (v) => patchSelectedEdge({
1666
+ marker: v
1657
1667
  }),
1658
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1659
- children: [
1660
- /* @__PURE__ */ jsx("option", { value: "end", children: "Forward" }),
1661
- /* @__PURE__ */ jsx("option", { value: "start", children: "Backward" }),
1662
- /* @__PURE__ */ jsx("option", { value: "both", children: "Both ends" }),
1663
- /* @__PURE__ */ jsx("option", { value: "none", children: "No arrowheads" })
1668
+ options: [
1669
+ { value: "end", label: "Forward" },
1670
+ { value: "start", label: "Backward" },
1671
+ { value: "both", label: "Both ends" },
1672
+ { value: "none", label: "No arrowheads" }
1664
1673
  ]
1665
1674
  }
1666
1675
  )
1667
1676
  ] }),
1668
1677
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1669
1678
  "Weight",
1670
- /* @__PURE__ */ jsxs(
1671
- "select",
1679
+ /* @__PURE__ */ jsx(
1680
+ Select,
1672
1681
  {
1682
+ "aria-label": "Weight",
1683
+ className: "mt-1 w-full",
1673
1684
  value: String(selectedEdge.data?.strokeWidth ?? 2),
1674
- onChange: (event) => patchSelectedEdge({
1675
- strokeWidth: Number(event.target.value)
1685
+ onValueChange: (v) => patchSelectedEdge({
1686
+ strokeWidth: Number(v)
1676
1687
  }),
1677
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1678
- children: [
1679
- /* @__PURE__ */ jsx("option", { value: "1", children: "Fine" }),
1680
- /* @__PURE__ */ jsx("option", { value: "2", children: "Regular" }),
1681
- /* @__PURE__ */ jsx("option", { value: "3", children: "Bold" }),
1682
- /* @__PURE__ */ jsx("option", { value: "4", children: "Heavy" })
1688
+ options: [
1689
+ { value: "1", label: "Fine" },
1690
+ { value: "2", label: "Regular" },
1691
+ { value: "3", label: "Bold" },
1692
+ { value: "4", label: "Heavy" }
1683
1693
  ]
1684
1694
  }
1685
1695
  )
@@ -1730,37 +1740,39 @@ const DiagramFlow = ({
1730
1740
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1731
1741
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1732
1742
  "Background",
1733
- /* @__PURE__ */ jsxs(
1734
- "select",
1743
+ /* @__PURE__ */ jsx(
1744
+ Select,
1735
1745
  {
1746
+ "aria-label": "Background",
1747
+ className: "mt-1 w-full",
1736
1748
  value: diagram.renderHints?.background ?? "dots",
1737
- onChange: (event) => chooseBackground(
1738
- event.target.value === "lines" ? BackgroundVariant.Lines : event.target.value === "cross" ? BackgroundVariant.Cross : BackgroundVariant.Dots
1749
+ onValueChange: (v) => chooseBackground(
1750
+ v === "lines" ? BackgroundVariant.Lines : v === "cross" ? BackgroundVariant.Cross : BackgroundVariant.Dots
1739
1751
  ),
1740
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1741
- children: [
1742
- /* @__PURE__ */ jsx("option", { value: "dots", children: "Dots" }),
1743
- /* @__PURE__ */ jsx("option", { value: "lines", children: "Lines" }),
1744
- /* @__PURE__ */ jsx("option", { value: "cross", children: "Crosses" })
1752
+ options: [
1753
+ { value: "dots", label: "Dots" },
1754
+ { value: "lines", label: "Lines" },
1755
+ { value: "cross", label: "Crosses" }
1745
1756
  ]
1746
1757
  }
1747
1758
  )
1748
1759
  ] }),
1749
1760
  /* @__PURE__ */ jsxs("label", { className: "block text-[11px] font-medium text-foreground", children: [
1750
1761
  "Flow",
1751
- /* @__PURE__ */ jsxs(
1752
- "select",
1762
+ /* @__PURE__ */ jsx(
1763
+ Select,
1753
1764
  {
1765
+ "aria-label": "Flow",
1766
+ className: "mt-1 w-full",
1754
1767
  value: diagram.layout?.direction ?? "TB",
1755
- onChange: (event) => patchLayout({
1756
- direction: event.target.value
1768
+ onValueChange: (v) => patchLayout({
1769
+ direction: v
1757
1770
  }),
1758
- className: "mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none focus:border-primary",
1759
- children: [
1760
- /* @__PURE__ */ jsx("option", { value: "TB", children: "Top to bottom" }),
1761
- /* @__PURE__ */ jsx("option", { value: "LR", children: "Left to right" }),
1762
- /* @__PURE__ */ jsx("option", { value: "BT", children: "Bottom to top" }),
1763
- /* @__PURE__ */ jsx("option", { value: "RL", children: "Right to left" })
1771
+ options: [
1772
+ { value: "TB", label: "Top to bottom" },
1773
+ { value: "LR", label: "Left to right" },
1774
+ { value: "BT", label: "Bottom to top" },
1775
+ { value: "RL", label: "Right to left" }
1764
1776
  ]
1765
1777
  }
1766
1778
  )
@@ -10,6 +10,7 @@ import { collectKindProblems } from "../../../kinds/react/kind-problems.js";
10
10
  import { readEnvelope } from "../../../kinds/redux/render-block-envelope.js";
11
11
  import { reportKindComponentIncident } from "../../../host/kind-incident.js";
12
12
  import { KindFixItBar } from "../../../host/app-bindings.js";
13
+ import { useInsideKindRenderHarness } from "../../../kinds/react/render-harness.js";
13
14
  import { StructuredValueTabs } from "./StructuredValueTabs.js";
14
15
  const StillArriving = /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
15
16
  /* @__PURE__ */ jsx(Braces, { className: "h-3.5 w-3.5 shrink-0 animate-pulse" }),
@@ -18,14 +19,15 @@ const StillArriving = /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-
18
19
  function useGenericFloorAlarm(metadata) {
19
20
  const kind = readEnvelope(metadata)?.root.kind ?? null;
20
21
  const reason = readIrRouteMarker(metadata)?.reason ?? null;
22
+ const insideHarness = useInsideKindRenderHarness();
21
23
  useEffect(() => {
22
- if (!kind) return;
24
+ if (!kind || insideHarness) return;
23
25
  reportKindComponentIncident({
24
26
  kind,
25
27
  errorType: "generic_floor_render",
26
28
  message: `"${kind}" rendered through the GENERIC structured viewer` + (reason === "inactive" ? " \u2014 a component row exists but is held is_active=false." : " \u2014 no component is bound, so the reader sees a key/value dump.")
27
29
  });
28
- }, [kind, reason]);
30
+ }, [kind, reason, insideHarness]);
29
31
  }
30
32
  const GenericStructuredBlock = (props) => {
31
33
  useGenericFloorAlarm(props.metadata);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { durableRecordId } from "@ai-matrx/kit/ids";
4
- import React, { useCallback } from "react";
4
+ import React, { useCallback, useRef } from "react";
5
5
  import { RichContentHostGate } from "../../../host/RichContentHostGate.js";
6
6
  import { BlockComponents, LoadingComponents } from "./BlockComponentRegistry.js";
7
7
  import { resolveArtifactDef } from "../../../host/app-bindings.js";
@@ -53,6 +53,7 @@ import {
53
53
  KindRecordChrome,
54
54
  kindHasRecordChrome
55
55
  } from "../../../host/app-bindings.js";
56
+ import { advanceRatchet, EMPTY_RATCHET } from "./render-ratchet.js";
56
57
  import {
57
58
  isBlockLoading,
58
59
  resolveBlockDispatch,
@@ -292,11 +293,19 @@ const BlockRendererGated = ({
292
293
  const envelopeKind = readEnvelope(terminalBlock.metadata)?.root.kind ?? announcedKind ?? null;
293
294
  const kindRouteVersion = useContentIrKindVersion(envelopeKind);
294
295
  useEnsureKindRenderable(envelopeKind);
295
- const decision = decideBlockRender(inputBlock, {
296
+ const frameDecision = decideBlockRender(inputBlock, {
296
297
  isStreamActive,
297
298
  suppressLoadingGate,
298
299
  route: (b) => routeBlockAtRegistryVersion(b, kindRouteVersion)
299
300
  });
301
+ const ratchetRef = useRef(EMPTY_RATCHET);
302
+ const settledBlock = !isStreamActive || inputBlock.isStreamingBlock === false;
303
+ const ratcheted = suppressLoadingGate ? { state: ratchetRef.current, decision: frameDecision } : advanceRatchet(ratchetRef.current, frameDecision, {
304
+ settled: settledBlock,
305
+ now: Date.now()
306
+ });
307
+ ratchetRef.current = ratcheted.state;
308
+ const decision = ratcheted.decision;
300
309
  const block = decision.block;
301
310
  const interruptedEnvelope = readEnvelope(block.metadata);
302
311
  const hasInterruptedKind = Boolean(
@@ -0,0 +1,76 @@
1
+ /**
2
+ * THE RENDER RATCHET — a streaming block's render only moves FORWARD.
3
+ *
4
+ * Arman, 2026-10-14: "Going forwards is ok, but once you go forward, you do not go back
5
+ * unless we're ABSOLUTELY CONFIDENT something has gone wrong and we have multiple pieces of
6
+ * evidence." The stages, in order:
7
+ *
8
+ * 0 TEXT — prose, or a raw code/JSON card
9
+ * 1 LOADER — a JSON region that could be (or is) a kind, still arriving
10
+ * 2 GENERIC — the structured floor drew it
11
+ * 3 COMPONENT — the kind's own component (routed, provisional, artifact, dispatch)
12
+ *
13
+ * `decideBlockRender` is a pure function of ONE frame, and a single frame of a token stream
14
+ * is often not valid: a bridge declines a half-arrived item, a nested value is mid-string, a
15
+ * component row is a beat late, a parser has not seen the closing brace. Each of those used
16
+ * to drop the block a stage (component → loader → component), and because each stage is a
17
+ * different element, React unmounted the component and mounted it again — the "re-render"
18
+ * the reader sees. The ratchet sits on top of that decision at the ONE place every block is
19
+ * drawn (`BlockRenderer`): a frame that would move the block BACKWARD keeps the last good
20
+ * decision on screen (its block, its props) and waits for the next frame.
21
+ *
22
+ * It lets go only on strong evidence:
23
+ * - the block SETTLED (its own `isStreamingBlock === false`, or the message stream ended):
24
+ * the final decision is the truth, whatever stage it is;
25
+ * - the content stopped CONTINUING the held block (the slot now holds a different block):
26
+ * the ratchet starts over for the new one;
27
+ * - mid-stream, the frames have disagreed for at least RATCHET_RELEASE_MIN_FRAMES distinct
28
+ * content updates AND RATCHET_RELEASE_MIN_MS of wall time, both — a region that really
29
+ * turned into something else keeps saying so, a transient hiccup never lasts that long.
30
+ *
31
+ * Pure: `advanceRatchet` takes the previous state and returns the next; the hook that keeps
32
+ * the state per mounted block lives in BlockRenderer.
33
+ */
34
+ import type { BlockRenderDecision } from "./BlockRenderer.js";
35
+ /**
36
+ * Distinct content updates a backward verdict must survive before it is believed mid-stream.
37
+ * A token stream sends roughly one update per animation frame; transient invalid states
38
+ * (mid-string, mid-key, a declined half item) last a handful. 120 is ~2 s of steady tokens.
39
+ */
40
+ export declare const RATCHET_RELEASE_MIN_FRAMES = 120;
41
+ /**
42
+ * Wall time a backward verdict must persist mid-stream, AND-ed with the frame count, so a
43
+ * burst of fast chunks cannot meet the bar in a blink and a stalled stream cannot meet it on
44
+ * time alone. Generous on purpose: the stream's end is the normal release.
45
+ */
46
+ export declare const RATCHET_RELEASE_MIN_MS = 8000;
47
+ export type RenderStage = 0 | 1 | 2 | 3;
48
+ export declare const RENDER_STAGE_NAMES: readonly ["text", "loader", "generic", "component"];
49
+ /** Where a single frame's decision sits on the forward path. */
50
+ export declare function renderStageOf(decision: BlockRenderDecision): RenderStage;
51
+ export interface RatchetState {
52
+ /** The decision on screen. */
53
+ held: BlockRenderDecision | null;
54
+ heldStage: RenderStage;
55
+ /** The held block's content — the continuation check reads it. */
56
+ heldContent: string;
57
+ /** The last content seen (frames are counted on content CHANGE, so a re-render is not a frame). */
58
+ lastContent: string;
59
+ /** Consecutive distinct frames that wanted to go backward, and since when. */
60
+ disagreeFrames: number;
61
+ disagreeSince: number | null;
62
+ }
63
+ export declare const EMPTY_RATCHET: RatchetState;
64
+ export interface RatchetInput {
65
+ /** The block settled: its own stream closed, or the message stream ended. */
66
+ settled: boolean;
67
+ /** Wall clock (ms). */
68
+ now: number;
69
+ }
70
+ /**
71
+ * One frame through the ratchet: the state to keep and the decision to draw.
72
+ */
73
+ export declare function advanceRatchet(state: RatchetState, next: BlockRenderDecision, { settled, now }: RatchetInput): {
74
+ state: RatchetState;
75
+ decision: BlockRenderDecision;
76
+ };
@@ -0,0 +1,73 @@
1
+ import { GENERIC_STRUCTURED_COMPONENT_KEY } from "../../../kinds/react/kind-route.js";
2
+ const RATCHET_RELEASE_MIN_FRAMES = 120;
3
+ const RATCHET_RELEASE_MIN_MS = 8e3;
4
+ const CONTINUATION_PREFIX = 48;
5
+ const RENDER_STAGE_NAMES = ["text", "loader", "generic", "component"];
6
+ const TEXT_TYPES = /* @__PURE__ */ new Set(["text", "code"]);
7
+ function renderStageOf(decision) {
8
+ if (decision.gate?.kind === "provisional") return 3;
9
+ if (decision.gate?.kind === "loader") return 1;
10
+ const type = String(decision.block.type);
11
+ if (TEXT_TYPES.has(type)) return 0;
12
+ if (type === GENERIC_STRUCTURED_COMPONENT_KEY) return 2;
13
+ return 3;
14
+ }
15
+ const EMPTY_RATCHET = {
16
+ held: null,
17
+ heldStage: 0,
18
+ heldContent: "",
19
+ lastContent: "",
20
+ disagreeFrames: 0,
21
+ disagreeSince: null
22
+ };
23
+ function contentOf(decision) {
24
+ const content = decision.rawBlock.content;
25
+ return typeof content === "string" ? content : "";
26
+ }
27
+ function continues(heldContent, next) {
28
+ const prefix = heldContent.slice(0, CONTINUATION_PREFIX).trimEnd();
29
+ if (!prefix) return true;
30
+ return next.startsWith(prefix) || prefix.startsWith(next.slice(0, CONTINUATION_PREFIX).trimEnd());
31
+ }
32
+ function accept(next, stage, content) {
33
+ return {
34
+ held: next,
35
+ heldStage: stage,
36
+ heldContent: content,
37
+ lastContent: content,
38
+ disagreeFrames: 0,
39
+ disagreeSince: null
40
+ };
41
+ }
42
+ function advanceRatchet(state, next, { settled, now }) {
43
+ const stage = renderStageOf(next);
44
+ const content = contentOf(next);
45
+ const take = () => {
46
+ const accepted = accept(next, stage, content);
47
+ return { state: accepted, decision: next };
48
+ };
49
+ if (!state.held || settled) return take();
50
+ if (!content.trim() && state.heldContent.trim()) {
51
+ return { state: { ...state, lastContent: content }, decision: state.held };
52
+ }
53
+ if (!continues(state.heldContent, content)) return take();
54
+ if (stage >= state.heldStage) return take();
55
+ const isNewFrame = content !== state.lastContent;
56
+ const disagreeFrames = state.disagreeFrames + (isNewFrame ? 1 : 0);
57
+ const disagreeSince = state.disagreeSince ?? now;
58
+ if (disagreeFrames >= RATCHET_RELEASE_MIN_FRAMES && now - disagreeSince >= RATCHET_RELEASE_MIN_MS) {
59
+ return take();
60
+ }
61
+ return {
62
+ state: { ...state, lastContent: content, disagreeFrames, disagreeSince },
63
+ decision: state.held
64
+ };
65
+ }
66
+ export {
67
+ EMPTY_RATCHET,
68
+ RATCHET_RELEASE_MIN_FRAMES,
69
+ RATCHET_RELEASE_MIN_MS,
70
+ RENDER_STAGE_NAMES,
71
+ advanceRatchet,
72
+ renderStageOf
73
+ };
@@ -96,6 +96,10 @@ function withFenceSurface(metadata, language, content, envelopes) {
96
96
  const envelope = envelopes.fenceRegion(language, content);
97
97
  return envelope ? { ...metadata ?? {}, [IR_ENVELOPE_KEY]: envelope } : metadata;
98
98
  }
99
+ const XML_TAG = /<\/?[A-Za-z][\w:.-]*(?:\s[^<>]*)?\/?>/g;
100
+ function isBareXmlTagPiece(content) {
101
+ return /</.test(content) && !content.replace(XML_TAG, "").trim();
102
+ }
99
103
  const wrapperSources = /* @__PURE__ */ new WeakMap();
100
104
  function wrapperSourceOf(block) {
101
105
  return wrapperSources.get(block);
@@ -166,6 +170,7 @@ function recoverEmbeddedKindJsonBlocksWith(input, envelopes) {
166
170
  );
167
171
  followsKind = false;
168
172
  if (!content) continue;
173
+ if (genericXml && isBareXmlTagPiece(content)) continue;
169
174
  recovered.push({
170
175
  ...block,
171
176
  content,
@@ -11,6 +11,12 @@ export interface RichContentKindRegistry {
11
11
  getSchema(kind: string): KindSchema | undefined;
12
12
  getEmittedJsonSchema(kind: string): unknown;
13
13
  getDeclaredLoadingComponent(kind: string): string | null;
14
+ /**
15
+ * Has this kind's schema been read yet? "pending" until the read settles. Optional: a host
16
+ * without it is treated as always settled. The loading-slug answer reads it so a cold
17
+ * registry says "checking" instead of a wrong "generic" (one measured answer, 2026-10-14).
18
+ */
19
+ schemaState?(kind: string): "loaded" | "pending" | "absent" | "failed";
14
20
  isKnownKind(kind: string): boolean;
15
21
  isRenderableKind(kind: string): boolean;
16
22
  listDefinitions(): KindDefinition[];
@@ -1,6 +1,6 @@
1
1
  import { type KindLoadingSlug } from "./kind-loading-slugs.js";
2
2
  /** Where the answer came from — surfaced so the Stream tab / doctor can say. */
3
- export type LoadingSlugOrigin = "declared" | "derived" | "generic";
3
+ export type LoadingSlugOrigin = "declared" | "derived" | "generic" | "pending";
4
4
  export interface LoadingSlugResolution {
5
5
  /** The library slug, or null when nothing beat the generic skeleton. */
6
6
  slug: KindLoadingSlug | null;
@@ -43,6 +43,9 @@ function resolveLoadingSlugForKind(kind) {
43
43
  if (derived) {
44
44
  return invalidDeclared === void 0 ? { slug: derived, origin: "derived" } : { slug: derived, origin: "derived", invalidDeclared };
45
45
  }
46
+ if (invalidDeclared === void 0 && kindRegistry.schemaState?.(kind) === "pending") {
47
+ return { slug: null, origin: "pending" };
48
+ }
46
49
  return invalidDeclared === void 0 ? GENERIC : { slug: null, origin: "generic", invalidDeclared };
47
50
  }
48
51
  export {
@@ -0,0 +1,4 @@
1
+ import { type LoadingSlugResolution } from "./resolve-loading-slug.js";
2
+ export declare function useLoadingSlugForKind(kind: string | null | undefined): LoadingSlugResolution;
3
+ /** The version is an ARGUMENT so no compiler pass can memoise the answer past an arrival. */
4
+ export declare function readLoadingSlugAtVersion(kind: string | null | undefined, version: number): LoadingSlugResolution;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { useContentIrKindVersion } from "../use-registry-repaint.js";
3
+ import { useEnsureKindRenderable } from "../ensure-kind-renderable.js";
4
+ import { resolveLoadingSlugForKind } from "./resolve-loading-slug.js";
5
+ function useLoadingSlugForKind(kind) {
6
+ const version = useContentIrKindVersion(kind ?? null);
7
+ useEnsureKindRenderable(kind ?? null);
8
+ return readLoadingSlugAtVersion(kind, version);
9
+ }
10
+ function readLoadingSlugAtVersion(kind, version) {
11
+ void version;
12
+ return resolveLoadingSlugForKind(kind);
13
+ }
14
+ export {
15
+ readLoadingSlugAtVersion,
16
+ useLoadingSlugForKind
17
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * RENDER HARNESSES ARE NOT READERS (defect 2026-10-14).
3
+ *
4
+ * A harness — the kind-registry Preview, the Shape Studio — renders every path
5
+ * on purpose: streaming frames, partial payloads, a component landing after
6
+ * first paint. A block that reaches the generic floor INSIDE one is the harness
7
+ * doing its job, not a reader being let down, so it must not file a
8
+ * "a reader got the generic view" sighting. Scoped to the harness's own
9
+ * subtree: a block rendered anywhere else on the same page (a chat dock, a
10
+ * second surface) still files.
11
+ */
12
+ import React from "react";
13
+ export declare function KindRenderHarness({ children }: {
14
+ children: React.ReactNode;
15
+ }): React.JSX.Element;
16
+ /** True when this component renders inside a {@link KindRenderHarness}. */
17
+ export declare function useInsideKindRenderHarness(): boolean;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ const KindRenderHarnessContext = createContext(false);
5
+ function KindRenderHarness({ children }) {
6
+ return /* @__PURE__ */ jsx(KindRenderHarnessContext.Provider, { value: true, children });
7
+ }
8
+ function useInsideKindRenderHarness() {
9
+ return useContext(KindRenderHarnessContext);
10
+ }
11
+ export {
12
+ KindRenderHarness,
13
+ useInsideKindRenderHarness
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-content",
3
- "version": "0.2.160",
3
+ "version": "0.2.162",
4
4
  "description": "The AI Matrx formatted-text engine: the one markdown renderer, streaming markdown, the block splitter and renderer, the generic blocks, <RichContent> levels, the rich-document action registry and menus, copy, and the selection toolbar — as portable React.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -130,21 +130,21 @@
130
130
  "unist-util-visit": "^5.1.0",
131
131
  "unist-util-visit-parents": "^6.0.2",
132
132
  "yaml": "^2.9.1",
133
- "@ai-matrx/canvas": "latest",
133
+ "@ai-matrx/alchemy": "latest",
134
134
  "@ai-matrx/api": "latest",
135
+ "@ai-matrx/canvas": "latest",
135
136
  "@ai-matrx/content-ir": "latest",
136
- "@ai-matrx/agents": "latest",
137
- "@ai-matrx/data": "latest",
138
137
  "@ai-matrx/content-ir-react": "latest",
139
- "@ai-matrx/alchemy": "latest",
138
+ "@ai-matrx/data": "latest",
140
139
  "@ai-matrx/design-system": "latest",
141
140
  "@ai-matrx/icons": "latest",
142
- "@ai-matrx/diff": "latest",
143
- "@ai-matrx/kit": "latest",
141
+ "@ai-matrx/agents": "latest",
144
142
  "@ai-matrx/media": "latest",
145
143
  "@ai-matrx/print": "latest",
146
144
  "@ai-matrx/records": "latest",
147
- "@ai-matrx/records-ui": "latest"
145
+ "@ai-matrx/records-ui": "latest",
146
+ "@ai-matrx/kit": "latest",
147
+ "@ai-matrx/diff": "latest"
148
148
  },
149
149
  "peerDependencies": {
150
150
  "react": ">=19.0.0",
@@ -205,5 +205,5 @@
205
205
  "verify:modules-tarball": "node ./scripts/verify-modules-tarball.mjs",
206
206
  "verify:tarball": "node ./scripts/verify-tarball.mjs"
207
207
  },
208
- "gitHead": "4d6a83481c2d4de4a5a1561f666fc8eba07e2617"
208
+ "gitHead": "3260b81ccf1721d1a6a0ba08b238fdfa7d609ba5"
209
209
  }