@sentry/junior-dashboard 0.60.1 → 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 +29 -4
- package/dist/client/types.d.ts +1 -0
- package/dist/client.js +50 -32
- package/package.json +2 -2
- package/src/client/code.tsx +1 -1
- package/src/client/components/TranscriptText.tsx +6 -3
- package/src/client/components/TranscriptTurn.tsx +10 -14
- package/src/client/components/transcriptRenderModel.ts +8 -4
- package/src/client/format.ts +65 -15
- package/src/client/types.ts +1 -1
|
@@ -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. */
|
|
@@ -52,10 +56,31 @@ export declare function unavailableTranscriptLabel(turn: ConversationTurn): stri
|
|
|
52
56
|
export declare function conversationPath(conversationId: string): string;
|
|
53
57
|
/** Detect the syntax highlighter language for raw transcript blocks. */
|
|
54
58
|
export declare function detectLanguage(text: string): BundledLanguage;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Detect the language for LLM text output prose: json if the text is valid
|
|
61
|
+
* JSON or JSONL, markdown otherwise. Never auto-detects XML, HTML, TypeScript,
|
|
62
|
+
* or shell — those heuristics are unreliable for rendered assistant output.
|
|
63
|
+
*/
|
|
64
|
+
export declare function detectOutputLanguage(text: string): BundledLanguage;
|
|
65
|
+
/**
|
|
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.
|
|
69
|
+
*/
|
|
70
|
+
export declare function canRenderStructuredMarkup(block: CodeBlock): boolean;
|
|
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[];
|
|
59
84
|
/** Parse XML/HTML-ish fragments for the collapsible transcript renderer. */
|
|
60
85
|
export declare function parseMarkupNodes(code: string, language: BundledLanguage): MarkupNode[];
|
|
61
86
|
/** Group recent turn summaries into conversation rows. */
|
package/dist/client/types.d.ts
CHANGED
|
@@ -140,6 +140,7 @@ export type SessionFilter = "active" | "recent" | "hung" | "failed" | "all";
|
|
|
140
140
|
export type VisualStatus = "active" | "failed" | "hung" | "idle";
|
|
141
141
|
export type CodeBlock = {
|
|
142
142
|
code: string;
|
|
143
|
+
fenced?: boolean;
|
|
143
144
|
language: BundledLanguage;
|
|
144
145
|
};
|
|
145
146
|
export type MarkupNode = {
|
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) {
|
|
@@ -31045,10 +31050,22 @@ function prettyJsonData(text2) {
|
|
|
31045
31050
|
function formatCodeBlock(code, language) {
|
|
31046
31051
|
return language === "json" ? prettyJsonData(code) ?? code : code;
|
|
31047
31052
|
}
|
|
31048
|
-
function
|
|
31049
|
-
|
|
31053
|
+
function detectOutputLanguage(text2) {
|
|
31054
|
+
const trimmed = text2.trim();
|
|
31055
|
+
if (!trimmed) return "markdown";
|
|
31056
|
+
try {
|
|
31057
|
+
JSON.parse(trimmed);
|
|
31058
|
+
return "json";
|
|
31059
|
+
} catch {
|
|
31060
|
+
}
|
|
31061
|
+
if (prettyJsonl(trimmed)) return "json";
|
|
31062
|
+
return "markdown";
|
|
31063
|
+
}
|
|
31064
|
+
function canRenderStructuredMarkup(block) {
|
|
31065
|
+
return block.language === "xml" || block.language === "html";
|
|
31050
31066
|
}
|
|
31051
|
-
function parseMarkdownBlocks(text2) {
|
|
31067
|
+
function parseMarkdownBlocks(text2, opts = {}) {
|
|
31068
|
+
const detectProse = opts.outputOnly ? detectOutputLanguage : detectLanguage;
|
|
31052
31069
|
const blocks = [];
|
|
31053
31070
|
const fence = /```([A-Za-z0-9_-]+)?\n([\s\S]*?)```/g;
|
|
31054
31071
|
let cursor = 0;
|
|
@@ -31056,24 +31073,25 @@ function parseMarkdownBlocks(text2) {
|
|
|
31056
31073
|
while (match = fence.exec(text2)) {
|
|
31057
31074
|
const prose = text2.slice(cursor, match.index).trim();
|
|
31058
31075
|
if (prose) {
|
|
31059
|
-
const language3 =
|
|
31060
|
-
blocks.push({ code: formatCodeBlock(prose, language3), language: language3 });
|
|
31076
|
+
const language3 = detectProse(prose);
|
|
31077
|
+
blocks.push({ code: formatCodeBlock(prose, language3), fenced: false, language: language3 });
|
|
31061
31078
|
}
|
|
31062
31079
|
const language2 = normalizeLanguage(match[1]);
|
|
31063
31080
|
blocks.push({
|
|
31064
31081
|
code: formatCodeBlock(match[2] ?? "", language2),
|
|
31082
|
+
fenced: true,
|
|
31065
31083
|
language: language2
|
|
31066
31084
|
});
|
|
31067
31085
|
cursor = match.index + match[0].length;
|
|
31068
31086
|
}
|
|
31069
31087
|
const rest = text2.slice(cursor).trim();
|
|
31070
31088
|
if (rest) {
|
|
31071
|
-
const language2 =
|
|
31072
|
-
blocks.push({ code: formatCodeBlock(rest, language2), language: language2 });
|
|
31089
|
+
const language2 = detectProse(rest);
|
|
31090
|
+
blocks.push({ code: formatCodeBlock(rest, language2), fenced: false, language: language2 });
|
|
31073
31091
|
}
|
|
31074
31092
|
if (blocks.length > 0) return blocks;
|
|
31075
|
-
const language =
|
|
31076
|
-
return [{ code: formatCodeBlock(text2, language), language }];
|
|
31093
|
+
const language = detectProse(text2);
|
|
31094
|
+
return [{ code: formatCodeBlock(text2, language), fenced: false, language }];
|
|
31077
31095
|
}
|
|
31078
31096
|
function parseMarkupNodes(code, language) {
|
|
31079
31097
|
const parser = new DOMParser();
|
|
@@ -54731,7 +54749,7 @@ var import_react51 = __toESM(require_react(), 1);
|
|
|
54731
54749
|
var import_react50 = __toESM(require_react(), 1);
|
|
54732
54750
|
var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
|
|
54733
54751
|
function countStructuredBlockChildren(block) {
|
|
54734
|
-
if (!canRenderStructuredMarkup(block
|
|
54752
|
+
if (!canRenderStructuredMarkup(block)) return 1;
|
|
54735
54753
|
const rootCount = parseMarkupNodes(block.code, block.language).length;
|
|
54736
54754
|
return rootCount > 0 ? rootCount : 1;
|
|
54737
54755
|
}
|
|
@@ -54882,13 +54900,15 @@ function toolHeaderClass(interactive) {
|
|
|
54882
54900
|
// src/client/components/TranscriptText.tsx
|
|
54883
54901
|
var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
|
|
54884
54902
|
function TranscriptText(props) {
|
|
54885
|
-
const blocks = parseMarkdownBlocks(props.text
|
|
54903
|
+
const blocks = parseMarkdownBlocks(props.text, {
|
|
54904
|
+
outputOnly: transcriptRoleKind(props.role ?? "") === "assistant"
|
|
54905
|
+
});
|
|
54886
54906
|
let seenChildren = props.firstChildIndex;
|
|
54887
54907
|
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "grid min-w-0 gap-2", children: blocks.map((block, index) => {
|
|
54888
54908
|
const firstChildIndex = seenChildren;
|
|
54889
54909
|
const childCount = countStructuredBlockChildren(block);
|
|
54890
54910
|
seenChildren += childCount;
|
|
54891
|
-
if (!canRenderStructuredMarkup(block
|
|
54911
|
+
if (!canRenderStructuredMarkup(block)) {
|
|
54892
54912
|
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
54893
54913
|
HighlightedCode,
|
|
54894
54914
|
{
|
|
@@ -55183,15 +55203,18 @@ function messageRawText(message) {
|
|
|
55183
55203
|
return stringifyPartValue(part.output ?? part.input ?? part.text ?? part);
|
|
55184
55204
|
}).filter((part) => part.trim().length > 0).join("\n\n");
|
|
55185
55205
|
}
|
|
55186
|
-
function countTextRenderedChildren(text2) {
|
|
55187
|
-
return parseMarkdownBlocks(text2).reduce((count, block) => {
|
|
55206
|
+
function countTextRenderedChildren(text2, outputOnly) {
|
|
55207
|
+
return parseMarkdownBlocks(text2, { outputOnly }).reduce((count, block) => {
|
|
55188
55208
|
return count + countStructuredBlockChildren(block);
|
|
55189
55209
|
}, 0);
|
|
55190
55210
|
}
|
|
55191
|
-
function countRenderedTranscriptChildren(part) {
|
|
55211
|
+
function countRenderedTranscriptChildren(part, role) {
|
|
55192
55212
|
if (part.kind === "tool") return 1;
|
|
55193
55213
|
if (part.part.type === "text") {
|
|
55194
|
-
return countTextRenderedChildren(
|
|
55214
|
+
return countTextRenderedChildren(
|
|
55215
|
+
part.part.text ?? "",
|
|
55216
|
+
transcriptRoleKind(role ?? "") === "assistant"
|
|
55217
|
+
);
|
|
55195
55218
|
}
|
|
55196
55219
|
return 1;
|
|
55197
55220
|
}
|
|
@@ -55228,14 +55251,6 @@ function turnMarkerClass(status) {
|
|
|
55228
55251
|
status === "idle" && "border-[#beaaff]/70 bg-[#beaaff]/50"
|
|
55229
55252
|
);
|
|
55230
55253
|
}
|
|
55231
|
-
function transcriptRoleKind(role) {
|
|
55232
|
-
const normalized = role.toLowerCase();
|
|
55233
|
-
if (normalized === "assistant") return "assistant";
|
|
55234
|
-
if (normalized === "user") return "user";
|
|
55235
|
-
if (normalized === "system") return "system";
|
|
55236
|
-
if (normalized.includes("tool")) return "tool";
|
|
55237
|
-
return "other";
|
|
55238
|
-
}
|
|
55239
55254
|
function transcriptRoleLabel(role, turn) {
|
|
55240
55255
|
const kind = transcriptRoleKind(role);
|
|
55241
55256
|
if (kind === "assistant") return "Junior";
|
|
@@ -55438,8 +55453,9 @@ function TranscriptMessageView(props) {
|
|
|
55438
55453
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
55439
55454
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
55440
55455
|
const rawText = messageRawText(props.message);
|
|
55456
|
+
const role = props.message.role;
|
|
55441
55457
|
const totalRenderedChildren = renderedParts.reduce(
|
|
55442
|
-
(count, part) => count + countRenderedTranscriptChildren(part),
|
|
55458
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
55443
55459
|
0
|
|
55444
55460
|
);
|
|
55445
55461
|
let seenRenderedChildren = 0;
|
|
@@ -55466,13 +55482,14 @@ function TranscriptMessageView(props) {
|
|
|
55466
55482
|
}
|
|
55467
55483
|
) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "grid min-w-0 gap-2", children: renderedParts.map((part, index) => {
|
|
55468
55484
|
const firstChildIndex = seenRenderedChildren;
|
|
55469
|
-
seenRenderedChildren += countRenderedTranscriptChildren(part);
|
|
55485
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
55470
55486
|
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
55471
55487
|
TranscriptPartView,
|
|
55472
55488
|
{
|
|
55473
55489
|
firstChildIndex,
|
|
55474
55490
|
lastChildIndex: totalRenderedChildren - 1,
|
|
55475
|
-
part
|
|
55491
|
+
part,
|
|
55492
|
+
role
|
|
55476
55493
|
},
|
|
55477
55494
|
index
|
|
55478
55495
|
);
|
|
@@ -55492,6 +55509,7 @@ function TranscriptPartView(props) {
|
|
|
55492
55509
|
{
|
|
55493
55510
|
firstChildIndex: props.firstChildIndex,
|
|
55494
55511
|
lastChildIndex: props.lastChildIndex,
|
|
55512
|
+
role: props.role,
|
|
55495
55513
|
text: part.text ?? ""
|
|
55496
55514
|
}
|
|
55497
55515
|
);
|
|
@@ -55531,7 +55549,7 @@ function ThinkingPartView(props) {
|
|
|
55531
55549
|
HighlightedCode,
|
|
55532
55550
|
{
|
|
55533
55551
|
code: rendered || "{}",
|
|
55534
|
-
language:
|
|
55552
|
+
language: detectOutputLanguage(rendered)
|
|
55535
55553
|
}
|
|
55536
55554
|
) })
|
|
55537
55555
|
]
|
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",
|
package/src/client/code.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import type { CodeBlock, MarkupNode } from "./types";
|
|
|
7
7
|
|
|
8
8
|
/** Count rendered children so transcripts can decide which markup node expands. */
|
|
9
9
|
export function countStructuredBlockChildren(block: CodeBlock): number {
|
|
10
|
-
if (!canRenderStructuredMarkup(block
|
|
10
|
+
if (!canRenderStructuredMarkup(block)) return 1;
|
|
11
11
|
const rootCount = parseMarkupNodes(block.code, block.language).length;
|
|
12
12
|
return rootCount > 0 ? rootCount : 1;
|
|
13
13
|
}
|
|
@@ -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 (
|
|
@@ -21,7 +24,7 @@ export function TranscriptText(props: {
|
|
|
21
24
|
const childCount = countStructuredBlockChildren(block);
|
|
22
25
|
seenChildren += childCount;
|
|
23
26
|
|
|
24
|
-
if (!canRenderStructuredMarkup(block
|
|
27
|
+
if (!canRenderStructuredMarkup(block)) {
|
|
25
28
|
return (
|
|
26
29
|
<HighlightedCode
|
|
27
30
|
code={block.code}
|
|
@@ -3,6 +3,9 @@ import { useState, type ClipboardEventHandler, type ReactNode } from "react";
|
|
|
3
3
|
import { HighlightedCode } from "../code";
|
|
4
4
|
import {
|
|
5
5
|
detectLanguage,
|
|
6
|
+
detectOutputLanguage,
|
|
7
|
+
transcriptRoleKind,
|
|
8
|
+
type TranscriptRoleKind,
|
|
6
9
|
formatBytes,
|
|
7
10
|
formatMessageOffset,
|
|
8
11
|
formatMessageTimestamp,
|
|
@@ -71,17 +74,6 @@ function turnMarkerClass(
|
|
|
71
74
|
);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
type TranscriptRoleKind = "assistant" | "other" | "system" | "tool" | "user";
|
|
75
|
-
|
|
76
|
-
function transcriptRoleKind(role: string): TranscriptRoleKind {
|
|
77
|
-
const normalized = role.toLowerCase();
|
|
78
|
-
if (normalized === "assistant") return "assistant";
|
|
79
|
-
if (normalized === "user") return "user";
|
|
80
|
-
if (normalized === "system") return "system";
|
|
81
|
-
if (normalized.includes("tool")) return "tool";
|
|
82
|
-
return "other";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
77
|
function transcriptRoleLabel(role: string, turn: ConversationTurn): string {
|
|
86
78
|
const kind = transcriptRoleKind(role);
|
|
87
79
|
if (kind === "assistant") return "Junior";
|
|
@@ -375,8 +367,9 @@ function TranscriptMessageView(props: {
|
|
|
375
367
|
const offset = formatMessageOffset(props.turn, props.message.timestamp);
|
|
376
368
|
const renderedParts = groupTranscriptParts(props.message.parts);
|
|
377
369
|
const rawText = messageRawText(props.message);
|
|
370
|
+
const role = props.message.role;
|
|
378
371
|
const totalRenderedChildren = renderedParts.reduce(
|
|
379
|
-
(count, part) => count + countRenderedTranscriptChildren(part),
|
|
372
|
+
(count, part) => count + countRenderedTranscriptChildren(part, role),
|
|
380
373
|
0,
|
|
381
374
|
);
|
|
382
375
|
let seenRenderedChildren = 0;
|
|
@@ -410,13 +403,14 @@ function TranscriptMessageView(props: {
|
|
|
410
403
|
<div className="grid min-w-0 gap-2">
|
|
411
404
|
{renderedParts.map((part, index) => {
|
|
412
405
|
const firstChildIndex = seenRenderedChildren;
|
|
413
|
-
seenRenderedChildren += countRenderedTranscriptChildren(part);
|
|
406
|
+
seenRenderedChildren += countRenderedTranscriptChildren(part, role);
|
|
414
407
|
return (
|
|
415
408
|
<TranscriptPartView
|
|
416
409
|
firstChildIndex={firstChildIndex}
|
|
417
410
|
key={index}
|
|
418
411
|
lastChildIndex={totalRenderedChildren - 1}
|
|
419
412
|
part={part}
|
|
413
|
+
role={role}
|
|
420
414
|
/>
|
|
421
415
|
);
|
|
422
416
|
})}
|
|
@@ -430,6 +424,7 @@ function TranscriptPartView(props: {
|
|
|
430
424
|
firstChildIndex: number;
|
|
431
425
|
lastChildIndex: number;
|
|
432
426
|
part: RenderedTranscriptPart;
|
|
427
|
+
role?: string;
|
|
433
428
|
}) {
|
|
434
429
|
if (props.part.kind === "tool") {
|
|
435
430
|
return (
|
|
@@ -443,6 +438,7 @@ function TranscriptPartView(props: {
|
|
|
443
438
|
<TranscriptText
|
|
444
439
|
firstChildIndex={props.firstChildIndex}
|
|
445
440
|
lastChildIndex={props.lastChildIndex}
|
|
441
|
+
role={props.role}
|
|
446
442
|
text={part.text ?? ""}
|
|
447
443
|
/>
|
|
448
444
|
);
|
|
@@ -494,7 +490,7 @@ function ThinkingPartView(props: { value: unknown }) {
|
|
|
494
490
|
<div className="border-t border-[#beaaff]/15 px-3 py-3">
|
|
495
491
|
<HighlightedCode
|
|
496
492
|
code={rendered || "{}"}
|
|
497
|
-
language={
|
|
493
|
+
language={detectOutputLanguage(rendered)}
|
|
498
494
|
/>
|
|
499
495
|
</div>
|
|
500
496
|
</details>
|
|
@@ -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
|
|
|
@@ -511,13 +525,48 @@ function formatCodeBlock(code: string, language: BundledLanguage): string {
|
|
|
511
525
|
return language === "json" ? (prettyJsonData(code) ?? code) : code;
|
|
512
526
|
}
|
|
513
527
|
|
|
514
|
-
/**
|
|
515
|
-
|
|
516
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Detect the language for LLM text output prose: json if the text is valid
|
|
530
|
+
* JSON or JSONL, markdown otherwise. Never auto-detects XML, HTML, TypeScript,
|
|
531
|
+
* or shell — those heuristics are unreliable for rendered assistant output.
|
|
532
|
+
*/
|
|
533
|
+
export function detectOutputLanguage(text: string): BundledLanguage {
|
|
534
|
+
const trimmed = text.trim();
|
|
535
|
+
if (!trimmed) return "markdown";
|
|
536
|
+
try {
|
|
537
|
+
JSON.parse(trimmed);
|
|
538
|
+
return "json";
|
|
539
|
+
} catch {
|
|
540
|
+
// continue
|
|
541
|
+
}
|
|
542
|
+
if (prettyJsonl(trimmed)) return "json";
|
|
543
|
+
return "markdown";
|
|
517
544
|
}
|
|
518
545
|
|
|
519
|
-
/**
|
|
520
|
-
|
|
546
|
+
/**
|
|
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.
|
|
550
|
+
*/
|
|
551
|
+
export function canRenderStructuredMarkup(block: CodeBlock): boolean {
|
|
552
|
+
return block.language === "xml" || block.language === "html";
|
|
553
|
+
}
|
|
554
|
+
|
|
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;
|
|
521
570
|
const blocks: CodeBlock[] = [];
|
|
522
571
|
const fence = /```([A-Za-z0-9_-]+)?\n([\s\S]*?)```/g;
|
|
523
572
|
let cursor = 0;
|
|
@@ -525,24 +574,25 @@ export function parseMarkdownBlocks(text: string): CodeBlock[] {
|
|
|
525
574
|
while ((match = fence.exec(text))) {
|
|
526
575
|
const prose = text.slice(cursor, match.index).trim();
|
|
527
576
|
if (prose) {
|
|
528
|
-
const language =
|
|
529
|
-
blocks.push({ code: formatCodeBlock(prose, language), language });
|
|
577
|
+
const language = detectProse(prose);
|
|
578
|
+
blocks.push({ code: formatCodeBlock(prose, language), fenced: false, language });
|
|
530
579
|
}
|
|
531
580
|
const language = normalizeLanguage(match[1]);
|
|
532
581
|
blocks.push({
|
|
533
582
|
code: formatCodeBlock(match[2] ?? "", language),
|
|
583
|
+
fenced: true,
|
|
534
584
|
language,
|
|
535
585
|
});
|
|
536
586
|
cursor = match.index + match[0].length;
|
|
537
587
|
}
|
|
538
588
|
const rest = text.slice(cursor).trim();
|
|
539
589
|
if (rest) {
|
|
540
|
-
const language =
|
|
541
|
-
blocks.push({ code: formatCodeBlock(rest, language), language });
|
|
590
|
+
const language = detectProse(rest);
|
|
591
|
+
blocks.push({ code: formatCodeBlock(rest, language), fenced: false, language });
|
|
542
592
|
}
|
|
543
593
|
if (blocks.length > 0) return blocks;
|
|
544
|
-
const language =
|
|
545
|
-
return [{ code: formatCodeBlock(text, language), language }];
|
|
594
|
+
const language = detectProse(text);
|
|
595
|
+
return [{ code: formatCodeBlock(text, language), fenced: false, language }];
|
|
546
596
|
}
|
|
547
597
|
|
|
548
598
|
/** Parse XML/HTML-ish fragments for the collapsible transcript renderer. */
|
package/src/client/types.ts
CHANGED
|
@@ -141,7 +141,7 @@ export type SessionFilter = "active" | "recent" | "hung" | "failed" | "all";
|
|
|
141
141
|
|
|
142
142
|
export type VisualStatus = "active" | "failed" | "hung" | "idle";
|
|
143
143
|
|
|
144
|
-
export type CodeBlock = { code: string; language: BundledLanguage };
|
|
144
|
+
export type CodeBlock = { code: string; fenced?: boolean; language: BundledLanguage };
|
|
145
145
|
|
|
146
146
|
export type MarkupNode =
|
|
147
147
|
| {
|