@ai-matrx/chat 0.7.0 → 0.7.2
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 +14 -0
- package/dist/agents/components/conversation-history/ConversationHistorySidebar.js +1 -0
- package/dist/agents/redux/execution-system/instance-resources/default-remark-durability.d.ts +8 -2
- package/dist/agents/redux/execution-system/instance-resources/default-remark-durability.js +61 -18
- package/dist/agents/redux/execution-system/instance-resources/remark-feed.d.ts +26 -0
- package/dist/agents/redux/execution-system/instance-resources/remark-feed.js +87 -0
- package/dist/agents/redux/execution-system/instance-resources/remarks.d.ts +10 -0
- package/dist/agents/redux/execution-system/instance-resources/remarks.js +11 -3
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.d.ts +65 -335
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.js +224 -1909
- package/dist/host/react.js +5 -3
- package/dist/window-panels/windows/agents/AgentRunWindow.js +29 -16
- package/package.json +46 -6
- package/dist/agents/redux/execution-system/utils/content-prefilter.d.ts +0 -50
- package/dist/agents/redux/execution-system/utils/content-prefilter.js +0 -243
|
@@ -1,1572 +1,168 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { QuotedKindLift } from "@ai-matrx/content-ir/surfaces";
|
|
3
|
-
import { canonicalizeCompletedLegacyQuizEnvelope } from "@ai-matrx/content-ir/surfaces";
|
|
4
|
-
import { envelopeMatchesParsedSource } from "@ai-matrx/content-ir/surfaces";
|
|
5
|
-
import { MarkdownEscapedKindJson } from "@ai-matrx/content-ir/surfaces";
|
|
6
|
-
import { KindImageAltUnwrap } from "@ai-matrx/content-ir/surfaces";
|
|
7
|
-
import { FENCE_META_KEY, splitFenceInfo } from "@ai-matrx/content-ir/source";
|
|
8
|
-
import {
|
|
9
|
-
hasUnclosedBacktickRun,
|
|
10
|
-
indexOutsideInlineCode
|
|
11
|
-
} from "@ai-matrx/content-ir/source";
|
|
12
|
-
import {
|
|
13
|
-
hasKindKey,
|
|
14
|
-
isJsonFenceLanguage,
|
|
15
|
-
jsonKindSignal,
|
|
16
|
-
kindGrammar,
|
|
17
|
-
kindObjectProseBreak,
|
|
18
|
-
bareRegionBreaksIntoProse
|
|
19
|
-
} from "@ai-matrx/content-ir/surfaces";
|
|
20
|
-
import {
|
|
21
|
-
findBalancedXmlClose,
|
|
22
|
-
initialXmlBalance
|
|
23
|
-
} from "@ai-matrx/content-ir/source";
|
|
24
|
-
import {
|
|
25
|
-
classifyInnerFenceLine,
|
|
26
|
-
fenceNestsInnerFences,
|
|
27
|
-
fenceOpenerOf,
|
|
28
|
-
FenceReader,
|
|
29
|
-
findCodeRanges,
|
|
30
|
-
isHtmlBlockTagName,
|
|
31
|
-
trimFenceLine
|
|
32
|
-
} from "@ai-matrx/content-ir/source";
|
|
1
|
+
import { BlockSplitter } from "@ai-matrx/rich-content/display/markdown-classification/processors/utils/block-splitter";
|
|
33
2
|
import {
|
|
34
|
-
|
|
35
|
-
DirectiveContainerTracker
|
|
36
|
-
} from "@ai-matrx/content-ir/source";
|
|
37
|
-
import { TITLED_IMAGE_LINE } from "@ai-matrx/content-ir/source";
|
|
38
|
-
import { continuesTable, lineIndent, startsPipelessTable } from "@ai-matrx/content-ir/source";
|
|
39
|
-
import {
|
|
40
|
-
classifyLine,
|
|
41
|
-
isPlainText,
|
|
42
|
-
hasCandidate,
|
|
43
|
-
Candidate
|
|
44
|
-
} from "./content-prefilter.js";
|
|
45
|
-
import {
|
|
46
|
-
detectJsonBlockType,
|
|
47
|
-
parseXmlAttributes,
|
|
48
|
-
extractAudioLink,
|
|
49
|
-
detectImageMarkdown,
|
|
50
|
-
countInlineImages,
|
|
51
|
-
detectVideoMarkdown,
|
|
52
|
-
detectMatrxFileMarkdown,
|
|
53
|
-
isCompleteUnrecognizedXmlContainer,
|
|
54
|
-
isUnclosedGenericXmlOpening,
|
|
55
|
-
startUnrecognizedXmlContainer,
|
|
56
|
-
normalizeCodeLanguage,
|
|
57
|
-
SPECIAL_CODE_LANGUAGES
|
|
3
|
+
clientSplitterEnvelopes
|
|
58
4
|
} from "@ai-matrx/rich-content/display/markdown-classification/processors/utils/content-splitter-v2";
|
|
5
|
+
import { canonicalizeCompletedLegacyQuizEnvelope } from "@ai-matrx/content-ir/surfaces";
|
|
59
6
|
import { kindRegistry } from "@ai-matrx/rich-content/host/kind-registry";
|
|
60
7
|
import { componentRegistry } from "@ai-matrx/rich-content/host/component-registry";
|
|
61
|
-
import { hostSplitterEnvelopes } from "@ai-matrx/rich-content/host/splitter-envelopes";
|
|
62
|
-
import { withIrEnvelope } from "@ai-matrx/rich-content/kinds/registry/region-envelope-memo";
|
|
63
8
|
import { sessionEnvelope } from "@ai-matrx/rich-content/kinds/registry/kind-correctors";
|
|
64
9
|
import { disposeParseSession, openParseSession } from "@ai-matrx/content-ir";
|
|
65
|
-
import {
|
|
66
|
-
IR_ENVELOPE_KEY
|
|
67
|
-
} from "@ai-matrx/content-ir";
|
|
68
|
-
import {
|
|
69
|
-
normalizeRecoveredContainerPiece,
|
|
70
|
-
splitAroundEmbeddedKindJson
|
|
71
|
-
} from "@ai-matrx/content-ir/surfaces";
|
|
10
|
+
import { IR_ENVELOPE_KEY } from "@ai-matrx/content-ir";
|
|
72
11
|
import { captureError } from "../../../../host/diagnostics.js";
|
|
73
|
-
const isSpecialCodeLanguage = (language) => SPECIAL_CODE_LANGUAGES.includes(language);
|
|
74
|
-
const envelopeForCompletedXmlRegion = (tag, regionText) => hostSplitterEnvelopes()?.xmlRegion(tag, regionText) ?? null;
|
|
75
|
-
const envelopeForCompletedFenceRegion = (language, regionText) => hostSplitterEnvelopes()?.fenceRegion(language, regionText) ?? null;
|
|
76
|
-
function countStructuralObjectBraces(source) {
|
|
77
|
-
let opens = 0;
|
|
78
|
-
let closes = 0;
|
|
79
|
-
let inString = false;
|
|
80
|
-
let escaped = false;
|
|
81
|
-
for (const char of source) {
|
|
82
|
-
if (inString) {
|
|
83
|
-
if (escaped) escaped = false;
|
|
84
|
-
else if (char === "\\") escaped = true;
|
|
85
|
-
else if (char === '"') inString = false;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (char === '"') inString = true;
|
|
89
|
-
else if (char === "{") opens++;
|
|
90
|
-
else if (char === "}") closes++;
|
|
91
|
-
}
|
|
92
|
-
return { opens, closes };
|
|
93
|
-
}
|
|
94
|
-
function firstCompleteRootObjectEnd(source, initialDepth = 0) {
|
|
95
|
-
let depth = initialDepth;
|
|
96
|
-
let sawRoot = initialDepth > 0;
|
|
97
|
-
let inString = false;
|
|
98
|
-
let escaped = false;
|
|
99
|
-
for (let index = 0; index < source.length; index++) {
|
|
100
|
-
const char = source[index];
|
|
101
|
-
if (inString) {
|
|
102
|
-
if (escaped) escaped = false;
|
|
103
|
-
else if (char === "\\") escaped = true;
|
|
104
|
-
else if (char === '"') inString = false;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (char === '"') {
|
|
108
|
-
inString = true;
|
|
109
|
-
} else if (char === "{") {
|
|
110
|
-
depth++;
|
|
111
|
-
sawRoot = true;
|
|
112
|
-
} else if (char === "}") {
|
|
113
|
-
depth--;
|
|
114
|
-
if (sawRoot && depth === 0) return index + 1;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
const ORPHAN_REASONING_CLOSE = /<\/(thinking|think|reasoning)\s*>/i;
|
|
120
|
-
const ORPHAN_REASONING_CLOSE_GLOBAL = /<\/(thinking|think|reasoning)\s*>/gi;
|
|
121
|
-
const REASONING_OPEN_TAG = /<(thinking|think|reasoning)(\s[^>]*)?>/i;
|
|
122
12
|
const REASONING_BLOCK_TYPES = /* @__PURE__ */ new Set(["thinking", "reasoning"]);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"event",
|
|
133
|
-
"tool",
|
|
134
|
-
"questionnaire",
|
|
135
|
-
"flashcards",
|
|
136
|
-
"cooking_recipe",
|
|
137
|
-
"timeline",
|
|
138
|
-
"progress_tracker",
|
|
139
|
-
"troubleshooting",
|
|
140
|
-
"resources",
|
|
141
|
-
"research"
|
|
142
|
-
]);
|
|
143
|
-
const ATTR_XML_TAGS = /* @__PURE__ */ new Set([
|
|
144
|
-
"decision",
|
|
145
|
-
"artifact",
|
|
146
|
-
// Editor pills — round-trip representation of code-editor errors and selected
|
|
147
|
-
// code snippets. Attributes carry file/line/severity/language; the body
|
|
148
|
-
// carries `<message>`+`<surrounding_code>` for errors, raw code for snippets.
|
|
149
|
-
// Must mirror ATTRIBUTE_XML_BLOCKS in content-splitter-v2.ts.
|
|
150
|
-
"editor_error",
|
|
151
|
-
"editor_code_snippet"
|
|
152
|
-
]);
|
|
153
|
-
function isParseableJsonObject(text) {
|
|
154
|
-
try {
|
|
155
|
-
const parsed = JSON.parse(text);
|
|
156
|
-
return parsed !== null && typeof parsed === "object";
|
|
157
|
-
} catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function endsInsideXmlLiteral(source) {
|
|
162
|
-
const lastComment = source.lastIndexOf("<!--");
|
|
163
|
-
if (lastComment !== -1 && source.indexOf("-->", lastComment + 4) === -1) return true;
|
|
164
|
-
const lastCdata = source.lastIndexOf("<![CDATA[");
|
|
165
|
-
if (lastCdata !== -1 && source.indexOf("]]>", lastCdata + 9) === -1) return true;
|
|
166
|
-
const last = findCodeRanges(source).at(-1);
|
|
167
|
-
if (!last || last.kind !== "fence") return false;
|
|
168
|
-
const closer = /\n[ \t]*(`{3,}|~{3,})[ \t]*$/.test(source.slice(last.start, last.end));
|
|
169
|
-
return last.end >= source.length && !closer;
|
|
170
|
-
}
|
|
171
|
-
function genericCandidateBody(candidate) {
|
|
172
|
-
return (candidate.fence ? candidate.lines.slice(1) : candidate.lines).join("\n");
|
|
173
|
-
}
|
|
174
|
-
function isFenceCloserLine(line, fence) {
|
|
175
|
-
const trimmed = trimFenceLine(line);
|
|
176
|
-
return trimmed.length >= fence.ticks && [...trimmed].every((ch) => ch === fence.char);
|
|
177
|
-
}
|
|
178
|
-
const BARE_JSON_OPEN_RE = /^\{\s*"[^"]*"\s*:/;
|
|
179
|
-
const regionBreaksIntoProse = bareRegionBreaksIntoProse;
|
|
180
|
-
const LITERAL_KIND_KEY_AT = /(?<!\\)"(?:__kind|\\u005[fF]_kind)"\s*:/;
|
|
181
|
-
function ownsKindKey(line, at) {
|
|
182
|
-
const key = line.slice(at).search(LITERAL_KIND_KEY_AT);
|
|
183
|
-
if (key < 0) return true;
|
|
184
|
-
const keyAt = at + key;
|
|
185
|
-
const verdict = kindGrammar(line, "json", at);
|
|
186
|
-
if (verdict.status === "prefix") return true;
|
|
187
|
-
if (verdict.status === "complete") return verdict.end > keyAt;
|
|
188
|
-
return verdict.at > keyAt;
|
|
189
|
-
}
|
|
190
|
-
function proseKindObjectStart(line) {
|
|
191
|
-
if (!hasKindKey(line)) return -1;
|
|
192
|
-
for (let at = indexOutsideInlineCode(line, "{", 1); at > 0; at = indexOutsideInlineCode(line, "{", at + 1)) {
|
|
193
|
-
const rest = line.slice(at);
|
|
194
|
-
if (BARE_JSON_OPEN_RE.test(rest) && hasKindKey(rest) && ownsKindKey(line, at)) {
|
|
195
|
-
return line.slice(0, at).trim() ? at : -1;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return -1;
|
|
199
|
-
}
|
|
200
|
-
const HTML_PHRASING_TAGS = /* @__PURE__ */ new Set([
|
|
201
|
-
"a",
|
|
202
|
-
"abbr",
|
|
203
|
-
"b",
|
|
204
|
-
"bdi",
|
|
205
|
-
"bdo",
|
|
206
|
-
"big",
|
|
207
|
-
"br",
|
|
208
|
-
"cite",
|
|
209
|
-
"code",
|
|
210
|
-
"data",
|
|
211
|
-
"del",
|
|
212
|
-
"dfn",
|
|
213
|
-
"em",
|
|
214
|
-
"font",
|
|
215
|
-
"i",
|
|
216
|
-
"img",
|
|
217
|
-
"ins",
|
|
218
|
-
"kbd",
|
|
219
|
-
"mark",
|
|
220
|
-
"q",
|
|
221
|
-
"s",
|
|
222
|
-
"samp",
|
|
223
|
-
"small",
|
|
224
|
-
"span",
|
|
225
|
-
"strike",
|
|
226
|
-
"strong",
|
|
227
|
-
"sub",
|
|
228
|
-
"sup",
|
|
229
|
-
"time",
|
|
230
|
-
"tt",
|
|
231
|
-
"u",
|
|
232
|
-
"var",
|
|
233
|
-
"wbr"
|
|
234
|
-
]);
|
|
235
|
-
function startsStructuralLine(line) {
|
|
236
|
-
if (/^\s*(?:\{|\[\s*\{|\||`{3,}|~{3,}|:::)/.test(line)) return true;
|
|
237
|
-
if (/^\s*<!--/.test(line)) return false;
|
|
238
|
-
const tag = /^\s*<\/?([A-Za-z][\w-]*)/.exec(line);
|
|
239
|
-
if (!tag) return /^\s*</.test(line);
|
|
240
|
-
const name = tag[1].toLowerCase();
|
|
241
|
-
return !isHtmlBlockTagName(name) && !HTML_PHRASING_TAGS.has(name);
|
|
242
|
-
}
|
|
243
|
-
function extractFenceInfo(trimmed) {
|
|
244
|
-
const char = trimmed[0];
|
|
245
|
-
if (char !== "`" && char !== "~") return null;
|
|
246
|
-
let ticks = 0;
|
|
247
|
-
while (ticks < trimmed.length && trimmed[ticks] === char) ticks++;
|
|
248
|
-
if (ticks < 3) return null;
|
|
249
|
-
const { language, meta } = splitFenceInfo(trimmed.slice(ticks));
|
|
250
|
-
return { language: language ?? "", meta, ticks, char };
|
|
251
|
-
}
|
|
252
|
-
function extractOpeningXmlTag(trimmed) {
|
|
253
|
-
if (trimmed[0] !== "<") return null;
|
|
254
|
-
let i = 1;
|
|
255
|
-
while (i < trimmed.length) {
|
|
256
|
-
const c = trimmed.charCodeAt(i);
|
|
257
|
-
if (c >= 97 && c <= 122 || c >= 65 && c <= 90 || c >= 48 && c <= 57 || c === 95) {
|
|
258
|
-
i++;
|
|
259
|
-
} else {
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (i === 1) return null;
|
|
264
|
-
const tag = trimmed.substring(1, i).toLowerCase();
|
|
265
|
-
if (SIMPLE_XML_TAGS.has(tag) || ATTR_XML_TAGS.has(tag)) return tag;
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
function mapXmlTagToBlockType(tag) {
|
|
269
|
-
if (tag === "think") return "thinking";
|
|
270
|
-
return tag;
|
|
271
|
-
}
|
|
272
|
-
function extractOpeningTagText(trimmed, tag) {
|
|
273
|
-
const close = trimmed.indexOf(">");
|
|
274
|
-
if (close === -1) return `<${tag}>`;
|
|
275
|
-
return trimmed.slice(0, close + 1);
|
|
276
|
-
}
|
|
277
|
-
function findMidLineAttrXml(rawLine) {
|
|
278
|
-
for (const tag of ATTR_XML_TAGS) {
|
|
279
|
-
const prefix = `<${tag}`;
|
|
280
|
-
const idx = indexOutsideInlineCode(rawLine, prefix);
|
|
281
|
-
if (idx === -1) continue;
|
|
282
|
-
if (rawLine.trimStart().startsWith(prefix)) continue;
|
|
283
|
-
const after = rawLine[idx + prefix.length];
|
|
284
|
-
if (after !== " " && after !== ">") continue;
|
|
285
|
-
const close = rawLine.indexOf(">", idx);
|
|
286
|
-
if (close === -1) continue;
|
|
287
|
-
const openingTagText = rawLine.slice(idx, close + 1);
|
|
288
|
-
return {
|
|
289
|
-
tagStart: idx,
|
|
290
|
-
tag,
|
|
291
|
-
openingTagText,
|
|
292
|
-
attributes: parseXmlAttributes(openingTagText)
|
|
293
|
-
};
|
|
294
|
-
}
|
|
13
|
+
const MEDIA_BLOCK_TYPES = /* @__PURE__ */ new Set(["image", "video", "audio"]);
|
|
14
|
+
const FRONT_MATTER_OPEN = /^(?:)?(---|\+\+\+)[ \t]*\r?\n/;
|
|
15
|
+
function jsonRegionSource(block) {
|
|
16
|
+
if (block.type === "matrx" || block.type === "matrx_file") return null;
|
|
17
|
+
const body = block.content.trimStart();
|
|
18
|
+
if (!body.startsWith("{")) return null;
|
|
19
|
+
const language = (block.language ?? "").toLowerCase();
|
|
20
|
+
if (block.type === "code" && (language === "json" || language === "")) return block.content;
|
|
21
|
+
if (block.type === "artifact") return block.content;
|
|
295
22
|
return null;
|
|
296
23
|
}
|
|
297
|
-
function parseDecisionOptions(blockSourceText, decisionId) {
|
|
298
|
-
const options = parseDecisionOptionsFromBody(blockSourceText).map(
|
|
299
|
-
(o) => ({ id: o.id, label: o.label, text: o.text })
|
|
300
|
-
);
|
|
301
|
-
return options;
|
|
302
|
-
}
|
|
303
24
|
class StreamBlockAccumulator {
|
|
304
25
|
requestId;
|
|
305
|
-
currentBlockIndex = 0;
|
|
306
|
-
currentBlockType = "text";
|
|
307
|
-
currentBlockContent = "";
|
|
308
|
-
/**
|
|
309
|
-
* Number of lines appended to the current block. Used to preserve LEADING
|
|
310
|
-
* blank lines: the first appended line is written verbatim (even if it's an
|
|
311
|
-
* empty string), and every subsequent line is joined with "\n". Without this
|
|
312
|
-
* counter, appending "" to an empty `currentBlockContent` was a no-op, which
|
|
313
|
-
* silently swallowed a block's first blank line and produced a 1-byte drift
|
|
314
|
-
* vs the V2 splitter / Python server (which keep that blank line).
|
|
315
|
-
*/
|
|
316
|
-
currentBlockLineCount = 0;
|
|
317
|
-
pendingLineFragment = "";
|
|
318
|
-
/**
|
|
319
|
-
* True once the block at the current index has been dispatched to Redux —
|
|
320
|
-
* including speculative streaming projections of `pendingLineFragment`
|
|
321
|
-
* (see `emitCurrentBlock`). Reset whenever a new block is opened. Used by
|
|
322
|
-
* `closeCurrentBlock` to retract a speculative block whose line turned out
|
|
323
|
-
* to be a different typed block (see the bare-JSON duplication note there).
|
|
324
|
-
*/
|
|
325
|
-
currentBlockEmitted = false;
|
|
326
|
-
/**
|
|
327
|
-
* THE DATA KEEPS ITS KINDS. A JSON wrapper that holds kinds is drawn as its
|
|
328
|
-
* kinds plus a residual (A7) — the residual is the wrapper's data WITH ITS
|
|
329
|
-
* KINDS REMOVED, so it is a display projection, never the answer. Every
|
|
330
|
-
* such split remembers the bytes it was cut from, keyed by its first block
|
|
331
|
-
* index, so data capture (`deriveAnswerDataText`) reads what the model wrote.
|
|
332
|
-
* Live only; a rewind forgets every split at or past its boundary.
|
|
333
|
-
*/
|
|
334
|
-
wrapperSplits = /* @__PURE__ */ new Map();
|
|
335
|
-
subState = { kind: "none" };
|
|
336
|
-
/**
|
|
337
|
-
* URL + label for the current `audio` block. The accumulator's single-line
|
|
338
|
-
* media blocks (image/video) leave `data` null, which is why the legacy
|
|
339
|
-
* client image/video markdown path never populated `src`. Audio MUST carry
|
|
340
|
-
* its URL on `data.src` or BlockRenderer's `audio` case renders nothing —
|
|
341
|
-
* so we stash the extracted link here and emit it via buildBlockData.
|
|
342
|
-
*/
|
|
343
|
-
pendingMediaData = null;
|
|
344
|
-
ingestCount = 0;
|
|
345
|
-
emitCount = 0;
|
|
346
|
-
// ── content-ir delegation (ALWAYS ON — no env flag, per repo rule) ────────
|
|
347
|
-
/** Live kind-parser session for the OPEN JSON region (fence or bare). */
|
|
348
|
-
irSession = null;
|
|
349
|
-
/** Lines fed to the session for this region (drives the "\n" separators). */
|
|
350
|
-
irRegionLineCount = 0;
|
|
351
|
-
/** How many chars of the current pendingLineFragment were already fed. */
|
|
352
|
-
irFedFragmentLen = 0;
|
|
353
|
-
/** Final envelope of the region being closed — consumed by the complete emit. */
|
|
354
|
-
irEnvelope = null;
|
|
355
|
-
/**
|
|
356
|
-
* True only between seeing a LITERAL closing fence line and the block's
|
|
357
|
-
* emit (set in the code_fence close branch). Stream-death finalize never
|
|
358
|
-
* sets it, so the fence-surface convergence hook fires exclusively on
|
|
359
|
-
* genuinely completed fence regions.
|
|
360
|
-
*/
|
|
361
|
-
fenceClosedCleanly = false;
|
|
362
|
-
/**
|
|
363
|
-
* True once a simple-XML region's LITERAL closing tag has been seen — the twin
|
|
364
|
-
* of `fenceClosedCleanly`. Never set by stream-death `finalize()`, so a
|
|
365
|
-
* truncated region keeps today's legacy rendering (no envelope).
|
|
366
|
-
*
|
|
367
|
-
* MUST NOT be re-derived by scanning `currentBlockContent` for the closing
|
|
368
|
-
* tag: the accumulator STRIPS the literal tag text out of a simple-XML
|
|
369
|
-
* block's content (so `</reasoning>` can't leak into visible chat), which
|
|
370
|
-
* silently killed the XML keystone once already. The state machine already
|
|
371
|
-
* knows when the region closed; ask it, don't re-parse its output.
|
|
372
|
-
*/
|
|
373
|
-
xmlClosedCleanly = false;
|
|
374
|
-
/**
|
|
375
|
-
* True while the OPEN ir region is the JSON BODY of an attribute-XML block
|
|
376
|
-
* (`<artifact …>{"__kind":…}</artifact>`) rather than a block that IS the
|
|
377
|
-
* JSON region. Two consequences, both required:
|
|
378
|
-
*
|
|
379
|
-
* - the parity check is skipped — it compares the envelope against
|
|
380
|
-
* `JSON.parse(currentBlockContent)`, and an attr-XML block's content is
|
|
381
|
-
* the tag chrome PLUS the body, which never parses as JSON. Without this
|
|
382
|
-
* every wrapped payload would fire a false "stream parity" alarm.
|
|
383
|
-
* - the fragment feed stays off the closing-tag line (see `ingest`).
|
|
384
|
-
*/
|
|
385
|
-
irRegionIsXmlBody = false;
|
|
386
|
-
/** Every session identity opened during this stream (disposed at finalize). */
|
|
387
|
-
irIdentities = [];
|
|
388
|
-
/**
|
|
389
|
-
* True only for the closeCurrentBlock call that closes the FIRST HALF of a
|
|
390
|
-
* region split by a tool boundary (breakTextBlock keeps the region open in
|
|
391
|
-
* a fresh block). Metadata built during that close must not report
|
|
392
|
-
* `isComplete: true` — the region genuinely continues.
|
|
393
|
-
*/
|
|
394
|
-
regionContinuesAfterClose = false;
|
|
395
26
|
upsertAction;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* The unused text slot opened after a clean container close is omitted.
|
|
410
|
-
*
|
|
411
|
-
* A completed bare JSON root opens a fresh text slot so subsequent source
|
|
412
|
-
* can continue in document order. If the stream ends there, emitting that
|
|
413
|
-
* slot creates a second, empty terminal block after the actual answer.
|
|
414
|
-
*/
|
|
415
|
-
suppressEmptyTrailingSlot = false;
|
|
416
|
-
/** Lifts JSON regions out of blockquotes before the line machine (V1). */
|
|
417
|
-
quoteLift = new QuotedKindLift();
|
|
418
|
-
/** Un-escapes a markdown-escaped kind (`"\_\_kind"`) before the quote lift (P8). */
|
|
419
|
-
kindUnescape = new MarkdownEscapedKindJson();
|
|
420
|
-
/** Drops the `` wrapper of a kind written as image alt text (P9). */
|
|
421
|
-
imageAlt = new KindImageAltUnwrap();
|
|
27
|
+
splitter = new BlockSplitter({ envelopes: clientSplitterEnvelopes() });
|
|
28
|
+
/** Absolute index of the current text run's first block (tool boundaries start a new run). */
|
|
29
|
+
base = 0;
|
|
30
|
+
/** How many blocks the current run has drawn (retraction bookkeeping). */
|
|
31
|
+
drawn = 0;
|
|
32
|
+
/** Last dispatched signature per absolute block index. */
|
|
33
|
+
signatures = /* @__PURE__ */ new Map();
|
|
34
|
+
region = null;
|
|
35
|
+
/** Final envelope of a region closed at a run's end (stream death or tool boundary). */
|
|
36
|
+
closedEnvelopes = /* @__PURE__ */ new Map();
|
|
37
|
+
identities = [];
|
|
38
|
+
unclosedReasoningReported = false;
|
|
422
39
|
constructor(requestId, upsertAction) {
|
|
423
40
|
this.requestId = requestId;
|
|
424
41
|
this.upsertAction = upsertAction;
|
|
425
42
|
}
|
|
426
43
|
// ── Public API ──────────────────────────────────────────────────────────
|
|
427
|
-
/**
|
|
428
|
-
* Ingest a text delta (the rAF-batched textBuffer content).
|
|
429
|
-
* Processes only complete lines; the trailing fragment is held until the
|
|
430
|
-
* next call or finalize().
|
|
431
|
-
*/
|
|
44
|
+
/** Ingest a text delta (the rAF-batched textBuffer content). */
|
|
432
45
|
ingest(delta, dispatch) {
|
|
433
|
-
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
dispatch
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
/** Release what the quote lift still holds (stream end or a hard boundary). */
|
|
440
|
-
flushQuoteLift(dispatch) {
|
|
441
|
-
const unescaped = this.imageAlt.push(this.kindUnescape.flush()) + this.imageAlt.flush();
|
|
442
|
-
const held = (unescaped ? this.quoteLift.push(unescaped) : "") + this.quoteLift.flush();
|
|
443
|
-
if (held) this.ingestText(held, dispatch);
|
|
444
|
-
}
|
|
445
|
-
ingestText(text, dispatch) {
|
|
446
|
-
const combined = this.pendingLineFragment + text;
|
|
447
|
-
const parts = combined.split("\n");
|
|
448
|
-
this.pendingLineFragment = parts[parts.length - 1];
|
|
449
|
-
parts.length -= 1;
|
|
450
|
-
for (const rawLine of parts) {
|
|
451
|
-
this.processLine(rawLine, dispatch);
|
|
452
|
-
}
|
|
453
|
-
if (this.subState.kind === "xml_tag" && !this.subState.isAttrXml) {
|
|
454
|
-
const closeIdx = this.pendingLineFragment.indexOf(
|
|
455
|
-
this.subState.closingTag
|
|
456
|
-
);
|
|
457
|
-
if (closeIdx !== -1) {
|
|
458
|
-
const upto = closeIdx + this.subState.closingTag.length;
|
|
459
|
-
const rest = this.pendingLineFragment.slice(upto);
|
|
460
|
-
if (rest.trimStart().length > 0) {
|
|
461
|
-
const tagLine = this.pendingLineFragment.slice(0, upto);
|
|
462
|
-
this.pendingLineFragment = rest;
|
|
463
|
-
this.processLine(tagLine, dispatch);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
this.maybeSplitProseBeforeKindFragment(dispatch);
|
|
468
|
-
this.maybeEnterKindArrayFromFragment(dispatch);
|
|
469
|
-
this.maybeSplitKindFromXmlFragment(dispatch);
|
|
470
|
-
this.maybeSplitKindFromGenericXmlFragment(dispatch);
|
|
471
|
-
this.maybeOpenBareJsonFromFragment(dispatch);
|
|
472
|
-
while (this.subState.kind === "bare_json") {
|
|
473
|
-
const rootEnd = firstCompleteRootObjectEnd(
|
|
474
|
-
this.pendingLineFragment,
|
|
475
|
-
this.subState.openBraces - this.subState.closeBraces
|
|
476
|
-
);
|
|
477
|
-
if (rootEnd === null) break;
|
|
478
|
-
const remainder = this.pendingLineFragment.slice(rootEnd);
|
|
479
|
-
if (!remainder.trim()) break;
|
|
480
|
-
const completeRoot = this.pendingLineFragment.slice(0, rootEnd);
|
|
481
|
-
this.pendingLineFragment = remainder.trimStart();
|
|
482
|
-
this.processLine(completeRoot, dispatch);
|
|
483
|
-
this.maybeOpenBareJsonFromFragment(dispatch);
|
|
484
|
-
}
|
|
485
|
-
if (this.subState.kind === "bare_json" && (!this.currentBlockContent && kindObjectProseBreak(this.pendingLineFragment.trimStart()) !== null || regionBreaksIntoProse(
|
|
486
|
-
this.currentBlockContent ? `${this.currentBlockContent}
|
|
487
|
-
${this.pendingLineFragment}` : this.pendingLineFragment
|
|
488
|
-
))) {
|
|
489
|
-
this.demoteKindLineToProse();
|
|
490
|
-
}
|
|
491
|
-
if (!this.irSession) {
|
|
492
|
-
this.maybeOpenXmlBodyIrRegion(this.pendingLineFragment, false);
|
|
493
|
-
}
|
|
494
|
-
if (this.irSession && (this.subState.kind === "bare_json" || this.subState.kind === "code_fence" && !/^[`~]/.test(this.pendingLineFragment.trimStart()) || this.irRegionIsXmlBody && this.subState.kind === "xml_tag" && !this.pendingLineFragment.trimStart().startsWith("<")) && this.pendingLineFragment.length > this.irFedFragmentLen) {
|
|
495
|
-
this.irWriteLinePart(
|
|
496
|
-
this.pendingLineFragment.slice(this.irFedFragmentLen),
|
|
497
|
-
false
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
this.emitCurrentBlock(dispatch, "streaming");
|
|
46
|
+
if (!delta) return;
|
|
47
|
+
this.splitter.append(delta);
|
|
48
|
+
this.sync(dispatch, false);
|
|
501
49
|
}
|
|
502
|
-
/**
|
|
503
|
-
* Flush remaining content and mark the final block as complete.
|
|
504
|
-
* Called once after the stream loop ends.
|
|
505
|
-
*/
|
|
50
|
+
/** The stream ended: every block becomes complete. Called once after the stream loop. */
|
|
506
51
|
finalize(dispatch) {
|
|
507
|
-
this.
|
|
508
|
-
|
|
509
|
-
this.processLine(this.pendingLineFragment, dispatch);
|
|
510
|
-
this.pendingLineFragment = "";
|
|
511
|
-
}
|
|
512
|
-
this.releaseHeldArray(dispatch);
|
|
513
|
-
if (this.subState.kind === "xml_tag" && !this.subState.isAttrXml && REASONING_BLOCK_TYPES.has(mapXmlTagToBlockType(this.subState.tagName))) {
|
|
514
|
-
this.reportUnclosedReasoningRegion(this.subState.tagName);
|
|
515
|
-
}
|
|
516
|
-
if (this.subState.kind === "bare_json") {
|
|
517
|
-
const jsonType = detectJsonBlockType(this.currentBlockContent);
|
|
518
|
-
if (jsonType) {
|
|
519
|
-
this.currentBlockType = jsonType;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (this.irSession) {
|
|
523
|
-
this.irCloseRegion();
|
|
524
|
-
}
|
|
525
|
-
if (!this.emitRecoveredEmbeddedKindBlocks(dispatch)) {
|
|
526
|
-
this.emitCurrentBlock(dispatch, "complete");
|
|
527
|
-
}
|
|
528
|
-
this.irEnvelope = null;
|
|
529
|
-
this.irRegionIsXmlBody = false;
|
|
530
|
-
this.irDisposeAll();
|
|
52
|
+
this.endRun(dispatch);
|
|
53
|
+
this.disposeAll();
|
|
531
54
|
}
|
|
532
55
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* the local streaming state so the replacement provider attempt cannot
|
|
536
|
-
* concatenate onto or resurrect the failed attempt.
|
|
56
|
+
* The source bytes behind every live A7 wrapper split. The splitter's reading of the stored
|
|
57
|
+
* text is the reading every surface draws, so no live block stands for bytes it does not hold.
|
|
537
58
|
*/
|
|
538
|
-
/** The source bytes behind every live A7 wrapper split (see `wrapperSplits`). */
|
|
539
59
|
getWrapperSplitSources() {
|
|
540
|
-
return [
|
|
541
|
-
blockIds: split.indices.map((i) => `client_block_${i}`),
|
|
542
|
-
source: split.source
|
|
543
|
-
}));
|
|
60
|
+
return [];
|
|
544
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Forget every in-flight byte at and after a durable content boundary (a provider retry).
|
|
64
|
+
* Redux owns removal of the already-dispatched suffix; the replacement attempt starts a fresh
|
|
65
|
+
* run at `blockCount`.
|
|
66
|
+
*/
|
|
545
67
|
rewindToBlockCount(blockCount) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
this.
|
|
552
|
-
this.currentBlockIndex = Math.max(0, blockCount);
|
|
553
|
-
this.currentBlockType = "text";
|
|
554
|
-
this.currentBlockContent = "";
|
|
555
|
-
this.currentBlockLineCount = 0;
|
|
556
|
-
this.pendingLineFragment = "";
|
|
557
|
-
this.currentBlockEmitted = false;
|
|
558
|
-
this.subState = { kind: "none" };
|
|
559
|
-
this.pendingMediaData = null;
|
|
560
|
-
this.irSession = null;
|
|
561
|
-
this.irRegionLineCount = 0;
|
|
562
|
-
this.irFedFragmentLen = 0;
|
|
563
|
-
this.irEnvelope = null;
|
|
564
|
-
this.irRegionIsXmlBody = false;
|
|
565
|
-
this.fenceClosedCleanly = false;
|
|
566
|
-
this.xmlClosedCleanly = false;
|
|
567
|
-
this.regionContinuesAfterClose = false;
|
|
568
|
-
this.lineQueue = [];
|
|
569
|
-
this.isProcessingLineQueue = false;
|
|
570
|
-
this.genericXmlRecoverySuppressed = false;
|
|
571
|
-
this.suppressEmptyTrailingSlot = false;
|
|
572
|
-
this.quoteLift = new QuotedKindLift();
|
|
573
|
-
this.kindUnescape = new MarkdownEscapedKindJson();
|
|
574
|
-
this.imageAlt = new KindImageAltUnwrap();
|
|
68
|
+
this.disposeAll();
|
|
69
|
+
this.splitter = new BlockSplitter({ envelopes: clientSplitterEnvelopes() });
|
|
70
|
+
this.base = Math.max(0, blockCount);
|
|
71
|
+
this.drawn = 0;
|
|
72
|
+
for (const index of [...this.signatures.keys()]) if (index >= this.base) this.signatures.delete(index);
|
|
73
|
+
for (const index of [...this.closedEnvelopes.keys()]) if (index >= this.base) this.closedEnvelopes.delete(index);
|
|
575
74
|
}
|
|
576
75
|
/**
|
|
577
|
-
* Close the current text
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* Why this exists: the accumulator lives for the whole stream and is never
|
|
583
|
-
* otherwise told about tool calls. Without this break, "text → tool → text"
|
|
584
|
-
* collapses both runs into a single `client_block_N`. The timeline still
|
|
585
|
-
* records a `text_end` for the second run, but its
|
|
586
|
-
* `[blockStartIndex, blockEndIndex)` range comes out EMPTY (no new blockId
|
|
587
|
-
* was pushed), so `selectUnifiedSlots` emits `[merged_text, tool]` — the
|
|
588
|
-
* tool card renders AFTER all the text, destroying chronological order.
|
|
589
|
-
* (The persisted/DB path is unaffected because it rebuilds each run's text
|
|
590
|
-
* from `text_end.rawText`, which is why a reload renders correctly.)
|
|
591
|
-
*
|
|
592
|
-
* Call this once per tool event, BEFORE the tool's `appendTimeline` dispatch
|
|
593
|
-
* and AFTER the pre-tool text has been flushed into the accumulator
|
|
594
|
-
* (process-stream's `dispatchBatch()` does that flush). It is a no-op when
|
|
595
|
-
* there is nothing open to break (back-to-back tool events, a tool with no
|
|
596
|
-
* preceding text, a tool right after a media block), so it is always safe.
|
|
76
|
+
* Close the current text run at an interleaved-content boundary (a tool call between two text
|
|
77
|
+
* runs): the next text opens fresh blocks after this run's, so "text → tool → text" keeps its
|
|
78
|
+
* chronological order. Reload splits each run's stored text on its own, and so does this.
|
|
79
|
+
* A no-op when nothing is open.
|
|
597
80
|
*/
|
|
598
81
|
breakTextBlock(dispatch) {
|
|
599
|
-
this.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
this.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// provider knows where its thinking ended — so the one thing the client owes
|
|
633
|
-
// the reader is that the failure is never silent.
|
|
634
|
-
orphanReasoningCloseReported = false;
|
|
635
|
-
unclosedReasoningReported = false;
|
|
636
|
-
reportOrphanReasoningClose(tag, rawLine) {
|
|
637
|
-
if (this.orphanReasoningCloseReported) return;
|
|
638
|
-
this.orphanReasoningCloseReported = true;
|
|
639
|
-
captureError({
|
|
640
|
-
source: "reasoning-leak",
|
|
641
|
-
message: `A closing reasoning tag (${tag}) arrived with NO open reasoning region: the provider's <reasoning> wrapper is unbalanced, so this turn's chain-of-thought streamed to the reader as the answer. The tag itself was stripped from the visible content. Remedy: the wrapper is per-stream state in aidream providers/reasoning_stream_state.py \u2014 an unbalanced wrapper means a provider parser is sharing it across concurrent turns again.`,
|
|
642
|
-
requestId: this.requestId,
|
|
643
|
-
raw: { sample: rawLine.slice(0, 500) }
|
|
82
|
+
if (!this.splitter.text) return;
|
|
83
|
+
this.endRun(dispatch);
|
|
84
|
+
this.base += this.drawn;
|
|
85
|
+
this.drawn = 0;
|
|
86
|
+
this.splitter = new BlockSplitter({ envelopes: clientSplitterEnvelopes() });
|
|
87
|
+
}
|
|
88
|
+
// ── The driver ──────────────────────────────────────────────────────────
|
|
89
|
+
endRun(dispatch) {
|
|
90
|
+
if (!this.splitter.text) return;
|
|
91
|
+
const blocks = this.splitter.blocks();
|
|
92
|
+
const last = blocks[blocks.length - 1];
|
|
93
|
+
if (last && this.region && this.region.index === this.base + blocks.length - 1) {
|
|
94
|
+
this.feedRegion(last);
|
|
95
|
+
this.closeRegion();
|
|
96
|
+
}
|
|
97
|
+
this.sync(dispatch, true);
|
|
98
|
+
if (last && REASONING_BLOCK_TYPES.has(last.type) && last.metadata?.isComplete === false) {
|
|
99
|
+
this.reportUnclosedReasoningRegion(last.type, last.content);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
sync(dispatch, final) {
|
|
103
|
+
const text = this.splitter.text;
|
|
104
|
+
if (!final && this.base === 0 && FRONT_MATTER_OPEN.test(text) && this.frontMatterStillOpen(text)) return;
|
|
105
|
+
let blocks = this.splitter.blocks();
|
|
106
|
+
if (!final && blocks.length > 0 && this.holdsNascentJson(blocks[blocks.length - 1])) {
|
|
107
|
+
blocks = blocks.slice(0, -1);
|
|
108
|
+
}
|
|
109
|
+
const lastIndex = blocks.length - 1;
|
|
110
|
+
blocks.forEach((block, i) => {
|
|
111
|
+
const index = this.base + i;
|
|
112
|
+
const open = !final && i === lastIndex;
|
|
113
|
+
const metadata = this.metadataFor(block, index, open);
|
|
114
|
+
this.emit(dispatch, index, block, open ? "streaming" : "complete", metadata);
|
|
644
115
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
this.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
this.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
this.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
this.kindLineIsProse = false;
|
|
691
|
-
this.linesRead++;
|
|
692
|
-
this.appendToCurrentBlock(rawLine);
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const trimmed = rawLine.trim();
|
|
696
|
-
const isFirstLine = this.linesRead === 0;
|
|
697
|
-
this.linesRead++;
|
|
698
|
-
const fenceLine = rawLine.replace(/\r$/, "").replace(isFirstLine ? /^\uFEFF/ : /$^/, "");
|
|
699
|
-
if (this.subState.kind === "none" && isFirstLine && (fenceLine === "---" || fenceLine === "+++")) {
|
|
700
|
-
if (this.currentBlockType !== "text") {
|
|
701
|
-
this.closeCurrentBlock(dispatch);
|
|
702
|
-
this.openBlock("text", dispatch);
|
|
703
|
-
}
|
|
704
|
-
this.appendToCurrentBlock(rawLine);
|
|
705
|
-
this.subState = { kind: "frontmatter", fence: fenceLine };
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (this.subState.kind !== "none") {
|
|
709
|
-
this.processSubStateLine(rawLine, trimmed, dispatch);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
if (DIRECTIVE_CONTAINER_OPEN.test(rawLine)) {
|
|
713
|
-
if (this.currentBlockType !== "text") {
|
|
714
|
-
this.closeCurrentBlock(dispatch);
|
|
715
|
-
this.openBlock("text", dispatch);
|
|
716
|
-
}
|
|
717
|
-
this.appendToCurrentBlock(rawLine);
|
|
718
|
-
this.subState = { kind: "directive", tracker: new DirectiveContainerTracker(rawLine) };
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
const genericXml = startUnrecognizedXmlContainer(rawLine);
|
|
722
|
-
if (genericXml) {
|
|
723
|
-
this.closeCurrentBlock(dispatch);
|
|
724
|
-
this.openBlock("code", dispatch);
|
|
725
|
-
this.subState = { kind: "generic_xml", tracker: genericXml.tracker };
|
|
726
|
-
const source = rawLine.slice(genericXml.rootStart);
|
|
727
|
-
const rootEnd = genericXml.tracker.consumeLine(source);
|
|
728
|
-
if (rootEnd === null) {
|
|
729
|
-
this.appendToCurrentBlock(source);
|
|
730
|
-
} else {
|
|
731
|
-
this.appendToCurrentBlock(source.slice(0, rootEnd));
|
|
732
|
-
this.closeCurrentBlock(dispatch);
|
|
733
|
-
this.subState = { kind: "none" };
|
|
734
|
-
this.openBlock("text", dispatch);
|
|
735
|
-
const remainder = source.slice(rootEnd).trim();
|
|
736
|
-
if (remainder) this.processLine(remainder, dispatch);
|
|
737
|
-
else this.suppressEmptyTrailingSlot = true;
|
|
738
|
-
}
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const orphanClose = rawLine.match(ORPHAN_REASONING_CLOSE);
|
|
742
|
-
const selfOpened = orphanClose?.index !== void 0 && REASONING_OPEN_TAG.test(rawLine.slice(0, orphanClose.index));
|
|
743
|
-
if (orphanClose && !selfOpened) {
|
|
744
|
-
this.reportOrphanReasoningClose(orphanClose[0], rawLine);
|
|
745
|
-
const stripped = rawLine.replace(ORPHAN_REASONING_CLOSE_GLOBAL, "");
|
|
746
|
-
if (stripped.trim().length === 0) return;
|
|
747
|
-
this.processLine(stripped, dispatch);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
if (/^\[\s*(?:\{|$)/.test(trimmed)) {
|
|
751
|
-
const signal = jsonKindSignal(trimmed);
|
|
752
|
-
if (signal !== "not_kind") {
|
|
753
|
-
this.subState = { kind: "kind_array", held: [rawLine] };
|
|
754
|
-
if (signal === "kind") this.commitKindArray(dispatch);
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
if (!startsStructuralLine(rawLine)) {
|
|
759
|
-
const at = proseKindObjectStart(rawLine);
|
|
760
|
-
if (at > 0 && kindObjectProseBreak(rawLine.slice(at)) === null) {
|
|
761
|
-
const rest = rawLine.slice(at);
|
|
762
|
-
const rootEnd = firstCompleteRootObjectEnd(rest);
|
|
763
|
-
const parts = [rawLine.slice(0, at)];
|
|
764
|
-
if (rootEnd === null) parts.push(rest);
|
|
765
|
-
else {
|
|
766
|
-
parts.push(rest.slice(0, rootEnd));
|
|
767
|
-
const tail = rest.slice(rootEnd).replace(/^[ \t]+/, "");
|
|
768
|
-
if (tail.trim()) parts.push(tail);
|
|
769
|
-
}
|
|
770
|
-
this.lineQueue.unshift(...parts.map((part) => ({ rawLine: part, dispatch })));
|
|
771
|
-
this.linesRead--;
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
const flags = classifyLine(rawLine, trimmed);
|
|
776
|
-
if (this.currentBlockType === "text" && this.currentBlockLineCount > 0) {
|
|
777
|
-
const cut = this.currentBlockContent.lastIndexOf("\n");
|
|
778
|
-
const header = this.currentBlockContent.slice(cut + 1);
|
|
779
|
-
if (startsPipelessTable(header, rawLine)) {
|
|
780
|
-
this.currentBlockContent = cut >= 0 ? this.currentBlockContent.slice(0, cut) : "";
|
|
781
|
-
this.currentBlockLineCount -= 1;
|
|
782
|
-
this.closeCurrentBlock(dispatch);
|
|
783
|
-
this.openBlock("table", dispatch);
|
|
784
|
-
this.subState = { kind: "table", container: lineIndent(header) };
|
|
785
|
-
this.appendToCurrentBlock(header);
|
|
786
|
-
this.appendToCurrentBlock(rawLine);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
if (isPlainText(flags)) {
|
|
791
|
-
this.appendToCurrentBlock(rawLine);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
if (hasCandidate(flags, Candidate.CODE)) {
|
|
795
|
-
const fence = extractFenceInfo(trimmed);
|
|
796
|
-
if (fence) {
|
|
797
|
-
this.closeCurrentBlock(dispatch);
|
|
798
|
-
const normalizedLang = normalizeCodeLanguage(fence.language);
|
|
799
|
-
const blockType = normalizedLang && isSpecialCodeLanguage(normalizedLang) ? normalizedLang : "code";
|
|
800
|
-
this.openBlock(blockType, dispatch);
|
|
801
|
-
this.subState = {
|
|
802
|
-
kind: "code_fence",
|
|
803
|
-
language: fence.language,
|
|
804
|
-
meta: fence.meta,
|
|
805
|
-
fenceTicks: fence.ticks,
|
|
806
|
-
tildeReader: fence.char === "~" ? new FenceReader({
|
|
807
|
-
char: "~",
|
|
808
|
-
ticks: fence.ticks,
|
|
809
|
-
lang: fence.language
|
|
810
|
-
}) : null,
|
|
811
|
-
nestedFences: 0,
|
|
812
|
-
earlyTypeResolved: false,
|
|
813
|
-
resumeXml: this.nextBareJsonResumeXml ?? void 0
|
|
814
|
-
};
|
|
815
|
-
this.nextBareJsonResumeXml = null;
|
|
816
|
-
if (normalizedLang === "json") {
|
|
817
|
-
this.irOpenRegion();
|
|
818
|
-
}
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
if (hasCandidate(flags, Candidate.XML_SIMPLE) || hasCandidate(flags, Candidate.XML_ATTR)) {
|
|
823
|
-
const tag = extractOpeningXmlTag(trimmed);
|
|
824
|
-
if (tag) {
|
|
825
|
-
this.openXmlTagBlock(tag, rawLine, trimmed, dispatch);
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
{
|
|
830
|
-
const midLine = findMidLineAttrXml(rawLine);
|
|
831
|
-
if (midLine) {
|
|
832
|
-
const before = rawLine.slice(0, midLine.tagStart);
|
|
833
|
-
const fromTag = rawLine.slice(midLine.tagStart);
|
|
834
|
-
if (before.trimEnd()) {
|
|
835
|
-
this.appendToCurrentBlock(before.trimEnd());
|
|
836
|
-
}
|
|
837
|
-
this.closeCurrentBlock(dispatch);
|
|
838
|
-
const blockType = mapXmlTagToBlockType(midLine.tag);
|
|
839
|
-
this.openBlock(blockType, dispatch);
|
|
840
|
-
this.subState = {
|
|
841
|
-
kind: "xml_tag",
|
|
842
|
-
tagName: midLine.tag,
|
|
843
|
-
closingTag: `</${midLine.tag}>`,
|
|
844
|
-
openingTagText: midLine.openingTagText,
|
|
845
|
-
attributes: midLine.attributes,
|
|
846
|
-
isAttrXml: true,
|
|
847
|
-
balance: initialXmlBalance()
|
|
848
|
-
};
|
|
849
|
-
this.appendToCurrentBlock(fromTag);
|
|
850
|
-
if (fromTag.includes(`</${midLine.tag}>`)) {
|
|
851
|
-
this.closeCurrentBlock(dispatch);
|
|
852
|
-
this.subState = { kind: "none" };
|
|
853
|
-
this.openBlock("text", dispatch);
|
|
854
|
-
this.suppressEmptyTrailingSlot = true;
|
|
855
|
-
}
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
if (hasCandidate(flags, Candidate.TABLE) && this.currentBlockType !== "table") {
|
|
860
|
-
this.closeCurrentBlock(dispatch);
|
|
861
|
-
this.openBlock("table", dispatch);
|
|
862
|
-
this.subState = { kind: "table", container: lineIndent(rawLine) };
|
|
863
|
-
this.appendToCurrentBlock(rawLine);
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const image = hasCandidate(flags, Candidate.IMAGE) ? detectImageMarkdown(rawLine) : null;
|
|
867
|
-
if (image?.isImage && image.src && countInlineImages(rawLine) < 2 && !TITLED_IMAGE_LINE.test(rawLine)) {
|
|
868
|
-
this.closeCurrentBlock(dispatch);
|
|
869
|
-
this.openBlock("image", dispatch);
|
|
870
|
-
this.pendingMediaData = { src: image.src, alt: image.alt ?? "" };
|
|
871
|
-
this.appendToCurrentBlock(trimmed);
|
|
872
|
-
this.closeCurrentBlock(dispatch);
|
|
873
|
-
this.openBlock("text", dispatch);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const video = hasCandidate(flags, Candidate.VIDEO) ? detectVideoMarkdown(rawLine) : null;
|
|
877
|
-
if (video?.isVideo && video.src) {
|
|
878
|
-
this.closeCurrentBlock(dispatch);
|
|
879
|
-
this.openBlock("video", dispatch);
|
|
880
|
-
this.pendingMediaData = { src: video.src, alt: video.alt ?? "" };
|
|
881
|
-
this.appendToCurrentBlock(trimmed);
|
|
882
|
-
this.closeCurrentBlock(dispatch);
|
|
883
|
-
this.openBlock("text", dispatch);
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
if (hasCandidate(flags, Candidate.AUDIO)) {
|
|
887
|
-
const audio = extractAudioLink(rawLine);
|
|
888
|
-
if (audio) {
|
|
889
|
-
this.closeCurrentBlock(dispatch);
|
|
890
|
-
this.openBlock("audio", dispatch);
|
|
891
|
-
this.pendingMediaData = audio;
|
|
892
|
-
this.appendToCurrentBlock(rawLine);
|
|
893
|
-
this.closeCurrentBlock(dispatch);
|
|
894
|
-
this.openBlock("text", dispatch);
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
if (hasCandidate(flags, Candidate.MATRX_FILE) && detectMatrxFileMarkdown(rawLine).isMatrxFile) {
|
|
899
|
-
this.closeCurrentBlock(dispatch);
|
|
900
|
-
this.openBlock("matrx_file", dispatch);
|
|
901
|
-
this.appendToCurrentBlock(rawLine);
|
|
902
|
-
this.closeCurrentBlock(dispatch);
|
|
903
|
-
this.openBlock("text", dispatch);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
if (hasCandidate(flags, Candidate.DIVIDER)) {
|
|
907
|
-
this.closeCurrentBlock(dispatch);
|
|
908
|
-
const isHeavy = trimmed.startsWith("#");
|
|
909
|
-
this.openBlock(isHeavy ? "heavy-divider" : "accent-divider", dispatch);
|
|
910
|
-
this.appendToCurrentBlock(rawLine);
|
|
911
|
-
this.closeCurrentBlock(dispatch);
|
|
912
|
-
this.openBlock("text", dispatch);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (hasCandidate(flags, Candidate.BARE_JSON) && trimmed.startsWith("{")) {
|
|
916
|
-
const { opens: openCount, closes: closeCount } = countStructuralObjectBraces(trimmed);
|
|
917
|
-
if (openCount === closeCount && openCount > 0 && !isParseableJsonObject(trimmed) || // A line whose JSON grammar already broke into prose is prose (round 10).
|
|
918
|
-
regionBreaksIntoProse(trimmed)) {
|
|
919
|
-
this.appendToCurrentBlock(rawLine);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const inArray = this.nextBareJsonInArray;
|
|
923
|
-
this.nextBareJsonInArray = false;
|
|
924
|
-
const resumeXml = this.nextBareJsonResumeXml ?? void 0;
|
|
925
|
-
this.nextBareJsonResumeXml = null;
|
|
926
|
-
this.closeCurrentBlock(dispatch);
|
|
927
|
-
this.openBlock("code", dispatch);
|
|
928
|
-
this.subState = {
|
|
929
|
-
kind: "bare_json",
|
|
930
|
-
openBraces: openCount,
|
|
931
|
-
closeBraces: closeCount,
|
|
932
|
-
earlyTypeResolved: false,
|
|
933
|
-
inArray,
|
|
934
|
-
resumeXml
|
|
935
|
-
};
|
|
936
|
-
this.irOpenRegion();
|
|
937
|
-
this.irFeedLine(rawLine);
|
|
938
|
-
this.appendToCurrentBlock(rawLine);
|
|
939
|
-
if (openCount === closeCount && openCount > 0) {
|
|
940
|
-
const jsonType = detectJsonBlockType(this.currentBlockContent);
|
|
941
|
-
this.currentBlockType = jsonType ?? "code";
|
|
942
|
-
this.closeCurrentBlock(dispatch);
|
|
943
|
-
if (resumeXml) {
|
|
944
|
-
this.resumeXmlSection(resumeXml, dispatch);
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
this.subState = inArray ? { kind: "kind_array", held: null } : { kind: "none" };
|
|
948
|
-
this.openBlock("text", dispatch);
|
|
949
|
-
if (inArray) this.suppressEmptyTrailingSlot = true;
|
|
950
|
-
} else {
|
|
951
|
-
const jsonType = detectJsonBlockType(this.currentBlockContent);
|
|
952
|
-
if (jsonType) {
|
|
953
|
-
this.subState.earlyTypeResolved = true;
|
|
954
|
-
this.currentBlockType = jsonType;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
this.appendToCurrentBlock(rawLine);
|
|
960
|
-
}
|
|
961
|
-
/** Set while the next bare-JSON region opens as an array element (A6). */
|
|
962
|
-
nextBareJsonInArray = false;
|
|
963
|
-
/** Set while the next bare-JSON region is a kind split out of a section (A8). */
|
|
964
|
-
nextBareJsonResumeXml = null;
|
|
965
|
-
/** True while a split kind's earlier lines replay into its region (the XML tracker already saw them). */
|
|
966
|
-
replayingSplitKind = false;
|
|
967
|
-
/**
|
|
968
|
-
* Track a JSON object inside a simple XML section; split it out the moment
|
|
969
|
-
* it carries a `__kind` key (A8). `line` is the section body part of a
|
|
970
|
-
* line. Returns true when the line was consumed by the split.
|
|
971
|
-
*/
|
|
972
|
-
trackXmlKindCandidate(line, dispatch) {
|
|
973
|
-
if (this.subState.kind !== "xml_tag" || this.subState.isAttrXml) return false;
|
|
974
|
-
let candidate = this.subState.jsonCandidate ?? null;
|
|
975
|
-
if (!candidate) {
|
|
976
|
-
if (!line.trimStart().startsWith("{")) return false;
|
|
977
|
-
candidate = {
|
|
978
|
-
contentLen: this.currentBlockContent.length,
|
|
979
|
-
lineCount: this.currentBlockLineCount,
|
|
980
|
-
lines: [],
|
|
981
|
-
depth: 0
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
candidate.lines.push(line);
|
|
985
|
-
const { opens, closes } = countStructuralObjectBraces(line);
|
|
986
|
-
candidate.depth += opens - closes;
|
|
987
|
-
if (hasKindKey(candidate.lines.join("\n"))) {
|
|
988
|
-
this.splitKindOutOfXmlSection(candidate, dispatch);
|
|
989
|
-
return true;
|
|
990
|
-
}
|
|
991
|
-
this.subState.jsonCandidate = candidate.depth > 0 ? candidate : null;
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
/**
|
|
995
|
-
* Close the section's part before the kind (trimmed, like the reload's
|
|
996
|
-
* piece), then replay the kind's lines so far as a bare-JSON region that
|
|
997
|
-
* resumes the section when it balances (A8).
|
|
998
|
-
*/
|
|
999
|
-
splitKindOutOfXmlSection(candidate, dispatch) {
|
|
1000
|
-
if (this.subState.kind !== "xml_tag") return;
|
|
1001
|
-
const section = {
|
|
1002
|
-
...this.subState,
|
|
1003
|
-
jsonCandidate: null,
|
|
1004
|
-
balance: { ...this.subState.balance }
|
|
1005
|
-
};
|
|
1006
|
-
this.currentBlockContent = this.currentBlockContent.slice(0, candidate.contentLen).trim();
|
|
1007
|
-
this.currentBlockLineCount = this.currentBlockContent ? candidate.lineCount : 0;
|
|
1008
|
-
this.regionContinuesAfterClose = true;
|
|
1009
|
-
this.closeCurrentBlock(dispatch);
|
|
1010
|
-
this.regionContinuesAfterClose = false;
|
|
1011
|
-
this.subState = { kind: "none" };
|
|
1012
|
-
this.openBlock("text", dispatch);
|
|
1013
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1014
|
-
this.nextBareJsonResumeXml = section;
|
|
1015
|
-
const [first, ...rest] = candidate.lines;
|
|
1016
|
-
if (first !== void 0) {
|
|
1017
|
-
const trimmedFirst = first.trimStart();
|
|
1018
|
-
const end = firstCompleteRootObjectEnd(trimmedFirst);
|
|
1019
|
-
if (rest.length === 0 && end !== null && trimmedFirst.slice(end).trim()) {
|
|
1020
|
-
this.processLineNow(trimmedFirst.slice(0, end), dispatch);
|
|
1021
|
-
this.processLine(trimmedFirst.slice(end), dispatch);
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
this.processLineNow(trimmedFirst, dispatch);
|
|
1025
|
-
}
|
|
1026
|
-
for (const line of rest) this.processLineNow(line, dispatch);
|
|
1027
|
-
}
|
|
1028
|
-
/** The section a kind was split out of continues in a fresh block (A8). */
|
|
1029
|
-
resumeXmlSection(section, dispatch) {
|
|
1030
|
-
if (section.kind === "generic_xml") {
|
|
1031
|
-
this.openBlock("code", dispatch);
|
|
1032
|
-
this.subState = { ...section, kindCandidate: null };
|
|
1033
|
-
this.currentBlockLineCount = 1;
|
|
1034
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
this.openBlock(mapXmlTagToBlockType(section.tagName), dispatch);
|
|
1038
|
-
this.subState = { ...section, jsonCandidate: null, resumedAfterKind: true };
|
|
1039
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Track a JSON object or JSON-family fence inside a generic XML container
|
|
1043
|
-
* (an XML TAG — structure, ruling (b), round 3); split it out the moment its
|
|
1044
|
-
* text carries a `__kind` key (X1). Returns true when the line was consumed.
|
|
1045
|
-
*/
|
|
1046
|
-
trackGenericXmlKindCandidate(line, dispatch) {
|
|
1047
|
-
if (this.subState.kind !== "generic_xml") return false;
|
|
1048
|
-
let candidate = this.subState.kindCandidate ?? null;
|
|
1049
|
-
if (!candidate) {
|
|
1050
|
-
if (endsInsideXmlLiteral(this.currentBlockContent)) return false;
|
|
1051
|
-
const trimmed = line.trimStart();
|
|
1052
|
-
const opener = /^ {0,3}[`~]/.test(line) ? fenceOpenerOf(trimmed) : null;
|
|
1053
|
-
const fence = opener && isJsonFenceLanguage(opener.lang) ? opener : null;
|
|
1054
|
-
if (!fence && !trimmed.startsWith("{")) return false;
|
|
1055
|
-
candidate = {
|
|
1056
|
-
contentLen: this.currentBlockContent.length,
|
|
1057
|
-
lineCount: this.currentBlockLineCount,
|
|
1058
|
-
lines: [],
|
|
1059
|
-
depth: 0,
|
|
1060
|
-
fence: fence ? { char: fence.char, ticks: fence.ticks } : null
|
|
1061
|
-
};
|
|
1062
|
-
if (fence) {
|
|
1063
|
-
candidate.lines.push(line);
|
|
1064
|
-
this.subState.kindCandidate = candidate;
|
|
1065
|
-
return false;
|
|
1066
|
-
}
|
|
1067
|
-
} else if (candidate.fence && isFenceCloserLine(line, candidate.fence)) {
|
|
1068
|
-
this.subState.kindCandidate = null;
|
|
1069
|
-
return false;
|
|
1070
|
-
}
|
|
1071
|
-
candidate.lines.push(line);
|
|
1072
|
-
if (!candidate.fence) {
|
|
1073
|
-
const { opens, closes } = countStructuralObjectBraces(line);
|
|
1074
|
-
candidate.depth += opens - closes;
|
|
1075
|
-
}
|
|
1076
|
-
if (hasKindKey(genericCandidateBody(candidate))) {
|
|
1077
|
-
this.splitKindOutOfGenericXml(candidate, dispatch);
|
|
1078
|
-
return true;
|
|
1079
|
-
}
|
|
1080
|
-
this.subState.kindCandidate = candidate.fence || candidate.depth > 0 ? candidate : null;
|
|
1081
|
-
return false;
|
|
1082
|
-
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Close the XML card's part before the kind, then replay the kind's lines
|
|
1085
|
-
* so far as its own region (bare JSON or the JSON fence) that resumes the
|
|
1086
|
-
* container when it closes (X1). The reload's recovery splits the same bytes.
|
|
1087
|
-
*/
|
|
1088
|
-
splitKindOutOfGenericXml(candidate, dispatch) {
|
|
1089
|
-
if (this.subState.kind !== "generic_xml") return;
|
|
1090
|
-
const container = { ...this.subState, kindCandidate: null };
|
|
1091
|
-
const before = this.currentBlockContent.slice(0, candidate.contentLen);
|
|
1092
|
-
this.currentBlockContent = before.trim() ? `${before}
|
|
1093
|
-
` : "";
|
|
1094
|
-
this.currentBlockLineCount = before.trim() ? candidate.lineCount : 0;
|
|
1095
|
-
this.closeCurrentBlock(dispatch);
|
|
1096
|
-
this.subState = { kind: "none" };
|
|
1097
|
-
this.openBlock("text", dispatch);
|
|
1098
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1099
|
-
this.nextBareJsonResumeXml = container;
|
|
1100
|
-
this.replayingSplitKind = true;
|
|
116
|
+
for (let i = blocks.length; i < this.drawn; i++) this.retract(dispatch, this.base + i);
|
|
117
|
+
this.drawn = blocks.length;
|
|
118
|
+
}
|
|
119
|
+
metadataFor(block, index, open) {
|
|
120
|
+
const metadata = block.metadata;
|
|
121
|
+
if (metadata?.[IR_ENVELOPE_KEY]) return metadata;
|
|
122
|
+
const source = jsonRegionSource(block);
|
|
123
|
+
if (!source) {
|
|
124
|
+
if (this.region?.index === index) this.closeRegion();
|
|
125
|
+
return metadata;
|
|
126
|
+
}
|
|
127
|
+
if (open) {
|
|
128
|
+
const envelope = this.feedRegion(block, index);
|
|
129
|
+
return envelope ? { ...metadata ?? {}, [IR_ENVELOPE_KEY]: envelope } : metadata;
|
|
130
|
+
}
|
|
131
|
+
const closed = this.closedEnvelopes.get(index);
|
|
132
|
+
if (closed) return { ...metadata ?? {}, [IR_ENVELOPE_KEY]: closed };
|
|
133
|
+
if (this.region?.index === index) {
|
|
134
|
+
this.feedRegion(block, index);
|
|
135
|
+
const envelope = this.closeRegion();
|
|
136
|
+
if (envelope) return { ...metadata ?? {}, [IR_ENVELOPE_KEY]: envelope };
|
|
137
|
+
}
|
|
138
|
+
return metadata;
|
|
139
|
+
}
|
|
140
|
+
/** Feed the open region's new bytes; reopen when the block is a different region. */
|
|
141
|
+
feedRegion(block, index = this.region?.index ?? -1) {
|
|
142
|
+
const source = jsonRegionSource(block);
|
|
143
|
+
if (!source) return null;
|
|
144
|
+
if (this.region && (this.region.index !== index || !source.startsWith(this.region.fed))) {
|
|
145
|
+
this.dropRegion();
|
|
146
|
+
}
|
|
147
|
+
if (!this.region) {
|
|
148
|
+
const session = this.openRegion(index);
|
|
149
|
+
if (!session) return null;
|
|
150
|
+
this.region = { index, identity: `${this.requestId}:client_block_${index}`, session, fed: "" };
|
|
151
|
+
}
|
|
152
|
+
const delta = source.slice(this.region.fed.length);
|
|
153
|
+
if (delta) {
|
|
154
|
+
this.region.session.write(delta);
|
|
155
|
+
this.region.fed = source;
|
|
156
|
+
}
|
|
157
|
+
return sessionEnvelope(this.region.session);
|
|
158
|
+
}
|
|
159
|
+
openRegion(index) {
|
|
160
|
+
const identity = `${this.requestId}:client_block_${index}`;
|
|
1101
161
|
try {
|
|
1102
|
-
const
|
|
1103
|
-
if (first !== void 0) {
|
|
1104
|
-
const trimmedFirst = first.trimStart();
|
|
1105
|
-
const end = candidate.fence ? null : firstCompleteRootObjectEnd(trimmedFirst);
|
|
1106
|
-
if (rest.length === 0 && end !== null && trimmedFirst.slice(end).trim()) {
|
|
1107
|
-
this.processLineNow(trimmedFirst.slice(0, end), dispatch);
|
|
1108
|
-
this.replayingSplitKind = false;
|
|
1109
|
-
this.processLine(trimmedFirst.slice(end), dispatch);
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
this.processLineNow(candidate.fence ? first : trimmedFirst, dispatch);
|
|
1113
|
-
}
|
|
1114
|
-
for (const line of rest) this.processLineNow(line, dispatch);
|
|
1115
|
-
} finally {
|
|
1116
|
-
this.replayingSplitKind = false;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
/** The fragment twin of the generic-XML split (X1): `<output>\n{"__kind":…` with no newline yet. */
|
|
1120
|
-
maybeSplitKindFromGenericXmlFragment(dispatch) {
|
|
1121
|
-
if (this.subState.kind !== "generic_xml") return;
|
|
1122
|
-
const fragment = this.pendingLineFragment;
|
|
1123
|
-
if (fragment.includes(`</${this.subState.tracker.rootTag}`)) return;
|
|
1124
|
-
const candidate = this.subState.kindCandidate ?? null;
|
|
1125
|
-
if (!candidate && !BARE_JSON_OPEN_RE.test(fragment.trimStart())) return;
|
|
1126
|
-
if (!candidate && endsInsideXmlLiteral(this.currentBlockContent)) return;
|
|
1127
|
-
const text = candidate ? genericCandidateBody({ ...candidate, lines: [...candidate.lines, fragment] }) : fragment;
|
|
1128
|
-
if (!hasKindKey(text)) return;
|
|
1129
|
-
this.splitKindOutOfGenericXml(
|
|
1130
|
-
candidate ?? {
|
|
1131
|
-
contentLen: this.currentBlockContent.length,
|
|
1132
|
-
lineCount: this.currentBlockLineCount,
|
|
1133
|
-
lines: [],
|
|
1134
|
-
depth: 0,
|
|
1135
|
-
fence: null
|
|
1136
|
-
},
|
|
1137
|
-
dispatch
|
|
1138
|
-
);
|
|
1139
|
-
if (!candidate) this.pendingLineFragment = fragment.trimStart();
|
|
1140
|
-
}
|
|
1141
|
-
/**
|
|
1142
|
-
* The fragment twin of the section split: a newline-less kind object inside
|
|
1143
|
-
* a simple section (`<info>{"__kind":…`) leaves it as soon as `__kind` is
|
|
1144
|
-
* visible, and the fragment opens the region below (A8).
|
|
1145
|
-
*/
|
|
1146
|
-
maybeSplitKindFromXmlFragment(dispatch) {
|
|
1147
|
-
if (this.subState.kind !== "xml_tag" || this.subState.isAttrXml) return;
|
|
1148
|
-
const fragment = this.pendingLineFragment;
|
|
1149
|
-
if (fragment.includes(this.subState.closingTag)) return;
|
|
1150
|
-
const candidate = this.subState.jsonCandidate;
|
|
1151
|
-
const text = candidate ? [...candidate.lines, fragment].join("\n") : fragment;
|
|
1152
|
-
if (!candidate && !BARE_JSON_OPEN_RE.test(fragment.trimStart())) return;
|
|
1153
|
-
if (!hasKindKey(text)) return;
|
|
1154
|
-
this.splitKindOutOfXmlSection(
|
|
1155
|
-
candidate ?? {
|
|
1156
|
-
contentLen: this.currentBlockContent.length,
|
|
1157
|
-
lineCount: this.currentBlockLineCount,
|
|
1158
|
-
lines: [],
|
|
1159
|
-
depth: 0
|
|
1160
|
-
},
|
|
1161
|
-
dispatch
|
|
1162
|
-
);
|
|
1163
|
-
if (!candidate) this.pendingLineFragment = fragment.trimStart();
|
|
1164
|
-
}
|
|
1165
|
-
/**
|
|
1166
|
-
* The held array start is a kind array: close the prose before it, drop the
|
|
1167
|
-
* `[`, and replay the held lines as array content (A6).
|
|
1168
|
-
*/
|
|
1169
|
-
commitKindArray(dispatch) {
|
|
1170
|
-
if (this.subState.kind !== "kind_array" || this.subState.held === null) return;
|
|
1171
|
-
const [first, ...rest] = this.subState.held;
|
|
1172
|
-
this.subState = { kind: "kind_array", held: null };
|
|
1173
|
-
this.closeCurrentBlock(dispatch);
|
|
1174
|
-
this.openBlock("text", dispatch);
|
|
1175
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1176
|
-
this.continueKindArray((first ?? "").replace(/^\s*\[/, ""), dispatch);
|
|
1177
|
-
for (const line of rest) this.processLineNow(line, dispatch);
|
|
1178
|
-
}
|
|
1179
|
-
/** Not a kind array after all: the held lines are ordinary lines, in order. */
|
|
1180
|
-
releaseHeldArray(dispatch) {
|
|
1181
|
-
if (this.subState.kind !== "kind_array" || this.subState.held === null) return;
|
|
1182
|
-
const [first, ...rest] = this.subState.held;
|
|
1183
|
-
this.subState = { kind: "none" };
|
|
1184
|
-
this.appendToCurrentBlock(first ?? "");
|
|
1185
|
-
this.lineQueue.unshift(...rest.map((rawLine) => ({ rawLine, dispatch })));
|
|
1186
|
-
this.drainLineQueue();
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Array content between (or opening) elements: `,` and whitespace are
|
|
1190
|
-
* chrome, `]` ends the array, `{` opens the next element as its own
|
|
1191
|
-
* bare-JSON region. Anything else ends the array as ordinary source.
|
|
1192
|
-
*/
|
|
1193
|
-
continueKindArray(text, dispatch) {
|
|
1194
|
-
let rest = text;
|
|
1195
|
-
while (this.subState.kind === "kind_array") {
|
|
1196
|
-
rest = rest.replace(/^[\s,]*/, "");
|
|
1197
|
-
if (!rest) return;
|
|
1198
|
-
if (rest.startsWith("]")) {
|
|
1199
|
-
this.subState = { kind: "none" };
|
|
1200
|
-
const after = rest.slice(1);
|
|
1201
|
-
if (after.trim()) this.processLineNow(after, dispatch);
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
if (!rest.startsWith("{")) {
|
|
1205
|
-
this.subState = { kind: "none" };
|
|
1206
|
-
this.processLineNow(rest, dispatch);
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
const end = firstCompleteRootObjectEnd(rest);
|
|
1210
|
-
this.subState = { kind: "none" };
|
|
1211
|
-
this.nextBareJsonInArray = true;
|
|
1212
|
-
if (end === null) {
|
|
1213
|
-
this.processLineNow(rest, dispatch);
|
|
1214
|
-
this.nextBareJsonInArray = false;
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
this.processLineNow(rest.slice(0, end), dispatch);
|
|
1218
|
-
this.nextBareJsonInArray = false;
|
|
1219
|
-
rest = rest.slice(end);
|
|
1220
|
-
}
|
|
1221
|
-
if (rest.trim()) this.processLineNow(rest, dispatch);
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* The fragment twin of the array entry in processLineNow: a newline-less
|
|
1225
|
-
* `[{"__kind":…` decides on its first key while it streams (A6).
|
|
1226
|
-
*/
|
|
1227
|
-
maybeEnterKindArrayFromFragment(dispatch) {
|
|
1228
|
-
if (this.subState.kind === "kind_array" && this.subState.held !== null) {
|
|
1229
|
-
const signal = jsonKindSignal(
|
|
1230
|
-
[...this.subState.held, this.pendingLineFragment].join("\n")
|
|
1231
|
-
);
|
|
1232
|
-
if (signal !== "kind") return;
|
|
1233
|
-
this.commitKindArray(dispatch);
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
if (this.subState.kind !== "none") return;
|
|
1237
|
-
const fragment = this.pendingLineFragment;
|
|
1238
|
-
if (!/^\s*\[\s*\{/.test(fragment)) return;
|
|
1239
|
-
if (jsonKindSignal(fragment) !== "kind") return;
|
|
1240
|
-
this.closeCurrentBlock(dispatch);
|
|
1241
|
-
this.openBlock("text", dispatch);
|
|
1242
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1243
|
-
this.subState = { kind: "kind_array", held: null };
|
|
1244
|
-
this.pendingLineFragment = fragment.replace(/^\s*\[/, "");
|
|
1245
|
-
}
|
|
1246
|
-
/** The fragment twin of the complete-line split in processLineNow (A5). */
|
|
1247
|
-
maybeSplitProseBeforeKindFragment(dispatch) {
|
|
1248
|
-
if (this.subState.kind !== "none" || this.kindLineIsProse) return;
|
|
1249
|
-
const fragment = this.pendingLineFragment;
|
|
1250
|
-
if (startsStructuralLine(fragment)) return;
|
|
1251
|
-
const at = proseKindObjectStart(fragment);
|
|
1252
|
-
if (at <= 0) return;
|
|
1253
|
-
if (hasUnclosedBacktickRun(fragment.slice(0, at))) return;
|
|
1254
|
-
this.pendingLineFragment = fragment.slice(at);
|
|
1255
|
-
this.processLine(fragment.slice(0, at), dispatch);
|
|
1256
|
-
}
|
|
1257
|
-
/**
|
|
1258
|
-
* Open a bare_json region directly from the trailing line fragment — the
|
|
1259
|
-
* newline-less minified JSON case (`{"__kind":…}` streamed as one line with
|
|
1260
|
-
* no `\n` until the end). Without this, processLine (which only sees COMPLETE
|
|
1261
|
-
* lines) never opens the region and the raw JSON projects as a text block
|
|
1262
|
-
* until finalize — the "shows the whole JSON, converts only when done" bug.
|
|
1263
|
-
*
|
|
1264
|
-
* Mirrors processLine's bare-JSON branch but fragment-driven: it commits the
|
|
1265
|
-
* moment the fragment reveals `{ "key":` (unambiguous JSON), retracting the
|
|
1266
|
-
* speculative text projection of those same chars, and lets the existing
|
|
1267
|
-
* fragment-feed path (in ingest) and finalize/newline completion carry the
|
|
1268
|
-
* region the rest of the way. openBraces/closeBraces start at 0 — the
|
|
1269
|
-
* completing line counts them exactly once (see the bare_json substate case).
|
|
1270
|
-
*/
|
|
1271
|
-
maybeOpenBareJsonFromFragment(dispatch) {
|
|
1272
|
-
let inArray = false;
|
|
1273
|
-
if (this.subState.kind === "kind_array" && this.subState.held === null) {
|
|
1274
|
-
const chrome = /^\s*,?\s*/.exec(this.pendingLineFragment)[0];
|
|
1275
|
-
this.pendingLineFragment = this.pendingLineFragment.slice(chrome.length);
|
|
1276
|
-
if (this.pendingLineFragment.startsWith("]")) {
|
|
1277
|
-
this.pendingLineFragment = this.pendingLineFragment.slice(1);
|
|
1278
|
-
this.subState = { kind: "none" };
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
inArray = true;
|
|
1282
|
-
} else if (this.subState.kind !== "none" || this.kindLineIsProse) return;
|
|
1283
|
-
const completeTag = /^\s*<([A-Za-z][\w-]*)>[\s\S]*?<\/\1>/.exec(
|
|
1284
|
-
this.pendingLineFragment
|
|
1285
|
-
);
|
|
1286
|
-
if (completeTag && this.pendingLineFragment.slice(completeTag[0].length).trimStart().startsWith("{")) {
|
|
1287
|
-
this.pendingLineFragment = this.pendingLineFragment.slice(
|
|
1288
|
-
completeTag[0].length
|
|
1289
|
-
);
|
|
1290
|
-
this.processLine(completeTag[0], dispatch);
|
|
1291
|
-
if (this.subState.kind !== "none" || this.currentBlockContent) return;
|
|
1292
|
-
}
|
|
1293
|
-
const frag = this.pendingLineFragment;
|
|
1294
|
-
if (!BARE_JSON_OPEN_RE.test(frag.trimStart())) return;
|
|
1295
|
-
const resumeXml = this.nextBareJsonResumeXml ?? void 0;
|
|
1296
|
-
this.nextBareJsonResumeXml = null;
|
|
1297
|
-
this.closeCurrentBlock(dispatch);
|
|
1298
|
-
this.openBlock("code", dispatch);
|
|
1299
|
-
this.subState = {
|
|
1300
|
-
kind: "bare_json",
|
|
1301
|
-
openBraces: 0,
|
|
1302
|
-
closeBraces: 0,
|
|
1303
|
-
earlyTypeResolved: false,
|
|
1304
|
-
inArray,
|
|
1305
|
-
resumeXml
|
|
1306
|
-
};
|
|
1307
|
-
this.irOpenRegion();
|
|
1308
|
-
this.irWriteLinePart(frag, false);
|
|
1309
|
-
const jsonType = detectJsonBlockType(frag.trimStart());
|
|
1310
|
-
if (jsonType) {
|
|
1311
|
-
this.subState.earlyTypeResolved = true;
|
|
1312
|
-
this.currentBlockType = jsonType;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
processSubStateLine(rawLine, trimmed, dispatch) {
|
|
1316
|
-
switch (this.subState.kind) {
|
|
1317
|
-
case "directive": {
|
|
1318
|
-
this.appendToCurrentBlock(rawLine);
|
|
1319
|
-
if (this.subState.tracker.consume(rawLine)) this.subState = { kind: "none" };
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
case "frontmatter": {
|
|
1323
|
-
this.appendToCurrentBlock(rawLine);
|
|
1324
|
-
const line = rawLine.replace(/\r$/, "");
|
|
1325
|
-
if (line === this.subState.fence || this.subState.fence === "---" && line === "...") {
|
|
1326
|
-
this.subState = { kind: "none" };
|
|
1327
|
-
}
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
case "code_fence": {
|
|
1331
|
-
if (this.subState.resumeXml?.kind === "generic_xml" && !this.replayingSplitKind) {
|
|
1332
|
-
this.subState.resumeXml.tracker.consumeLine(rawLine);
|
|
1333
|
-
}
|
|
1334
|
-
const fenceLine = this.subState.tildeReader ? this.subState.tildeReader.feed(rawLine) ? "close-outer" : "content" : classifyInnerFenceLine(
|
|
1335
|
-
trimFenceLine(rawLine),
|
|
1336
|
-
this.subState.fenceTicks,
|
|
1337
|
-
fenceNestsInnerFences(this.subState.language),
|
|
1338
|
-
this.subState.nestedFences
|
|
1339
|
-
);
|
|
1340
|
-
if (fenceLine === "open-nested") this.subState.nestedFences++;
|
|
1341
|
-
if (fenceLine === "close-nested") this.subState.nestedFences--;
|
|
1342
|
-
if (fenceLine === "close-outer") {
|
|
1343
|
-
if (normalizeCodeLanguage(this.subState.language) === "json") {
|
|
1344
|
-
const confirmed = detectJsonBlockType(this.currentBlockContent);
|
|
1345
|
-
this.currentBlockType = confirmed ?? "code";
|
|
1346
|
-
}
|
|
1347
|
-
this.fenceClosedCleanly = true;
|
|
1348
|
-
const resumeXml = this.subState.resumeXml;
|
|
1349
|
-
this.closeCurrentBlock(dispatch);
|
|
1350
|
-
if (resumeXml) {
|
|
1351
|
-
this.resumeXmlSection(resumeXml, dispatch);
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
this.subState = { kind: "none" };
|
|
1355
|
-
this.openBlock("text", dispatch);
|
|
1356
|
-
} else {
|
|
1357
|
-
if (normalizeCodeLanguage(this.subState.language) === "json" && !this.subState.earlyTypeResolved) {
|
|
1358
|
-
const soFar = this.currentBlockContent ? this.currentBlockContent + "\n" + rawLine : rawLine;
|
|
1359
|
-
const jsonType = detectJsonBlockType(soFar);
|
|
1360
|
-
if (jsonType) {
|
|
1361
|
-
this.subState.earlyTypeResolved = true;
|
|
1362
|
-
this.currentBlockType = jsonType;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
this.irFeedLine(rawLine);
|
|
1366
|
-
this.appendToCurrentBlock(rawLine);
|
|
1367
|
-
}
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
case "xml_tag": {
|
|
1371
|
-
const { closingTag, isAttrXml, tagName, balance } = this.subState;
|
|
1372
|
-
const closingIdxRaw = findBalancedXmlClose(rawLine, tagName, balance, {
|
|
1373
|
-
trackFences: false
|
|
1374
|
-
});
|
|
1375
|
-
const remainder = closingIdxRaw === -1 ? "" : rawLine.slice(closingIdxRaw + closingTag.length);
|
|
1376
|
-
let lineToAppend = rawLine;
|
|
1377
|
-
if (closingIdxRaw !== -1) {
|
|
1378
|
-
lineToAppend = isAttrXml ? rawLine.slice(0, closingIdxRaw + closingTag.length) : rawLine.slice(0, closingIdxRaw);
|
|
1379
|
-
}
|
|
1380
|
-
const isClosingLine = closingIdxRaw !== -1;
|
|
1381
|
-
if (!isAttrXml) {
|
|
1382
|
-
const body = isClosingLine ? rawLine.slice(0, closingIdxRaw) : rawLine;
|
|
1383
|
-
if (this.trackXmlKindCandidate(body, dispatch)) {
|
|
1384
|
-
if (isClosingLine) this.processLine(rawLine.slice(closingIdxRaw), dispatch);
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
if (isAttrXml && !isClosingLine) {
|
|
1389
|
-
if (this.irSession) {
|
|
1390
|
-
this.irFeedLine(rawLine);
|
|
1391
|
-
} else {
|
|
1392
|
-
this.maybeOpenXmlBodyIrRegion(rawLine, true);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
this.appendToCurrentBlock(lineToAppend);
|
|
1396
|
-
if (isClosingLine && this.subState.resumedAfterKind) {
|
|
1397
|
-
this.currentBlockContent = this.currentBlockContent.trim();
|
|
1398
|
-
}
|
|
1399
|
-
if (isClosingLine) {
|
|
1400
|
-
this.xmlClosedCleanly = true;
|
|
1401
|
-
this.closeCurrentBlock(dispatch);
|
|
1402
|
-
this.subState = { kind: "none" };
|
|
1403
|
-
this.openBlock("text", dispatch);
|
|
1404
|
-
if (remainder.length > 0) {
|
|
1405
|
-
this.processLine(remainder, dispatch);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
case "kind_array": {
|
|
1411
|
-
const held = this.subState.held;
|
|
1412
|
-
if (held === null) {
|
|
1413
|
-
this.continueKindArray(rawLine, dispatch);
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
held.push(rawLine);
|
|
1417
|
-
const signal = jsonKindSignal(held.join("\n"));
|
|
1418
|
-
if (signal === "kind") this.commitKindArray(dispatch);
|
|
1419
|
-
else if (signal === "not_kind") this.releaseHeldArray(dispatch);
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
case "generic_xml": {
|
|
1423
|
-
const rootEnd = this.subState.tracker.consumeLine(rawLine);
|
|
1424
|
-
if (rootEnd === null) {
|
|
1425
|
-
if (this.trackGenericXmlKindCandidate(rawLine, dispatch)) return;
|
|
1426
|
-
this.appendToCurrentBlock(rawLine);
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
this.subState.kindCandidate = null;
|
|
1430
|
-
this.appendToCurrentBlock(rawLine.slice(0, rootEnd));
|
|
1431
|
-
this.closeCurrentBlock(dispatch);
|
|
1432
|
-
this.subState = { kind: "none" };
|
|
1433
|
-
this.openBlock("text", dispatch);
|
|
1434
|
-
const remainder = rawLine.slice(rootEnd).trim();
|
|
1435
|
-
if (remainder) this.processLine(remainder, dispatch);
|
|
1436
|
-
else this.suppressEmptyTrailingSlot = true;
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
case "table": {
|
|
1440
|
-
const flags = classifyLine(rawLine, trimmed);
|
|
1441
|
-
const { container } = this.subState;
|
|
1442
|
-
const indented = lineIndent(rawLine) - container >= 4;
|
|
1443
|
-
if (hasCandidate(flags, Candidate.TABLE) && !indented || continuesTable(rawLine, container)) {
|
|
1444
|
-
this.appendToCurrentBlock(rawLine);
|
|
1445
|
-
} else {
|
|
1446
|
-
this.closeCurrentBlock(dispatch);
|
|
1447
|
-
this.subState = { kind: "none" };
|
|
1448
|
-
this.openBlock("text", dispatch);
|
|
1449
|
-
this.processLine(rawLine, dispatch);
|
|
1450
|
-
}
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
case "bare_json": {
|
|
1454
|
-
if (!this.currentBlockContent && kindObjectProseBreak(rawLine.trimStart()) !== null || regionBreaksIntoProse(this.currentBlockContent ? `${this.currentBlockContent}
|
|
1455
|
-
${rawLine}` : rawLine)) {
|
|
1456
|
-
this.demoteKindLineToProse();
|
|
1457
|
-
this.kindLineIsProse = false;
|
|
1458
|
-
this.appendToCurrentBlock(rawLine);
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
const completionEnd = firstCompleteRootObjectEnd(
|
|
1462
|
-
rawLine,
|
|
1463
|
-
this.subState.openBraces - this.subState.closeBraces
|
|
1464
|
-
);
|
|
1465
|
-
if (completionEnd !== null && rawLine.slice(completionEnd).trim().length > 0) {
|
|
1466
|
-
this.processSubStateLine(
|
|
1467
|
-
rawLine.slice(0, completionEnd),
|
|
1468
|
-
rawLine.slice(0, completionEnd).trim(),
|
|
1469
|
-
dispatch
|
|
1470
|
-
);
|
|
1471
|
-
this.processLine(rawLine.slice(completionEnd).trimStart(), dispatch);
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
const { opens: lineOpens, closes: lineCloses } = countStructuralObjectBraces(trimmed);
|
|
1475
|
-
const willBalance = this.subState.openBraces + lineOpens === this.subState.closeBraces + lineCloses && this.subState.openBraces + lineOpens > 0;
|
|
1476
|
-
if (!willBalance && !this.subState.earlyTypeResolved) {
|
|
1477
|
-
const soFar = this.currentBlockContent ? this.currentBlockContent + "\n" + rawLine : rawLine;
|
|
1478
|
-
const jsonType = detectJsonBlockType(soFar);
|
|
1479
|
-
if (jsonType) {
|
|
1480
|
-
this.subState.earlyTypeResolved = true;
|
|
1481
|
-
this.currentBlockType = jsonType;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
this.irFeedLine(rawLine);
|
|
1485
|
-
this.appendToCurrentBlock(rawLine);
|
|
1486
|
-
this.subState.openBraces += lineOpens;
|
|
1487
|
-
this.subState.closeBraces += lineCloses;
|
|
1488
|
-
if (this.subState.openBraces === this.subState.closeBraces && this.subState.openBraces > 0) {
|
|
1489
|
-
const jsonType = detectJsonBlockType(this.currentBlockContent);
|
|
1490
|
-
this.currentBlockType = jsonType ?? "code";
|
|
1491
|
-
const inArray = this.subState.inArray === true;
|
|
1492
|
-
const resumeXml = this.subState.resumeXml;
|
|
1493
|
-
this.closeCurrentBlock(dispatch);
|
|
1494
|
-
if (resumeXml) {
|
|
1495
|
-
this.resumeXmlSection(resumeXml, dispatch);
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
this.subState = inArray ? { kind: "kind_array", held: null } : { kind: "none" };
|
|
1499
|
-
this.openBlock("text", dispatch);
|
|
1500
|
-
this.suppressEmptyTrailingSlot = true;
|
|
1501
|
-
}
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
// ── XML opening helper ──────────────────────────────────────────────
|
|
1507
|
-
openXmlTagBlock(tag, rawLine, trimmed, dispatch) {
|
|
1508
|
-
this.closeCurrentBlock(dispatch);
|
|
1509
|
-
const blockType = mapXmlTagToBlockType(tag);
|
|
1510
|
-
this.openBlock(blockType, dispatch);
|
|
1511
|
-
const isAttrXml = ATTR_XML_TAGS.has(tag);
|
|
1512
|
-
const openingTagText = extractOpeningTagText(trimmed, tag);
|
|
1513
|
-
const attributes = isAttrXml ? parseXmlAttributes(openingTagText) : {};
|
|
1514
|
-
const closingTag = `</${tag}>`;
|
|
1515
|
-
this.subState = {
|
|
1516
|
-
kind: "xml_tag",
|
|
1517
|
-
tagName: tag,
|
|
1518
|
-
closingTag,
|
|
1519
|
-
openingTagText,
|
|
1520
|
-
attributes,
|
|
1521
|
-
isAttrXml,
|
|
1522
|
-
balance: initialXmlBalance()
|
|
1523
|
-
};
|
|
1524
|
-
const closingIdxRaw = rawLine.indexOf(closingTag);
|
|
1525
|
-
const remainder = closingIdxRaw === -1 ? "" : rawLine.slice(closingIdxRaw + closingTag.length);
|
|
1526
|
-
let lineToAppend = rawLine;
|
|
1527
|
-
if (!isAttrXml) {
|
|
1528
|
-
const openIdx = rawLine.indexOf(openingTagText);
|
|
1529
|
-
if (openIdx !== -1) {
|
|
1530
|
-
lineToAppend = rawLine.slice(0, openIdx) + rawLine.slice(openIdx + openingTagText.length);
|
|
1531
|
-
}
|
|
1532
|
-
const closingIdx = lineToAppend.indexOf(closingTag);
|
|
1533
|
-
if (closingIdx !== -1) {
|
|
1534
|
-
lineToAppend = lineToAppend.slice(0, closingIdx);
|
|
1535
|
-
}
|
|
1536
|
-
} else if (closingIdxRaw !== -1) {
|
|
1537
|
-
lineToAppend = rawLine.slice(0, closingIdxRaw + closingTag.length);
|
|
1538
|
-
}
|
|
1539
|
-
this.appendToCurrentBlock(lineToAppend);
|
|
1540
|
-
if (closingIdxRaw !== -1) {
|
|
1541
|
-
this.closeCurrentBlock(dispatch);
|
|
1542
|
-
this.subState = { kind: "none" };
|
|
1543
|
-
this.openBlock("text", dispatch);
|
|
1544
|
-
if (remainder.length > 0) {
|
|
1545
|
-
this.processLine(remainder, dispatch);
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
// ── content-ir region delegation (Phase 2) ─────────────────────────
|
|
1550
|
-
//
|
|
1551
|
-
// Boundary rule: the accumulator (host) finds the JSON region and remains
|
|
1552
|
-
// the region-end oracle (fence close / brace balance); the kind parser owns
|
|
1553
|
-
// everything INSIDE the region. Blocks gain a dark `metadata.__ir` envelope
|
|
1554
|
-
// while rendering stays content-driven — the parity check below proves the
|
|
1555
|
-
// two paths agree on real traffic before Phase 4 flips anything visible.
|
|
1556
|
-
irOpenRegion() {
|
|
1557
|
-
const identity = `${this.requestId}:${this.currentBlockId}`;
|
|
1558
|
-
try {
|
|
1559
|
-
this.irSession = openParseSession({
|
|
162
|
+
const session = openParseSession({
|
|
1560
163
|
identity,
|
|
1561
164
|
schemas: kindRegistry.resolver(),
|
|
1562
165
|
onSchemaArrived: (deliver) => kindRegistry.onSchemaArrived(deliver),
|
|
1563
|
-
// EAGER LIGHTWEIGHT FETCH: the instant a cloud kind is identified
|
|
1564
|
-
// mid-stream, fire the schema cold-fetch (the parser also requests
|
|
1565
|
-
// it — deduped) AND the targeted component fetch in parallel, so by
|
|
1566
|
-
// the time the region completes both registries can answer.
|
|
1567
|
-
// (web, output) is DELIBERATE: chat streaming renders exactly that
|
|
1568
|
-
// surface; other roles/platforms ride the warm/refresh path by
|
|
1569
|
-
// design (see SHAPE_SYSTEM.md § streaming db/cloud kinds).
|
|
1570
166
|
onEvent: (event) => {
|
|
1571
167
|
if (event.type === "kind_identified" || event.type === "pending_schema") {
|
|
1572
168
|
void kindRegistry.ensureWarm();
|
|
@@ -1576,398 +172,117 @@ ${rawLine}` : rawLine)) {
|
|
|
1576
172
|
}
|
|
1577
173
|
}
|
|
1578
174
|
});
|
|
1579
|
-
this.
|
|
1580
|
-
|
|
1581
|
-
this.irFedFragmentLen = 0;
|
|
1582
|
-
this.irEnvelope = null;
|
|
1583
|
-
this.irRegionIsXmlBody = false;
|
|
175
|
+
this.identities.push(identity);
|
|
176
|
+
return session;
|
|
1584
177
|
} catch (error) {
|
|
1585
178
|
captureError({
|
|
1586
179
|
source: "content-ir",
|
|
1587
180
|
message: `failed to open parse region ${identity}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1588
181
|
raw: error
|
|
1589
182
|
});
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
/**
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1604
|
-
if (part) {
|
|
1605
|
-
this.irSession.write(part);
|
|
1606
|
-
}
|
|
1607
|
-
if (isLineComplete) {
|
|
1608
|
-
this.irRegionLineCount++;
|
|
1609
|
-
this.irFedFragmentLen = 0;
|
|
1610
|
-
} else {
|
|
1611
|
-
this.irFedFragmentLen += part.length;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
/** Complete a region line, accounting for any fragment chars already fed. */
|
|
1615
|
-
irFeedLine(line) {
|
|
1616
|
-
if (!this.irSession) return;
|
|
1617
|
-
this.irWriteLinePart(line.slice(this.irFedFragmentLen), true);
|
|
1618
|
-
}
|
|
1619
|
-
/** End the open region: final envelope + shadow parity telemetry. */
|
|
1620
|
-
irCloseRegion() {
|
|
1621
|
-
const session = this.irSession;
|
|
1622
|
-
if (!session) return;
|
|
1623
|
-
this.irSession = null;
|
|
1624
|
-
session.end();
|
|
1625
|
-
session.flushNotify();
|
|
1626
|
-
const parsedEnvelope = sessionEnvelope(session);
|
|
1627
|
-
this.irEnvelope = canonicalizeCompletedLegacyQuizEnvelope(
|
|
1628
|
-
parsedEnvelope,
|
|
1629
|
-
this.currentBlockContent
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** End the open region; its final envelope is kept for the block it belongs to. */
|
|
187
|
+
closeRegion() {
|
|
188
|
+
const region = this.region;
|
|
189
|
+
if (!region) return null;
|
|
190
|
+
this.region = null;
|
|
191
|
+
region.session.end();
|
|
192
|
+
region.session.flushNotify();
|
|
193
|
+
const envelope = canonicalizeCompletedLegacyQuizEnvelope(
|
|
194
|
+
sessionEnvelope(region.session),
|
|
195
|
+
region.fed
|
|
1630
196
|
);
|
|
1631
|
-
this.
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
if (
|
|
1669
|
-
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
if (!envelopeMatchesParsedSource(envelope, parsed)) {
|
|
1684
|
-
captureError({
|
|
1685
|
-
source: "content-ir",
|
|
1686
|
-
message: `stream parity mismatch for kind "${envelope.root.kind || "raw"}" \u2014 envelope != JSON.parse(content)`,
|
|
1687
|
-
relation: envelope.root.kind || "raw",
|
|
1688
|
-
raw: { fingerprint: envelope.fingerprint }
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
/** Dispose all sessions this stream opened (finalize-time cleanup). */
|
|
1693
|
-
irDisposeAll() {
|
|
1694
|
-
for (const identity of this.irIdentities) {
|
|
1695
|
-
disposeParseSession(identity);
|
|
1696
|
-
}
|
|
1697
|
-
this.irIdentities = [];
|
|
1698
|
-
}
|
|
1699
|
-
// ── Block lifecycle helpers ─────────────────────────────────────────
|
|
1700
|
-
appendToCurrentBlock(line) {
|
|
1701
|
-
this.suppressEmptyTrailingSlot = false;
|
|
1702
|
-
if (this.currentBlockLineCount === 0) {
|
|
1703
|
-
this.currentBlockContent = line;
|
|
1704
|
-
} else {
|
|
1705
|
-
this.currentBlockContent += "\n" + line;
|
|
1706
|
-
}
|
|
1707
|
-
this.currentBlockLineCount++;
|
|
1708
|
-
}
|
|
1709
|
-
closeCurrentBlock(dispatch) {
|
|
1710
|
-
if (this.irSession) {
|
|
1711
|
-
this.irCloseRegion();
|
|
1712
|
-
}
|
|
1713
|
-
if (!this.currentBlockContent.trim()) {
|
|
1714
|
-
if (this.currentBlockEmitted) {
|
|
1715
|
-
this.retractCurrentBlock(dispatch);
|
|
1716
|
-
this.currentBlockEmitted = false;
|
|
1717
|
-
}
|
|
1718
|
-
this.irEnvelope = null;
|
|
1719
|
-
this.irRegionIsXmlBody = false;
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
if (this.subState.kind === "xml_tag" && !this.subState.isAttrXml && this.xmlClosedCleanly) {
|
|
1723
|
-
const openTag = this.subState.openingTagText;
|
|
1724
|
-
const tagName = this.subState.tagName;
|
|
1725
|
-
const xmlEnvelope = envelopeForCompletedXmlRegion(
|
|
1726
|
-
tagName,
|
|
1727
|
-
openTag && openTag !== `<${tagName}>` ? `${openTag}
|
|
1728
|
-
${this.currentBlockContent}
|
|
1729
|
-
</${tagName}>` : this.currentBlockContent
|
|
1730
|
-
);
|
|
1731
|
-
if (xmlEnvelope) this.irEnvelope = xmlEnvelope;
|
|
1732
|
-
}
|
|
1733
|
-
if (this.subState.kind === "code_fence" && this.fenceClosedCleanly) {
|
|
1734
|
-
const normalizedLang = normalizeCodeLanguage(this.subState.language);
|
|
1735
|
-
if (normalizedLang) {
|
|
1736
|
-
const fenceEnvelope = envelopeForCompletedFenceRegion(
|
|
1737
|
-
normalizedLang,
|
|
1738
|
-
this.currentBlockContent
|
|
1739
|
-
);
|
|
1740
|
-
if (fenceEnvelope) this.irEnvelope = fenceEnvelope;
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
if (this.emitRecoveredEmbeddedKindBlocks(dispatch)) {
|
|
1744
|
-
this.fenceClosedCleanly = false;
|
|
1745
|
-
this.xmlClosedCleanly = false;
|
|
1746
|
-
this.irEnvelope = null;
|
|
1747
|
-
this.irRegionIsXmlBody = false;
|
|
1748
|
-
this.currentBlockContent = "";
|
|
1749
|
-
this.currentBlockLineCount = 0;
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
this.fenceClosedCleanly = false;
|
|
1753
|
-
this.xmlClosedCleanly = false;
|
|
1754
|
-
this.emitCurrentBlock(dispatch, "complete");
|
|
1755
|
-
this.irEnvelope = null;
|
|
1756
|
-
this.irRegionIsXmlBody = false;
|
|
1757
|
-
this.currentBlockContent = "";
|
|
1758
|
-
this.currentBlockLineCount = 0;
|
|
1759
|
-
}
|
|
1760
|
-
/**
|
|
1761
|
-
* A surrounding Markdown/code/XML classification never outranks an inner
|
|
1762
|
-
* complete object carrying `__kind`. Replace the formerly monolithic Redux
|
|
1763
|
-
* block with lossless container fragments plus independently routable JSON
|
|
1764
|
-
* blocks. The first fragment reuses the already-streamed block id; later
|
|
1765
|
-
* fragments take consecutive ids, so the next ordinary openBlock continues
|
|
1766
|
-
* at the correct document position.
|
|
1767
|
-
*/
|
|
1768
|
-
emitRecoveredEmbeddedKindBlocks(dispatch) {
|
|
1769
|
-
if (this.currentBlockType === "artifact" || this.irEnvelope?.root.kind) {
|
|
1770
|
-
return false;
|
|
1771
|
-
}
|
|
1772
|
-
if (this.genericXmlRecoverySuppressed || this.currentBlockType === "text" && this.currentBlockContent.split("\n").some(isUnclosedGenericXmlOpening)) {
|
|
1773
|
-
return false;
|
|
1774
|
-
}
|
|
1775
|
-
const recoverySource = this.subState.kind === "xml_tag" && !this.subState.isAttrXml ? this.currentBlockContent.trim() : this.currentBlockContent;
|
|
1776
|
-
const fence = this.subState.kind === "code_fence" ? this.subState : null;
|
|
1777
|
-
if (fence && this.currentBlockType === "code" && !isJsonFenceLanguage(fence.language)) {
|
|
1778
|
-
return false;
|
|
1779
|
-
}
|
|
1780
|
-
const genericXml = this.subState.kind === "generic_xml";
|
|
1781
|
-
const pieces = splitAroundEmbeddedKindJson(recoverySource, {
|
|
1782
|
-
excludeLiteralContexts: genericXml,
|
|
1783
|
-
liftJsonFences: genericXml,
|
|
1784
|
-
// Prose, tables and sections: an inline code span is quoted source.
|
|
1785
|
-
// A JSON fence's body is JSON — a backtick inside a string is no span.
|
|
1786
|
-
excludeQuotedSource: !genericXml && !(fence && isJsonFenceLanguage(fence.language))
|
|
1787
|
-
});
|
|
1788
|
-
if (pieces.length === 1 && pieces[0]?.type === "container") return false;
|
|
1789
|
-
const recoveredXmlContainer = this.currentBlockType === "text" && isCompleteUnrecognizedXmlContainer(recoverySource);
|
|
1790
|
-
const containerType = recoveredXmlContainer ? "code" : this.currentBlockType;
|
|
1791
|
-
const containerData = recoveredXmlContainer ? { language: "xml" } : this.buildBlockData();
|
|
1792
|
-
const containerMetadata = genericXml || recoveredXmlContainer ? { genericXmlContainer: true } : void 0;
|
|
1793
|
-
let emitted = 0;
|
|
1794
|
-
let followsKind = false;
|
|
1795
|
-
const splitStart = this.currentBlockIndex;
|
|
1796
|
-
const splitIndices = [];
|
|
1797
|
-
const isWrapperSplit = pieces.some((piece) => piece.type === "residual");
|
|
1798
|
-
for (const piece of pieces) {
|
|
1799
|
-
if (piece.type === "chrome") continue;
|
|
1800
|
-
const isKind = piece.type === "kind";
|
|
1801
|
-
const isResidual = piece.type === "residual";
|
|
1802
|
-
const content = isResidual ? piece.json : isKind ? piece.content : normalizeRecoveredContainerPiece(piece.content, containerType, followsKind);
|
|
1803
|
-
followsKind = isKind;
|
|
1804
|
-
if (!content) continue;
|
|
1805
|
-
if (emitted > 0) this.currentBlockIndex++;
|
|
1806
|
-
const asJson = isKind || isResidual;
|
|
1807
|
-
const block = {
|
|
1808
|
-
blockId: this.currentBlockId,
|
|
1809
|
-
blockIndex: this.currentBlockIndex,
|
|
1810
|
-
type: asJson ? "code" : containerType,
|
|
1811
|
-
status: "complete",
|
|
1812
|
-
content,
|
|
1813
|
-
data: asJson ? { language: "json" } : containerData,
|
|
1814
|
-
metadata: asJson ? withIrEnvelope(content, void 0) : containerMetadata
|
|
1815
|
-
};
|
|
1816
|
-
dispatch(this.upsertAction({ requestId: this.requestId, block }));
|
|
1817
|
-
splitIndices.push(this.currentBlockIndex);
|
|
1818
|
-
emitted++;
|
|
1819
|
-
this.emitCount++;
|
|
1820
|
-
}
|
|
1821
|
-
if (isWrapperSplit && emitted > 0) {
|
|
1822
|
-
this.wrapperSplits.set(splitStart, { indices: splitIndices, source: recoverySource });
|
|
1823
|
-
}
|
|
1824
|
-
this.currentBlockEmitted = emitted > 0;
|
|
1825
|
-
return emitted > 0;
|
|
197
|
+
this.closedEnvelopes.set(region.index, envelope);
|
|
198
|
+
return envelope;
|
|
199
|
+
}
|
|
200
|
+
dropRegion() {
|
|
201
|
+
if (!this.region) return;
|
|
202
|
+
disposeParseSession(this.region.identity);
|
|
203
|
+
this.region = null;
|
|
204
|
+
}
|
|
205
|
+
disposeAll() {
|
|
206
|
+
this.region = null;
|
|
207
|
+
for (const identity of this.identities) disposeParseSession(identity);
|
|
208
|
+
this.identities = [];
|
|
209
|
+
}
|
|
210
|
+
/** `{` (or `[{`) with no key yet may become a JSON region next frame: draw nothing yet. */
|
|
211
|
+
holdsNascentJson(block) {
|
|
212
|
+
if (block.type !== "text" && block.type !== "code") return false;
|
|
213
|
+
const lines = block.content.split("\n");
|
|
214
|
+
const lastLine = lines[lines.length - 1].trimStart();
|
|
215
|
+
if (block.type === "text" && lines.length > 1) return false;
|
|
216
|
+
return /^\[?\s*\{[^"]*$|^\[?\s*\{\s*"[^"]*$/.test(lastLine) || /^\[\s*$/.test(lastLine);
|
|
217
|
+
}
|
|
218
|
+
frontMatterStillOpen(text) {
|
|
219
|
+
const match = FRONT_MATTER_OPEN.exec(text);
|
|
220
|
+
const fence = match[1];
|
|
221
|
+
const rest = text.slice(match[0].length).split(/\r?\n/);
|
|
222
|
+
return !rest.slice(0, -1).some((line) => line.trim() === fence || fence === "---" && line.trim() === "...");
|
|
223
|
+
}
|
|
224
|
+
emit(dispatch, index, block, status, metadata) {
|
|
225
|
+
const payload = {
|
|
226
|
+
blockId: `client_block_${index}`,
|
|
227
|
+
blockIndex: index,
|
|
228
|
+
type: block.type,
|
|
229
|
+
status,
|
|
230
|
+
content: block.content || null,
|
|
231
|
+
data: this.dataFor(block),
|
|
232
|
+
metadata
|
|
233
|
+
};
|
|
234
|
+
if (block.language && !payload.data?.language) payload.language = block.language;
|
|
235
|
+
const signature = this.signatureOf(payload);
|
|
236
|
+
if (this.signatures.get(index) === signature) return;
|
|
237
|
+
this.signatures.set(index, signature);
|
|
238
|
+
dispatch(this.upsertAction({ requestId: this.requestId, block: payload }));
|
|
239
|
+
}
|
|
240
|
+
/** `data` as the renderer has always read it: a code block's language, a media block's link. */
|
|
241
|
+
dataFor(block) {
|
|
242
|
+
if (MEDIA_BLOCK_TYPES.has(block.type) && block.src) {
|
|
243
|
+
return { src: block.src, alt: block.alt ?? "" };
|
|
244
|
+
}
|
|
245
|
+
if (block.type === "code" && block.language) return { language: block.language };
|
|
246
|
+
return null;
|
|
1826
247
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
248
|
+
signatureOf(payload) {
|
|
249
|
+
const { [IR_ENVELOPE_KEY]: envelope, ...rest } = payload.metadata ?? {};
|
|
250
|
+
const ir = envelope;
|
|
251
|
+
return JSON.stringify([
|
|
252
|
+
payload.type,
|
|
253
|
+
payload.status,
|
|
254
|
+
payload.content,
|
|
255
|
+
payload.data,
|
|
256
|
+
payload.language,
|
|
257
|
+
rest,
|
|
258
|
+
ir ? [ir.fingerprint, ir.root.status, ir.root.kind] : null
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
retract(dispatch, index) {
|
|
1834
262
|
const block = {
|
|
1835
|
-
blockId:
|
|
1836
|
-
blockIndex:
|
|
1837
|
-
type:
|
|
263
|
+
blockId: `client_block_${index}`,
|
|
264
|
+
blockIndex: index,
|
|
265
|
+
type: "text",
|
|
1838
266
|
status: "complete",
|
|
1839
267
|
content: null,
|
|
1840
268
|
data: null,
|
|
1841
269
|
metadata: void 0
|
|
1842
270
|
};
|
|
271
|
+
const signature = this.signatureOf(block);
|
|
272
|
+
if (this.signatures.get(index) === signature) return;
|
|
273
|
+
this.signatures.set(index, signature);
|
|
1843
274
|
dispatch(this.upsertAction({ requestId: this.requestId, block }));
|
|
1844
275
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
this.
|
|
1848
|
-
this.
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
emitCurrentBlock(dispatch, status) {
|
|
1856
|
-
if (status === "streaming" && this.subState.kind === "frontmatter") return;
|
|
1857
|
-
let content = this.currentBlockContent;
|
|
1858
|
-
if (status === "streaming" && this.pendingLineFragment) {
|
|
1859
|
-
if (this.subState.kind === "none" || this.subState.kind === "code_fence" || this.subState.kind === "bare_json" || this.subState.kind === "generic_xml") {
|
|
1860
|
-
const holdNascentJson = this.subState.kind === "none" && !this.kindLineIsProse && (!this.currentBlockContent && this.pendingLineFragment.trimStart().startsWith("{") || // A nascent array of objects waits for its first element's first
|
|
1861
|
-
// key the same way (A6) — a kindless one shows once it is known.
|
|
1862
|
-
/^\s*\[\s*(?:\{|$)/.test(this.pendingLineFragment) && jsonKindSignal(this.pendingLineFragment) !== "not_kind");
|
|
1863
|
-
if (!holdNascentJson) {
|
|
1864
|
-
content = content ? content + "\n" + this.pendingLineFragment : this.pendingLineFragment;
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
if (this.currentBlockType === "text") {
|
|
1869
|
-
content = content.trimEnd();
|
|
1870
|
-
}
|
|
1871
|
-
if (!content && status === "streaming") return;
|
|
1872
|
-
if (!content && status === "complete" && this.suppressEmptyTrailingSlot)
|
|
1873
|
-
return;
|
|
1874
|
-
this.emitCount++;
|
|
1875
|
-
const unclosedXmlOpening = this.currentBlockType === "text" && isUnclosedGenericXmlOpening(content);
|
|
1876
|
-
const block = {
|
|
1877
|
-
blockId: this.currentBlockId,
|
|
1878
|
-
blockIndex: this.currentBlockIndex,
|
|
1879
|
-
type: unclosedXmlOpening ? "code" : this.currentBlockType,
|
|
1880
|
-
status,
|
|
1881
|
-
content: content || null,
|
|
1882
|
-
data: unclosedXmlOpening ? { language: "xml" } : this.buildBlockData(),
|
|
1883
|
-
metadata: unclosedXmlOpening ? { isComplete: false, genericXmlContainer: true } : this.buildBlockMetadata(status, content)
|
|
1884
|
-
};
|
|
1885
|
-
dispatch(this.upsertAction({ requestId: this.requestId, block }));
|
|
1886
|
-
this.currentBlockEmitted = true;
|
|
1887
|
-
}
|
|
1888
|
-
/**
|
|
1889
|
-
* Builds metadata for the block currently being emitted.
|
|
1890
|
-
*
|
|
1891
|
-
* Critical for attribute-XML tags (`<decision>`, `<artifact>`, `<editor_*>`):
|
|
1892
|
-
* BlockRenderer reads `metadata.decision` / `metadata.artifactId` /
|
|
1893
|
-
* `metadata.rawXml` etc. and falls back to raw-markdown rendering when those
|
|
1894
|
-
* are missing — which is exactly the bug we're fixing (decision tags
|
|
1895
|
-
* appearing as plain text mid-stream because metadata was hard-coded to
|
|
1896
|
-
* `undefined` here previously).
|
|
1897
|
-
*
|
|
1898
|
-
* For simple XML tags we emit `{ isComplete, rawXml }` so consumers (loaders,
|
|
1899
|
-
* inline-edit flows) can both gate on completion and round-trip the source.
|
|
1900
|
-
*/
|
|
1901
|
-
buildBlockMetadata(status, emittedContent) {
|
|
1902
|
-
const base = this.subState.kind === "generic_xml" ? (
|
|
1903
|
-
// An XML TAG is structure (ruling (b), round 3): its card draws kinds as kinds.
|
|
1904
|
-
{ genericXmlContainer: true }
|
|
1905
|
-
) : this.buildXmlBlockMetadata(status, emittedContent);
|
|
1906
|
-
if (this.currentBlockType === "matrx") {
|
|
1907
|
-
return base;
|
|
1908
|
-
}
|
|
1909
|
-
const envelope = this.irEnvelope ?? sessionEnvelope(this.irSession);
|
|
1910
|
-
if (envelope) {
|
|
1911
|
-
return { ...base ?? {}, [IR_ENVELOPE_KEY]: envelope };
|
|
1912
|
-
}
|
|
1913
|
-
return base;
|
|
1914
|
-
}
|
|
1915
|
-
buildXmlBlockMetadata(status, emittedContent) {
|
|
1916
|
-
if (this.subState.kind !== "xml_tag") return void 0;
|
|
1917
|
-
const { tagName, attributes, isAttrXml } = this.subState;
|
|
1918
|
-
const isComplete = status === "complete" && !this.regionContinuesAfterClose;
|
|
1919
|
-
const rawXml = emittedContent;
|
|
1920
|
-
if (tagName === "decision") {
|
|
1921
|
-
const decisionId = this.currentBlockId;
|
|
1922
|
-
const prompt = attributes.prompt || "Make a selection";
|
|
1923
|
-
const options = parseDecisionOptions(rawXml, decisionId);
|
|
1924
|
-
const decision = { id: decisionId, prompt, options };
|
|
1925
|
-
return { isComplete, decision, rawXml };
|
|
1926
|
-
}
|
|
1927
|
-
if (tagName === "artifact") {
|
|
1928
|
-
const artifactId = attributes.id || `artifact-client-${this.currentBlockIndex}`;
|
|
1929
|
-
const artifactIndex = artifactId.includes("_") ? parseInt(artifactId.split("_").pop() || "0", 10) || this.currentBlockIndex : this.currentBlockIndex;
|
|
1930
|
-
return {
|
|
1931
|
-
isComplete,
|
|
1932
|
-
artifactId,
|
|
1933
|
-
artifactIndex,
|
|
1934
|
-
artifactType: attributes.type || "text",
|
|
1935
|
-
artifactTitle: attributes.title || "",
|
|
1936
|
-
rawXml
|
|
1937
|
-
};
|
|
1938
|
-
}
|
|
1939
|
-
if (tagName === "editor_error" || tagName === "editor_code_snippet") {
|
|
1940
|
-
return {
|
|
1941
|
-
isComplete,
|
|
1942
|
-
...attributes,
|
|
1943
|
-
rawXml
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
1946
|
-
if (isAttrXml) {
|
|
1947
|
-
return { isComplete, ...attributes, rawXml };
|
|
1948
|
-
}
|
|
1949
|
-
return { isComplete, rawXml };
|
|
1950
|
-
}
|
|
1951
|
-
buildBlockData() {
|
|
1952
|
-
if ((this.currentBlockType === "audio" || this.currentBlockType === "image" || this.currentBlockType === "video") && this.pendingMediaData) {
|
|
1953
|
-
return { ...this.pendingMediaData };
|
|
1954
|
-
}
|
|
1955
|
-
if (this.subState.kind === "code_fence") {
|
|
1956
|
-
if (this.currentBlockType !== "code") return null;
|
|
1957
|
-
if (!this.subState.language && !this.subState.meta) return null;
|
|
1958
|
-
return {
|
|
1959
|
-
...this.subState.language ? { language: this.subState.language } : {},
|
|
1960
|
-
...this.subState.meta ? { [FENCE_META_KEY]: this.subState.meta } : {}
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
if (this.subState.kind === "generic_xml") {
|
|
1964
|
-
return { language: "xml" };
|
|
1965
|
-
}
|
|
1966
|
-
if (this.subState.kind === "bare_json") {
|
|
1967
|
-
if (this.currentBlockType !== "code") return null;
|
|
1968
|
-
return { language: "json" };
|
|
1969
|
-
}
|
|
1970
|
-
return null;
|
|
276
|
+
// ── Reasoning-boundary honesty ──────────────────────────────────────────
|
|
277
|
+
reportUnclosedReasoningRegion(tag, content) {
|
|
278
|
+
if (this.unclosedReasoningReported) return;
|
|
279
|
+
this.unclosedReasoningReported = true;
|
|
280
|
+
captureError({
|
|
281
|
+
source: "reasoning-leak",
|
|
282
|
+
message: `The stream ended INSIDE a <${tag}> region \u2014 its closing tag never arrived, so every token after the model stopped thinking (the whole visible ANSWER) is inside a thinking block and the assistant message renders empty behind a collapsed "Thought process". Remedy: the wrapper is per-stream state in aidream providers/reasoning_stream_state.py \u2014 a stranded open means a provider parser is sharing it across concurrent turns again.`,
|
|
283
|
+
requestId: this.requestId,
|
|
284
|
+
raw: { sample: content.slice(0, 500) }
|
|
285
|
+
});
|
|
1971
286
|
}
|
|
1972
287
|
}
|
|
1973
288
|
export {
|