@ai-matrx/rich-content 0.2.162 → 0.2.164

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.164
4
+
5
+ UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
+ Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/rich-content/src`.
7
+
8
+ ## 0.2.163
9
+
10
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
11
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
12
+ Diff: `git diff 3260b81ccf bd8cc634ac -- apps/shared/rich-content/src`
13
+
14
+ Changed shipped source files (2):
15
+ - `apps/shared/rich-content/src/display/chat-markdown/block-registry/BlockRenderer.tsx`
16
+ - `apps/shared/rich-content/src/display/chat-markdown/block-registry/render-ratchet.ts`
17
+
3
18
  ## 0.2.162
4
19
  ### Unreleased
5
20
 
@@ -82,6 +82,12 @@ export interface BlockRenderDecision {
82
82
  /** The routed block — what the dispatch registry receives. */
83
83
  block: RenderBlock;
84
84
  gate: BlockRenderGate;
85
+ /**
86
+ * CERTAIN evidence this block is not a kind: a JSON code region whose first key
87
+ * arrived complete and is not `__kind` (the first-key rule). The render ratchet
88
+ * releases a held loader on it at once; a transient frame never sets it.
89
+ */
90
+ notKind?: boolean;
85
91
  }
86
92
  /**
87
93
  * THE DECISION BlockRenderer makes, as a pure function (V5): terminal
@@ -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, useRef } from "react";
4
+ import React, { useCallback, useState } 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,7 +53,12 @@ import {
53
53
  KindRecordChrome,
54
54
  kindHasRecordChrome
55
55
  } from "../../../host/app-bindings.js";
56
- import { advanceRatchet, EMPTY_RATCHET } from "./render-ratchet.js";
56
+ import {
57
+ advanceRatchet,
58
+ EMPTY_RATCHET,
59
+ ratchetClock,
60
+ sameRatchetState
61
+ } from "./render-ratchet.js";
57
62
  import {
58
63
  isBlockLoading,
59
64
  resolveBlockDispatch,
@@ -251,7 +256,13 @@ function decideBlockRender(inputBlock, {
251
256
  }
252
257
  }
253
258
  }
254
- return decided(null);
259
+ return isCertainlyNotKind(block) ? { ...decided(null), notKind: true } : decided(null);
260
+ }
261
+ function isCertainlyNotKind(block) {
262
+ if (block.type !== "code") return false;
263
+ const text = typeof block.content === "string" ? block.content : "";
264
+ if (!/^\s*[[{]/.test(withoutLeadingJsonComments(text))) return false;
265
+ return jsonKindSignal(text, { json5: isJson5Language(block.language) }) === "not_kind";
255
266
  }
256
267
  const PendingStructuredBlock = ({
257
268
  envelope
@@ -298,14 +309,53 @@ const BlockRendererGated = ({
298
309
  suppressLoadingGate,
299
310
  route: (b) => routeBlockAtRegistryVersion(b, kindRouteVersion)
300
311
  });
301
- const ratchetRef = useRef(EMPTY_RATCHET);
312
+ const [ratchetState, setRatchetState] = useState(EMPTY_RATCHET);
302
313
  const settledBlock = !isStreamActive || inputBlock.isStreamingBlock === false;
303
- const ratcheted = suppressLoadingGate ? { state: ratchetRef.current, decision: frameDecision } : advanceRatchet(ratchetRef.current, frameDecision, {
314
+ const ratcheted = suppressLoadingGate ? { state: ratchetState, decision: frameDecision } : advanceRatchet(ratchetState, frameDecision, {
304
315
  settled: settledBlock,
305
- now: Date.now()
316
+ messageEnded: !isStreamActive,
317
+ now: ratchetClock()
306
318
  });
307
- ratchetRef.current = ratcheted.state;
308
- const decision = ratcheted.decision;
319
+ if (!sameRatchetState(ratcheted.state, ratchetState)) setRatchetState(ratcheted.state);
320
+ return /* @__PURE__ */ jsx(
321
+ BlockRendererDrawn,
322
+ {
323
+ requestId,
324
+ index,
325
+ isStreamActive,
326
+ onContentChange,
327
+ conversationId,
328
+ messageId,
329
+ taskId,
330
+ isLastReasoningBlock,
331
+ replaceBlockContent,
332
+ handleOpenEditor,
333
+ suppressLoadingGate,
334
+ outputSchema,
335
+ decision: ratcheted.decision,
336
+ envelopeKind,
337
+ durableMessageId
338
+ },
339
+ ratcheted.state.generation
340
+ );
341
+ };
342
+ const BlockRendererDrawn = ({
343
+ requestId,
344
+ index,
345
+ isStreamActive,
346
+ onContentChange,
347
+ conversationId,
348
+ messageId,
349
+ taskId,
350
+ isLastReasoningBlock,
351
+ replaceBlockContent,
352
+ handleOpenEditor,
353
+ suppressLoadingGate = false,
354
+ outputSchema = null,
355
+ decision,
356
+ envelopeKind,
357
+ durableMessageId
358
+ }) => {
309
359
  const block = decision.block;
310
360
  const interruptedEnvelope = readEnvelope(block.metadata);
311
361
  const hasInterruptedKind = Boolean(
@@ -24,6 +24,8 @@
24
24
  * the final decision is the truth, whatever stage it is;
25
25
  * - the content stopped CONTINUING the held block (the slot now holds a different block):
26
26
  * the ratchet starts over for the new one;
27
+ * - the frame carries CERTAIN evidence the held loader was wrong: the region's first key
28
+ * arrived complete and is not `__kind` (`decision.notKind`) — loader → text at once;
27
29
  * - mid-stream, the frames have disagreed for at least RATCHET_RELEASE_MIN_FRAMES distinct
28
30
  * content updates AND RATCHET_RELEASE_MIN_MS of wall time, both — a region that really
29
31
  * turned into something else keeps saying so, a transient hiccup never lasts that long.
@@ -59,18 +61,41 @@ export interface RatchetState {
59
61
  /** Consecutive distinct frames that wanted to go backward, and since when. */
60
62
  disagreeFrames: number;
61
63
  disagreeSince: number | null;
64
+ /**
65
+ * Which logical block this slot holds. Bumped when a DIFFERENT block takes the slot
66
+ * mid-stream (the content stopped continuing the held one): the renderer keys its
67
+ * subtree on it, so a new block never inherits the previous block's component state.
68
+ */
69
+ generation: number;
62
70
  }
63
71
  export declare const EMPTY_RATCHET: RatchetState;
64
72
  export interface RatchetInput {
65
73
  /** The block settled: its own stream closed, or the message stream ended. */
66
74
  settled: boolean;
75
+ /**
76
+ * The MESSAGE stream is over. An emptied block while the message still streams is a
77
+ * retraction or a re-split, never a final value — even when its frame says "complete".
78
+ */
79
+ messageEnded?: boolean;
67
80
  /** Wall clock (ms). */
68
81
  now: number;
69
82
  }
70
83
  /**
71
84
  * One frame through the ratchet: the state to keep and the decision to draw.
72
85
  */
73
- export declare function advanceRatchet(state: RatchetState, next: BlockRenderDecision, { settled, now }: RatchetInput): {
86
+ export declare function advanceRatchet(state: RatchetState, next: BlockRenderDecision, { settled, now, messageEnded }: RatchetInput): {
74
87
  state: RatchetState;
75
88
  decision: BlockRenderDecision;
76
89
  };
90
+ /**
91
+ * Two states that draw the same thing and will judge the next frame the same way. The
92
+ * renderer keeps the state in React state and stores a new one only when this is false,
93
+ * so re-rendering the same frame converges instead of looping.
94
+ */
95
+ export declare function sameRatchetState(a: RatchetState, b: RatchetState): boolean;
96
+ /**
97
+ * The ratchet's wall clock. Only the mid-stream release reads it (a backward verdict must
98
+ * persist RATCHET_RELEASE_MIN_MS); a re-render of the same frame never counts a frame, so
99
+ * reading it during render cannot change what is drawn.
100
+ */
101
+ export declare function ratchetClock(): number;
@@ -18,7 +18,8 @@ const EMPTY_RATCHET = {
18
18
  heldContent: "",
19
19
  lastContent: "",
20
20
  disagreeFrames: 0,
21
- disagreeSince: null
21
+ disagreeSince: null,
22
+ generation: 0
22
23
  };
23
24
  function contentOf(decision) {
24
25
  const content = decision.rawBlock.content;
@@ -29,8 +30,9 @@ function continues(heldContent, next) {
29
30
  if (!prefix) return true;
30
31
  return next.startsWith(prefix) || prefix.startsWith(next.slice(0, CONTINUATION_PREFIX).trimEnd());
31
32
  }
32
- function accept(next, stage, content) {
33
+ function accept(next, stage, content, generation) {
33
34
  return {
35
+ generation,
34
36
  held: next,
35
37
  heldStage: stage,
36
38
  heldContent: content,
@@ -39,19 +41,20 @@ function accept(next, stage, content) {
39
41
  disagreeSince: null
40
42
  };
41
43
  }
42
- function advanceRatchet(state, next, { settled, now }) {
44
+ function advanceRatchet(state, next, { settled, now, messageEnded = false }) {
43
45
  const stage = renderStageOf(next);
44
46
  const content = contentOf(next);
45
- const take = () => {
46
- const accepted = accept(next, stage, content);
47
+ const take = (generation = state.generation) => {
48
+ const accepted = accept(next, stage, content, generation);
47
49
  return { state: accepted, decision: next };
48
50
  };
49
- if (!state.held || settled) return take();
50
- if (!content.trim() && state.heldContent.trim()) {
51
+ if (state.held && !messageEnded && !content.trim() && state.heldContent.trim()) {
51
52
  return { state: { ...state, lastContent: content }, decision: state.held };
52
53
  }
53
- if (!continues(state.heldContent, content)) return take();
54
+ if (!state.held || settled) return take();
55
+ if (!continues(state.heldContent, content)) return take(state.generation + 1);
54
56
  if (stage >= state.heldStage) return take();
57
+ if (state.heldStage === 1 && stage === 0 && next.notKind === true) return take();
55
58
  const isNewFrame = content !== state.lastContent;
56
59
  const disagreeFrames = state.disagreeFrames + (isNewFrame ? 1 : 0);
57
60
  const disagreeSince = state.disagreeSince ?? now;
@@ -63,11 +66,19 @@ function advanceRatchet(state, next, { settled, now }) {
63
66
  decision: state.held
64
67
  };
65
68
  }
69
+ function sameRatchetState(a, b) {
70
+ return a.heldStage === b.heldStage && a.heldContent === b.heldContent && a.lastContent === b.lastContent && a.disagreeFrames === b.disagreeFrames && a.disagreeSince === b.disagreeSince && a.generation === b.generation && a.held === null === (b.held === null);
71
+ }
72
+ function ratchetClock() {
73
+ return Date.now();
74
+ }
66
75
  export {
67
76
  EMPTY_RATCHET,
68
77
  RATCHET_RELEASE_MIN_FRAMES,
69
78
  RATCHET_RELEASE_MIN_MS,
70
79
  RENDER_STAGE_NAMES,
71
80
  advanceRatchet,
72
- renderStageOf
81
+ ratchetClock,
82
+ renderStageOf,
83
+ sameRatchetState
73
84
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-content",
3
- "version": "0.2.162",
3
+ "version": "0.2.164",
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,6 +130,7 @@
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/agents": "latest",
133
134
  "@ai-matrx/alchemy": "latest",
134
135
  "@ai-matrx/api": "latest",
135
136
  "@ai-matrx/canvas": "latest",
@@ -138,13 +139,12 @@
138
139
  "@ai-matrx/data": "latest",
139
140
  "@ai-matrx/design-system": "latest",
140
141
  "@ai-matrx/icons": "latest",
141
- "@ai-matrx/agents": "latest",
142
+ "@ai-matrx/kit": "latest",
142
143
  "@ai-matrx/media": "latest",
143
144
  "@ai-matrx/print": "latest",
145
+ "@ai-matrx/diff": "latest",
144
146
  "@ai-matrx/records": "latest",
145
- "@ai-matrx/records-ui": "latest",
146
- "@ai-matrx/kit": "latest",
147
- "@ai-matrx/diff": "latest"
147
+ "@ai-matrx/records-ui": "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": "3260b81ccf1721d1a6a0ba08b238fdfa7d609ba5"
208
+ "gitHead": "1b4c21ab92d61a09b5d50abcdb456e52984b2b02"
209
209
  }