@sentry/junior-dashboard 0.61.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/components/TranscriptText.d.ts +1 -0
- package/dist/client/components/transcriptRenderModel.d.ts +1 -1
- package/dist/client/format.d.ts +20 -5
- package/dist/client.js +31 -25
- package/package.json +2 -2
- package/src/client/components/TranscriptText.tsx +5 -2
- package/src/client/components/TranscriptTurn.tsx +8 -13
- package/src/client/components/transcriptRenderModel.ts +8 -4
- package/src/client/format.ts +40 -16
|
@@ -26,5 +26,5 @@ export declare function groupTranscriptMessages(messages: TranscriptMessage[]):
|
|
|
26
26
|
/** Build the plain-text clipboard/raw view for one transcript message. */
|
|
27
27
|
export declare function messageRawText(message: TranscriptMessage): string;
|
|
28
28
|
/** Count rendered rows so structured transcript expansion opens the newest node. */
|
|
29
|
-
export declare function countRenderedTranscriptChildren(part: RenderedTranscriptPart): number;
|
|
29
|
+
export declare function countRenderedTranscriptChildren(part: RenderedTranscriptPart, role?: string): number;
|
|
30
30
|
export {};
|
package/dist/client/format.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare function formatMessageTimestamp(value: number | undefined): strin
|
|
|
18
18
|
export declare function formatMessageOffset(turn: ConversationTurn, value: number | undefined): string | undefined;
|
|
19
19
|
/** Format byte counts in lowercase compact units for transcript metadata. */
|
|
20
20
|
export declare function formatBytes(value: number | undefined): string;
|
|
21
|
+
/** Normalized role category for transcript messages. */
|
|
22
|
+
export type TranscriptRoleKind = "assistant" | "other" | "system" | "tool" | "user";
|
|
23
|
+
/** Normalize a raw transcript role string to a canonical kind. */
|
|
24
|
+
export declare function transcriptRoleKind(role: string): TranscriptRoleKind;
|
|
21
25
|
/** Count visible or redacted message records for a turn. */
|
|
22
26
|
export declare function turnMessageCount(turn: ConversationTurn): number;
|
|
23
27
|
/** Count tool calls from visible transcripts or safe redacted metadata. */
|
|
@@ -59,13 +63,24 @@ export declare function detectLanguage(text: string): BundledLanguage;
|
|
|
59
63
|
*/
|
|
60
64
|
export declare function detectOutputLanguage(text: string): BundledLanguage;
|
|
61
65
|
/**
|
|
62
|
-
* Decide whether a
|
|
63
|
-
*
|
|
64
|
-
*
|
|
66
|
+
* Decide whether a block can use the interactive markup renderer.
|
|
67
|
+
* Only xml/html language blocks qualify; fenced is tracked as metadata but
|
|
68
|
+
* does not gate eligibility — caller controls whether XML detection runs.
|
|
65
69
|
*/
|
|
66
70
|
export declare function canRenderStructuredMarkup(block: CodeBlock): boolean;
|
|
67
|
-
/**
|
|
68
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Parse markdown into renderable code blocks while preserving plain text blocks.
|
|
73
|
+
*
|
|
74
|
+
* `outputOnly` (default `false`): when `true`, prose sections use
|
|
75
|
+
* `detectOutputLanguage` (json or markdown only — no xml/html heuristics).
|
|
76
|
+
* Use `outputOnly: true` for LLM-generated text (assistant messages) to
|
|
77
|
+
* prevent Slack autolinks and HTML snippets from triggering the XML tree
|
|
78
|
+
* renderer. Leave `false` (default) for user/system messages that may
|
|
79
|
+
* contain genuine XML runtime context.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseMarkdownBlocks(text: string, opts?: {
|
|
82
|
+
outputOnly?: boolean;
|
|
83
|
+
}): CodeBlock[];
|
|
69
84
|
/** Parse XML/HTML-ish fragments for the collapsible transcript renderer. */
|
|
70
85
|
export declare function parseMarkupNodes(code: string, language: BundledLanguage): MarkupNode[];
|
|
71
86
|
/** Group recent turn summaries into conversation rows. */
|
package/dist/client.js
CHANGED
|
@@ -30836,9 +30836,13 @@ function formatBytes(value) {
|
|
|
30836
30836
|
function transcriptSource(turn) {
|
|
30837
30837
|
return turn.transcriptAvailable ? turn.transcript : turn.transcriptMetadata ?? [];
|
|
30838
30838
|
}
|
|
30839
|
-
function
|
|
30839
|
+
function transcriptRoleKind(role) {
|
|
30840
30840
|
const normalized = role.toLowerCase();
|
|
30841
|
-
|
|
30841
|
+
if (normalized === "assistant") return "assistant";
|
|
30842
|
+
if (normalized === "user") return "user";
|
|
30843
|
+
if (normalized === "system") return "system";
|
|
30844
|
+
if (normalized.includes("tool")) return "tool";
|
|
30845
|
+
return "other";
|
|
30842
30846
|
}
|
|
30843
30847
|
function hasTextPart(message) {
|
|
30844
30848
|
return message.parts.some((part) => {
|
|
@@ -30848,8 +30852,9 @@ function hasTextPart(message) {
|
|
|
30848
30852
|
});
|
|
30849
30853
|
}
|
|
30850
30854
|
function isConversationMessage(message) {
|
|
30851
|
-
|
|
30852
|
-
if (
|
|
30855
|
+
const kind = transcriptRoleKind(message.role);
|
|
30856
|
+
if (kind !== "user" && kind !== "assistant") return false;
|
|
30857
|
+
if (kind === "assistant") return hasTextPart(message);
|
|
30853
30858
|
return message.parts.length > 0;
|
|
30854
30859
|
}
|
|
30855
30860
|
function turnMessageCount(turn) {
|
|
@@ -31057,9 +31062,10 @@ function detectOutputLanguage(text2) {
|
|
|
31057
31062
|
return "markdown";
|
|
31058
31063
|
}
|
|
31059
31064
|
function canRenderStructuredMarkup(block) {
|
|
31060
|
-
return block.
|
|
31065
|
+
return block.language === "xml" || block.language === "html";
|
|
31061
31066
|
}
|
|
31062
|
-
function parseMarkdownBlocks(text2) {
|
|
31067
|
+
function parseMarkdownBlocks(text2, opts = {}) {
|
|
31068
|
+
const detectProse = opts.outputOnly ? detectOutputLanguage : detectLanguage;
|
|
31063
31069
|
const blocks = [];
|
|
31064
31070
|
const fence = /```([A-Za-z0-9_-]+)?\n([\s\S]*?)```/g;
|
|
31065
31071
|
let cursor = 0;
|
|
@@ -31067,7 +31073,7 @@ function parseMarkdownBlocks(text2) {
|
|
|
31067
31073
|
while (match = fence.exec(text2)) {
|
|
31068
31074
|
const prose = text2.slice(cursor, match.index).trim();
|
|
31069
31075
|
if (prose) {
|
|
31070
|
-
const language3 =
|
|
31076
|
+
const language3 = detectProse(prose);
|
|
31071
31077
|
blocks.push({ code: formatCodeBlock(prose, language3), fenced: false, language: language3 });
|
|
31072
31078
|
}
|
|
31073
31079
|
const language2 = normalizeLanguage(match[1]);
|
|
@@ -31080,11 +31086,11 @@ function parseMarkdownBlocks(text2) {
|
|
|
31080
31086
|
}
|
|
31081
31087
|
const rest = text2.slice(cursor).trim();
|
|
31082
31088
|
if (rest) {
|
|
31083
|
-
const language2 =
|
|
31089
|
+
const language2 = detectProse(rest);
|
|
31084
31090
|
blocks.push({ code: formatCodeBlock(rest, language2), fenced: false, language: language2 });
|
|
31085
31091
|
}
|
|
31086
31092
|
if (blocks.length > 0) return blocks;
|
|
31087
|
-
const language =
|
|
31093
|
+
const language = detectProse(text2);
|
|
31088
31094
|
return [{ code: formatCodeBlock(text2, language), fenced: false, language }];
|
|
31089
31095
|
}
|
|
31090
31096
|
function parseMarkupNodes(code, language) {
|
|
@@ -54894,7 +54900,9 @@ function toolHeaderClass(interactive) {
|
|
|
54894
54900
|
// src/client/components/TranscriptText.tsx
|
|
54895
54901
|
var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
|
|
54896
54902
|
function TranscriptText(props) {
|
|
54897
|
-
const blocks = parseMarkdownBlocks(props.text
|
|
54903
|
+
const blocks = parseMarkdownBlocks(props.text, {
|
|
54904
|
+
outputOnly: transcriptRoleKind(props.role ?? "") === "assistant"
|
|
54905
|
+
});
|
|
54898
54906
|
let seenChildren = props.firstChildIndex;
|
|
54899
54907
|
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "grid min-w-0 gap-2", children: blocks.map((block, index) => {
|
|
54900
54908
|
const firstChildIndex = seenChildren;
|
|
@@ -55195,15 +55203,18 @@ function messageRawText(message) {
|
|
|
55195
55203
|
return stringifyPartValue(part.output ?? part.input ?? part.text ?? part);
|
|
55196
55204
|
}).filter((part) => part.trim().length > 0).join("\n\n");
|
|
55197
55205
|
}
|
|
55198
|
-
function countTextRenderedChildren(text2) {
|
|
55199
|
-
return parseMarkdownBlocks(text2).reduce((count, block) => {
|
|
55206
|
+
function countTextRenderedChildren(text2, outputOnly) {
|
|
55207
|
+
return parseMarkdownBlocks(text2, { outputOnly }).reduce((count, block) => {
|
|
55200
55208
|
return count + countStructuredBlockChildren(block);
|
|
55201
55209
|
}, 0);
|
|
55202
55210
|
}
|
|
55203
|
-
function countRenderedTranscriptChildren(part) {
|
|
55211
|
+
function countRenderedTranscriptChildren(part, role) {
|
|
55204
55212
|
if (part.kind === "tool") return 1;
|
|
55205
55213
|
if (part.part.type === "text") {
|
|
55206
|
-
return countTextRenderedChildren(
|
|
55214
|
+
return countTextRenderedChildren(
|
|
55215
|
+
part.part.text ?? "",
|
|
55216
|
+
transcriptRoleKind(role ?? "") === "assistant"
|
|
55217
|
+
);
|
|
55207
55218
|
}
|
|
55208
55219
|
return 1;
|
|
55209
55220
|
}
|
|
@@ -55240,14 +55251,6 @@ function turnMarkerClass(status) {
|
|
|
55240
55251
|
status === "idle" && "border-[#beaaff]/70 bg-[#beaaff]/50"
|
|
55241
55252
|
);
|
|
55242
55253
|
}
|
|
55243
|
-
function transcriptRoleKind(role) {
|
|
55244
|
-
const normalized = role.toLowerCase();
|
|
55245
|
-
if (normalized === "assistant") return "assistant";
|
|
55246
|
-
if (normalized === "user") return "user";
|
|
55247
|
-
if (normalized === "system") return "system";
|
|
55248
|
-
if (normalized.includes("tool")) return "tool";
|
|
55249
|
-
return "other";
|
|
55250
|
-
}
|
|
55251
55254
|
function transcriptRoleLabel(role, turn) {
|
|
55252
55255
|
const kind = transcriptRoleKind(role);
|
|
55253
55256
|
if (kind === "assistant") return "Junior";
|
|
@@ -55450,8 +55453,9 @@ function TranscriptMessageView(props) {
|
|
|
55450
55453
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
55451
55454
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
55452
55455
|
const rawText = messageRawText(props.message);
|
|
55456
|
+
const role = props.message.role;
|
|
55453
55457
|
const totalRenderedChildren = renderedParts.reduce(
|
|
55454
|
-
(count, part) => count + countRenderedTranscriptChildren(part),
|
|
55458
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
55455
55459
|
0
|
|
55456
55460
|
);
|
|
55457
55461
|
let seenRenderedChildren = 0;
|
|
@@ -55478,13 +55482,14 @@ function TranscriptMessageView(props) {
|
|
|
55478
55482
|
}
|
|
55479
55483
|
) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "grid min-w-0 gap-2", children: renderedParts.map((part, index) => {
|
|
55480
55484
|
const firstChildIndex = seenRenderedChildren;
|
|
55481
|
-
seenRenderedChildren += countRenderedTranscriptChildren(part);
|
|
55485
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
55482
55486
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
55483
55487
|
TranscriptPartView,
|
|
55484
55488
|
{
|
|
55485
55489
|
firstChildIndex,
|
|
55486
55490
|
lastChildIndex: totalRenderedChildren - 1,
|
|
55487
|
-
part
|
|
55491
|
+
part,
|
|
55492
|
+
role
|
|
55488
55493
|
},
|
|
55489
55494
|
index
|
|
55490
55495
|
);
|
|
@@ -55504,6 +55509,7 @@ function TranscriptPartView(props) {
|
|
|
55504
55509
|
{
|
|
55505
55510
|
firstChildIndex: props.firstChildIndex,
|
|
55506
55511
|
lastChildIndex: props.lastChildIndex,
|
|
55512
|
+
role: props.role,
|
|
55507
55513
|
text: part.text ?? ""
|
|
55508
55514
|
}
|
|
55509
55515
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"react-router": "^7.16.0",
|
|
40
40
|
"recharts": "^3.8.1",
|
|
41
41
|
"shiki": "4.1.0",
|
|
42
|
-
"@sentry/junior": "0.
|
|
42
|
+
"@sentry/junior": "0.62.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@tailwindcss/cli": "^4.3.0",
|
|
@@ -3,15 +3,18 @@ import {
|
|
|
3
3
|
HighlightedCode,
|
|
4
4
|
StructuredMarkup,
|
|
5
5
|
} from "../code";
|
|
6
|
-
import { canRenderStructuredMarkup, parseMarkdownBlocks } from "../format";
|
|
6
|
+
import { canRenderStructuredMarkup, parseMarkdownBlocks, transcriptRoleKind } from "../format";
|
|
7
7
|
|
|
8
8
|
/** Render transcript markdown/code blocks with structured markup expansion. */
|
|
9
9
|
export function TranscriptText(props: {
|
|
10
10
|
firstChildIndex: number;
|
|
11
11
|
lastChildIndex: number;
|
|
12
|
+
role?: string;
|
|
12
13
|
text: string;
|
|
13
14
|
}) {
|
|
14
|
-
const blocks = parseMarkdownBlocks(props.text
|
|
15
|
+
const blocks = parseMarkdownBlocks(props.text, {
|
|
16
|
+
outputOnly: transcriptRoleKind(props.role ?? "") === "assistant",
|
|
17
|
+
});
|
|
15
18
|
let seenChildren = props.firstChildIndex;
|
|
16
19
|
|
|
17
20
|
return (
|
|
@@ -4,6 +4,8 @@ import { HighlightedCode } from "../code";
|
|
|
4
4
|
import {
|
|
5
5
|
detectLanguage,
|
|
6
6
|
detectOutputLanguage,
|
|
7
|
+
transcriptRoleKind,
|
|
8
|
+
type TranscriptRoleKind,
|
|
7
9
|
formatBytes,
|
|
8
10
|
formatMessageOffset,
|
|
9
11
|
formatMessageTimestamp,
|
|
@@ -72,17 +74,6 @@ function turnMarkerClass(
|
|
|
72
74
|
);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
type TranscriptRoleKind = "assistant" | "other" | "system" | "tool" | "user";
|
|
76
|
-
|
|
77
|
-
function transcriptRoleKind(role: string): TranscriptRoleKind {
|
|
78
|
-
const normalized = role.toLowerCase();
|
|
79
|
-
if (normalized === "assistant") return "assistant";
|
|
80
|
-
if (normalized === "user") return "user";
|
|
81
|
-
if (normalized === "system") return "system";
|
|
82
|
-
if (normalized.includes("tool")) return "tool";
|
|
83
|
-
return "other";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
77
|
function transcriptRoleLabel(role: string, turn: ConversationTurn): string {
|
|
87
78
|
const kind = transcriptRoleKind(role);
|
|
88
79
|
if (kind === "assistant") return "Junior";
|
|
@@ -376,8 +367,9 @@ function TranscriptMessageView(props: {
|
|
|
376
367
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
377
368
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
378
369
|
const rawText = messageRawText(props.message);
|
|
370
|
+
const role = props.message.role;
|
|
379
371
|
const totalRenderedChildren = renderedParts.reduce(
|
|
380
|
-
(count, part) => count + countRenderedTranscriptChildren(part),
|
|
372
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
381
373
|
0,
|
|
382
374
|
);
|
|
383
375
|
let seenRenderedChildren = 0;
|
|
@@ -411,13 +403,14 @@ function TranscriptMessageView(props: {
|
|
|
411
403
|
<div className="grid min-w-0 gap-2">
|
|
412
404
|
{renderedParts.map((part, index) => {
|
|
413
405
|
const firstChildIndex = seenRenderedChildren;
|
|
414
|
-
seenRenderedChildren += countRenderedTranscriptChildren(part);
|
|
406
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
415
407
|
return (
|
|
416
408
|
<TranscriptPartView
|
|
417
409
|
firstChildIndex={firstChildIndex}
|
|
418
410
|
key={index}
|
|
419
411
|
lastChildIndex={totalRenderedChildren - 1}
|
|
420
412
|
part={part}
|
|
413
|
+
role={role}
|
|
421
414
|
/>
|
|
422
415
|
);
|
|
423
416
|
})}
|
|
@@ -431,6 +424,7 @@ function TranscriptPartView(props: {
|
|
|
431
424
|
firstChildIndex: number;
|
|
432
425
|
lastChildIndex: number;
|
|
433
426
|
part: RenderedTranscriptPart;
|
|
427
|
+
role?: string;
|
|
434
428
|
}) {
|
|
435
429
|
if (props.part.kind === "tool") {
|
|
436
430
|
return (
|
|
@@ -444,6 +438,7 @@ function TranscriptPartView(props: {
|
|
|
444
438
|
<TranscriptText
|
|
445
439
|
firstChildIndex={props.firstChildIndex}
|
|
446
440
|
lastChildIndex={props.lastChildIndex}
|
|
441
|
+
role={props.role}
|
|
447
442
|
text={part.text ?? ""}
|
|
448
443
|
/>
|
|
449
444
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { countStructuredBlockChildren } from "../code";
|
|
2
|
-
import { parseMarkdownBlocks, stringifyPartValue } from "../format";
|
|
2
|
+
import { parseMarkdownBlocks, stringifyPartValue, transcriptRoleKind } from "../format";
|
|
3
3
|
import type { TranscriptMessage, TranscriptPart } from "../types";
|
|
4
4
|
|
|
5
5
|
export type RenderedTranscriptPart =
|
|
@@ -175,8 +175,8 @@ export function messageRawText(message: TranscriptMessage): string {
|
|
|
175
175
|
.join("\n\n");
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
function countTextRenderedChildren(text: string): number {
|
|
179
|
-
return parseMarkdownBlocks(text).reduce((count, block) => {
|
|
178
|
+
function countTextRenderedChildren(text: string, outputOnly: boolean): number {
|
|
179
|
+
return parseMarkdownBlocks(text, { outputOnly }).reduce((count, block) => {
|
|
180
180
|
return count + countStructuredBlockChildren(block);
|
|
181
181
|
}, 0);
|
|
182
182
|
}
|
|
@@ -184,10 +184,14 @@ function countTextRenderedChildren(text: string): number {
|
|
|
184
184
|
/** Count rendered rows so structured transcript expansion opens the newest node. */
|
|
185
185
|
export function countRenderedTranscriptChildren(
|
|
186
186
|
part: RenderedTranscriptPart,
|
|
187
|
+
role?: string,
|
|
187
188
|
): number {
|
|
188
189
|
if (part.kind === "tool") return 1;
|
|
189
190
|
if (part.part.type === "text") {
|
|
190
|
-
return countTextRenderedChildren(
|
|
191
|
+
return countTextRenderedChildren(
|
|
192
|
+
part.part.text ?? "",
|
|
193
|
+
transcriptRoleKind(role ?? "") === "assistant",
|
|
194
|
+
);
|
|
191
195
|
}
|
|
192
196
|
return 1;
|
|
193
197
|
}
|
package/src/client/format.ts
CHANGED
|
@@ -204,9 +204,22 @@ function transcriptSource(turn: ConversationTurn) {
|
|
|
204
204
|
: (turn.transcriptMetadata ?? []);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
/** Normalized role category for transcript messages. */
|
|
208
|
+
export type TranscriptRoleKind =
|
|
209
|
+
| "assistant"
|
|
210
|
+
| "other"
|
|
211
|
+
| "system"
|
|
212
|
+
| "tool"
|
|
213
|
+
| "user";
|
|
214
|
+
|
|
215
|
+
/** Normalize a raw transcript role string to a canonical kind. */
|
|
216
|
+
export function transcriptRoleKind(role: string): TranscriptRoleKind {
|
|
208
217
|
const normalized = role.toLowerCase();
|
|
209
|
-
|
|
218
|
+
if (normalized === "assistant") return "assistant";
|
|
219
|
+
if (normalized === "user") return "user";
|
|
220
|
+
if (normalized === "system") return "system";
|
|
221
|
+
if (normalized.includes("tool")) return "tool";
|
|
222
|
+
return "other";
|
|
210
223
|
}
|
|
211
224
|
|
|
212
225
|
function hasTextPart(
|
|
@@ -222,8 +235,9 @@ function hasTextPart(
|
|
|
222
235
|
function isConversationMessage(
|
|
223
236
|
message: Pick<ConversationTurn["transcript"][number], "parts" | "role">,
|
|
224
237
|
): boolean {
|
|
225
|
-
|
|
226
|
-
if (
|
|
238
|
+
const kind = transcriptRoleKind(message.role);
|
|
239
|
+
if (kind !== "user" && kind !== "assistant") return false;
|
|
240
|
+
if (kind === "assistant") return hasTextPart(message);
|
|
227
241
|
return message.parts.length > 0;
|
|
228
242
|
}
|
|
229
243
|
|
|
@@ -530,19 +544,29 @@ export function detectOutputLanguage(text: string): BundledLanguage {
|
|
|
530
544
|
}
|
|
531
545
|
|
|
532
546
|
/**
|
|
533
|
-
* Decide whether a
|
|
534
|
-
*
|
|
535
|
-
*
|
|
547
|
+
* Decide whether a block can use the interactive markup renderer.
|
|
548
|
+
* Only xml/html language blocks qualify; fenced is tracked as metadata but
|
|
549
|
+
* does not gate eligibility — caller controls whether XML detection runs.
|
|
536
550
|
*/
|
|
537
551
|
export function canRenderStructuredMarkup(block: CodeBlock): boolean {
|
|
538
|
-
return
|
|
539
|
-
block.fenced === true &&
|
|
540
|
-
(block.language === "xml" || block.language === "html")
|
|
541
|
-
);
|
|
552
|
+
return block.language === "xml" || block.language === "html";
|
|
542
553
|
}
|
|
543
554
|
|
|
544
|
-
/**
|
|
545
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Parse markdown into renderable code blocks while preserving plain text blocks.
|
|
557
|
+
*
|
|
558
|
+
* `outputOnly` (default `false`): when `true`, prose sections use
|
|
559
|
+
* `detectOutputLanguage` (json or markdown only — no xml/html heuristics).
|
|
560
|
+
* Use `outputOnly: true` for LLM-generated text (assistant messages) to
|
|
561
|
+
* prevent Slack autolinks and HTML snippets from triggering the XML tree
|
|
562
|
+
* renderer. Leave `false` (default) for user/system messages that may
|
|
563
|
+
* contain genuine XML runtime context.
|
|
564
|
+
*/
|
|
565
|
+
export function parseMarkdownBlocks(
|
|
566
|
+
text: string,
|
|
567
|
+
opts: { outputOnly?: boolean } = {},
|
|
568
|
+
): CodeBlock[] {
|
|
569
|
+
const detectProse = opts.outputOnly ? detectOutputLanguage : detectLanguage;
|
|
546
570
|
const blocks: CodeBlock[] = [];
|
|
547
571
|
const fence = /```([A-Za-z0-9_-]+)?\n([\s\S]*?)```/g;
|
|
548
572
|
let cursor = 0;
|
|
@@ -550,7 +574,7 @@ export function parseMarkdownBlocks(text: string): CodeBlock[] {
|
|
|
550
574
|
while ((match = fence.exec(text))) {
|
|
551
575
|
const prose = text.slice(cursor, match.index).trim();
|
|
552
576
|
if (prose) {
|
|
553
|
-
const language =
|
|
577
|
+
const language = detectProse(prose);
|
|
554
578
|
blocks.push({ code: formatCodeBlock(prose, language), fenced: false, language });
|
|
555
579
|
}
|
|
556
580
|
const language = normalizeLanguage(match[1]);
|
|
@@ -563,11 +587,11 @@ export function parseMarkdownBlocks(text: string): CodeBlock[] {
|
|
|
563
587
|
}
|
|
564
588
|
const rest = text.slice(cursor).trim();
|
|
565
589
|
if (rest) {
|
|
566
|
-
const language =
|
|
590
|
+
const language = detectProse(rest);
|
|
567
591
|
blocks.push({ code: formatCodeBlock(rest, language), fenced: false, language });
|
|
568
592
|
}
|
|
569
593
|
if (blocks.length > 0) return blocks;
|
|
570
|
-
const language =
|
|
594
|
+
const language = detectProse(text);
|
|
571
595
|
return [{ code: formatCodeBlock(text, language), fenced: false, language }];
|
|
572
596
|
}
|
|
573
597
|
|