@ai-matrx/rich-content 0.2.163 → 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 +15 -0
- package/dist/display/chat-markdown/block-registry/BlockRenderer.d.ts +6 -0
- package/dist/display/chat-markdown/block-registry/BlockRenderer.js +57 -8
- package/dist/display/chat-markdown/block-registry/render-ratchet.d.ts +20 -0
- package/dist/display/chat-markdown/block-registry/render-ratchet.js +17 -6
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
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
|
+
|
|
3
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
|
+
|
|
18
|
+
## 0.2.162
|
|
4
19
|
### Unreleased
|
|
5
20
|
|
|
6
21
|
- **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.
|
|
@@ -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,
|
|
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 {
|
|
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,15 +309,53 @@ const BlockRendererGated = ({
|
|
|
298
309
|
suppressLoadingGate,
|
|
299
310
|
route: (b) => routeBlockAtRegistryVersion(b, kindRouteVersion)
|
|
300
311
|
});
|
|
301
|
-
const
|
|
312
|
+
const [ratchetState, setRatchetState] = useState(EMPTY_RATCHET);
|
|
302
313
|
const settledBlock = !isStreamActive || inputBlock.isStreamingBlock === false;
|
|
303
|
-
const ratcheted = suppressLoadingGate ? { state:
|
|
314
|
+
const ratcheted = suppressLoadingGate ? { state: ratchetState, decision: frameDecision } : advanceRatchet(ratchetState, frameDecision, {
|
|
304
315
|
settled: settledBlock,
|
|
305
316
|
messageEnded: !isStreamActive,
|
|
306
|
-
now:
|
|
317
|
+
now: ratchetClock()
|
|
307
318
|
});
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}) => {
|
|
310
359
|
const block = decision.block;
|
|
311
360
|
const interruptedEnvelope = readEnvelope(block.metadata);
|
|
312
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,6 +61,12 @@ 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 {
|
|
@@ -79,3 +87,15 @@ export declare function advanceRatchet(state: RatchetState, next: BlockRenderDec
|
|
|
79
87
|
state: RatchetState;
|
|
80
88
|
decision: BlockRenderDecision;
|
|
81
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,
|
|
@@ -42,16 +44,17 @@ function accept(next, stage, content) {
|
|
|
42
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
51
|
if (state.held && !messageEnded && !content.trim() && state.heldContent.trim()) {
|
|
50
52
|
return { state: { ...state, lastContent: content }, decision: state.held };
|
|
51
53
|
}
|
|
52
54
|
if (!state.held || settled) return take();
|
|
53
|
-
if (!continues(state.heldContent, content)) 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, messageEnded = false }) {
|
|
|
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
|
-
|
|
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.
|
|
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,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/agents": "latest",
|
|
134
|
+
"@ai-matrx/alchemy": "latest",
|
|
133
135
|
"@ai-matrx/api": "latest",
|
|
134
136
|
"@ai-matrx/canvas": "latest",
|
|
135
137
|
"@ai-matrx/content-ir": "latest",
|
|
136
138
|
"@ai-matrx/content-ir-react": "latest",
|
|
137
|
-
"@ai-matrx/agents": "latest",
|
|
138
|
-
"@ai-matrx/alchemy": "latest",
|
|
139
139
|
"@ai-matrx/data": "latest",
|
|
140
140
|
"@ai-matrx/design-system": "latest",
|
|
141
|
-
"@ai-matrx/
|
|
141
|
+
"@ai-matrx/icons": "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/icons": "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": "
|
|
208
|
+
"gitHead": "1b4c21ab92d61a09b5d50abcdb456e52984b2b02"
|
|
209
209
|
}
|