Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/junior-dashboard/src/client/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { CodeBlock, MarkupNode } from "./types";

/** Count rendered children so transcripts can decide which markup node expands. */
export function countStructuredBlockChildren(block: CodeBlock): number {
if (!canRenderStructuredMarkup(block.language)) return 1;
if (!canRenderStructuredMarkup(block)) return 1;
const rootCount = parseMarkupNodes(block.code, block.language).length;
return rootCount > 0 ? rootCount : 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function TranscriptText(props: {
const childCount = countStructuredBlockChildren(block);
seenChildren += childCount;

if (!canRenderStructuredMarkup(block.language)) {
if (!canRenderStructuredMarkup(block)) {
return (
<HighlightedCode
code={block.code}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState, type ClipboardEventHandler, type ReactNode } from "react";
import { HighlightedCode } from "../code";
import {
detectLanguage,
detectOutputLanguage,
formatBytes,
formatMessageOffset,
formatMessageTimestamp,
Expand Down Expand Up @@ -494,7 +495,7 @@ function ThinkingPartView(props: { value: unknown }) {
<div className="border-t border-[#beaaff]/15 px-3 py-3">
<HighlightedCode
code={rendered || "{}"}
language={detectLanguage(rendered)}
language={detectOutputLanguage(rendered)}
/>
</div>
</details>
Expand Down
44 changes: 35 additions & 9 deletions packages/junior-dashboard/src/client/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,34 @@ function formatCodeBlock(code: string, language: BundledLanguage): string {
return language === "json" ? (prettyJsonData(code) ?? code) : code;
}

/** Decide whether a fenced block can use the interactive markup renderer. */
export function canRenderStructuredMarkup(language: BundledLanguage): boolean {
return language === "xml" || language === "html";
/**
* Detect the language for LLM text output prose: json if the text is valid
* JSON or JSONL, markdown otherwise. Never auto-detects XML, HTML, TypeScript,
* or shell — those heuristics are unreliable for rendered assistant output.
*/
export function detectOutputLanguage(text: string): BundledLanguage {
const trimmed = text.trim();
if (!trimmed) return "markdown";
try {
JSON.parse(trimmed);
return "json";
} catch {
// continue
}
if (prettyJsonl(trimmed)) return "json";
return "markdown";
}

/**
* Decide whether a fenced block can use the interactive markup renderer.
* Structured XML/HTML rendering is only enabled for explicitly-fenced blocks;
* auto-detected prose is never eligible regardless of inferred language.
*/
export function canRenderStructuredMarkup(block: CodeBlock): boolean {
return (
block.fenced === true &&
(block.language === "xml" || block.language === "html")
);
}

/** Parse markdown into renderable code blocks while preserving plain text blocks. */
Expand All @@ -525,24 +550,25 @@ export function parseMarkdownBlocks(text: string): CodeBlock[] {
while ((match = fence.exec(text))) {
const prose = text.slice(cursor, match.index).trim();
if (prose) {
const language = detectLanguage(prose);
blocks.push({ code: formatCodeBlock(prose, language), language });
const language = detectOutputLanguage(prose);
blocks.push({ code: formatCodeBlock(prose, language), fenced: false, language });
}
const language = normalizeLanguage(match[1]);
blocks.push({
code: formatCodeBlock(match[2] ?? "", language),
fenced: true,
language,
});
cursor = match.index + match[0].length;
}
const rest = text.slice(cursor).trim();
if (rest) {
const language = detectLanguage(rest);
blocks.push({ code: formatCodeBlock(rest, language), language });
const language = detectOutputLanguage(rest);
blocks.push({ code: formatCodeBlock(rest, language), fenced: false, language });
}
if (blocks.length > 0) return blocks;
const language = detectLanguage(text);
return [{ code: formatCodeBlock(text, language), language }];
const language = detectOutputLanguage(text);
return [{ code: formatCodeBlock(text, language), fenced: false, language }];
}

/** Parse XML/HTML-ish fragments for the collapsible transcript renderer. */
Expand Down
2 changes: 1 addition & 1 deletion packages/junior-dashboard/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export type SessionFilter = "active" | "recent" | "hung" | "failed" | "all";

export type VisualStatus = "active" | "failed" | "hung" | "idle";

export type CodeBlock = { code: string; language: BundledLanguage };
export type CodeBlock = { code: string; fenced?: boolean; language: BundledLanguage };

export type MarkupNode =
| {
Expand Down
83 changes: 83 additions & 0 deletions packages/junior-dashboard/tests/format.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { describe, expect, it } from "vitest";

import {
canRenderStructuredMarkup,
formatDurationTotal,
formatTokenTotal,
formatUsageTotal,
parseMarkdownBlocks,
turnMessageCount,
} from "../src/client/format";
import type { ConversationTurn } from "../src/client/types";
Expand Down Expand Up @@ -71,3 +73,84 @@ describe("dashboard token formatting", () => {
expect(turnMessageCount(turn)).toBe(2);
});
});

describe("parseMarkdownBlocks output language detection", () => {
it("treats XML-looking prose as markdown, never auto-detects XML", () => {
const [block] = parseMarkdownBlocks("<foo>bar</foo>");
expect(block?.language).toBe("markdown");
expect(block?.fenced).toBe(false);
});

it("treats HTML-looking prose as markdown", () => {
const [block] = parseMarkdownBlocks("<div>Hello</div>");
expect(block?.language).toBe("markdown");
});

it("treats TypeScript-looking prose as markdown", () => {
const [block] = parseMarkdownBlocks("const value = 1;");
expect(block?.language).toBe("markdown");
});

it("treats shell-looking prose as markdown", () => {
const [block] = parseMarkdownBlocks("npm install");
expect(block?.language).toBe("markdown");
});

it("detects valid JSON prose as json and pretty-prints it", () => {
const [block] = parseMarkdownBlocks('{"a":1}');
expect(block?.language).toBe("json");
expect(block?.code).toBe('{\n "a": 1\n}');
expect(block?.fenced).toBe(false);
});

it("marks prose blocks as not fenced", () => {
const blocks = parseMarkdownBlocks("some prose text");
expect(blocks[0]?.fenced).toBe(false);
});

it("marks explicit fenced blocks as fenced", () => {
const blocks = parseMarkdownBlocks("before\n```xml\n<foo/>\n```\nafter");
expect(blocks[1]?.language).toBe("xml");
expect(blocks[1]?.fenced).toBe(true);
});

it("keeps prose blocks as markdown when fenced XML is present", () => {
const blocks = parseMarkdownBlocks("before\n```xml\n<foo/>\n```\nafter");
expect(blocks[0]?.language).toBe("markdown");
expect(blocks[0]?.fenced).toBe(false);
expect(blocks[2]?.language).toBe("markdown");
expect(blocks[2]?.fenced).toBe(false);
});
});

describe("canRenderStructuredMarkup", () => {
it("returns false for auto-detected prose (fenced: false)", () => {
expect(
canRenderStructuredMarkup({ code: "<foo/>", language: "xml", fenced: false }),
).toBe(false);
});

it("returns true for explicitly-fenced xml", () => {
expect(
canRenderStructuredMarkup({ code: "<foo/>", language: "xml", fenced: true }),
).toBe(true);
});

it("returns true for explicitly-fenced html", () => {
expect(
canRenderStructuredMarkup({ code: "<div/>", language: "html", fenced: true }),
).toBe(true);
});

it("returns false for fenced non-xml/html blocks", () => {
expect(
canRenderStructuredMarkup({ code: "const x = 1", language: "typescript", fenced: true }),
).toBe(false);
});

it("returns false when fenced is undefined", () => {
expect(
canRenderStructuredMarkup({ code: "<foo/>", language: "xml" }),
).toBe(false);
});
});
Loading