browser: raw/markdown view switch on detail cards

The context browser's detail cards (system prompt, tool description,
user/assistant message and injection bodies) gain a segmented Raw |
Markdown switch at the card's top-right corner — the same pill chrome
as the trend chart's Step/Turn control. Markdown is the default view,
rendered through the harness's shared MarkdownText (GFM, sanitized,
resolved from the platform module table — zero plugin-side markdown
dependency); Raw restores the exact source <pre>. Each card owns its
mode state via the new shared richText kit (explicit render props, no
React context), so two open details switch independently. Reasoning
blocks, tool-call args and tool results stay raw.

Markdown chrome is scoped to the detail cards: base size 13px (a
proportional face matching the raw view's 12px monospace in visual
weight), headings collapse to a weight-only bump, inline code keeps
full size and is pinned under the paragraph line so wrapped chips no
longer stick across lines.
This commit is contained in:
Bowen Liang committed 2026-08-20 21:55:27 +08:00
1 parent 35e6a8b2e8
commit 81415caad4
5 files changed
+270 -30

No files matched your search

+61 -27
View File
@@ -26,6 +26,8 @@ import { React } from '../react'
import type { ConversationNodeLike, UseSessionLike } from '../services'
import type { ViewKit } from '../viewkit'
import { makeNodeText } from './nodes'
import { makeRichText } from './richText'
import type { RichKit, RichMode } from './richText'
import type { StackedBarProps } from './stackedBar'
export interface ContextBrowserProps {
@@ -181,11 +183,14 @@ function ParamRow(props: {
* The full body of one expanded tool row: description, a parsed parameter
* table (when the schema carries one), and the raw JSON behind a toggle.
* Owns its own open/closed state for the JSON so two expanded tools stay
* independent.
* independent. The description card carries the shared raw/markdown view
* toggle in its head (per-card state via `rich.useRichMode`).
*/
function ToolSchema(props: {
description: string | undefined
schema: unknown
/** Rich-text toggle kit (description card: raw <pre> vs markdown). */
rich: RichKit
/** Localized labels for the card titles, empty-state line, and toggle. */
labels: {
desc: string
@@ -195,7 +200,9 @@ function ToolSchema(props: {
hide: string
}
}): ReactNS.ReactElement {
const { rich } = props
const [jsonOpen, setJsonOpen] = React.useState(false)
const [descMode, setDescMode] = rich.useRichMode()
const params = React.useMemo(() => paramsOf(props.schema), [props.schema])
// Pull `properties` + `required` out of the parameter object as plain
// values (avoids re-deriving the same shape in every row).
@@ -225,8 +232,14 @@ function ToolSchema(props: {
<div className="lc-ts-card">
<div className="lc-ts-card-head">
<b>{props.labels.desc}</b>
<rich.RichSwitch mode={descMode} onPick={setDescMode} inHead />
</div>
<pre className="lc-ts-desc-body">{props.description}</pre>
<rich.RichText
text={props.description}
mode={descMode}
rawClass="lc-ts-desc-body"
mdClass="lc-ts-desc-md"
/>
</div>
) : null}
{params !== null && rows.length > 0 ? (
@@ -255,17 +268,23 @@ function ToolSchema(props: {
}
/** One raw content block (text/reasoning/tool-result/…), rendered defensively. */
function RawBlocks(props: { blocks: readonly unknown[] }): ReactNS.ReactElement {
function RawBlocks(props: { blocks: readonly unknown[]; mode: RichMode; rich: RichKit }): ReactNS.ReactElement {
const { rich } = props
return (
<>
{props.blocks.map((b, i) => {
const blk = b as { type?: string; text?: unknown; content?: unknown }
if (blk !== null && typeof blk === 'object'
&& (blk.type === 'text' || blk.type === 'reasoning') && typeof blk.text === 'string') {
return <pre key={i} className={'lc-br-pre' + (blk.type === 'reasoning' ? ' lc-br-dim' : '')}>{blk.text}</pre>
// Reasoning stays raw in either mode (plain model trace, not
// prose); text blocks follow the card's view mode.
if (blk.type === 'reasoning') {
return <pre key={i} className="lc-br-pre lc-br-dim">{blk.text}</pre>
}
return <rich.RichText key={i} text={blk.text} mode={props.mode} />
}
if (blk !== null && typeof blk === 'object' && blk.type === 'tool-result' && Array.isArray(blk.content)) {
return <RawBlocks key={i} blocks={blk.content as unknown[]} />
return <RawBlocks key={i} blocks={blk.content as unknown[]} mode={props.mode} rich={rich} />
}
return <pre key={i} className="lc-br-pre lc-br-dim">{JSON.stringify(b, null, 2)}</pre>
})}
@@ -274,23 +293,36 @@ function RawBlocks(props: { blocks: readonly unknown[] }): ReactNS.ReactElement
}
/** The actual content of one surface element, joined from the conversation snapshot. */
function NodeContent(props: { node: SurfaceNode; conv: ConversationNodeLike | undefined; hint: string }): ReactNS.ReactElement {
const { node, conv } = props
function NodeContent(props: { node: SurfaceNode; conv: ConversationNodeLike | undefined; hint: string; rich: RichKit }): ReactNS.ReactElement {
const { node, conv, rich } = props
// The raw/markdown toggle serves the prose categories (user / assistant /
// injection bodies); tool results stay raw (structured payload, not prose).
const richable = node.cat === 'user' || node.cat === 'assistant' || node.cat === 'inject'
const wrap = (render: (mode: RichMode) => ReactNS.ReactNode): ReactNS.ReactElement =>
richable
? <rich.RichCard render={render} />
: <div className="lc-br-content">{render('raw')}</div>
if (conv === undefined) {
return (
<div className="lc-br-content">
{node.text !== undefined && node.text !== '' ? <pre className="lc-br-pre">{node.text}</pre> : null}
if (node.text === undefined || node.text === '') {
return <div className="lc-br-content"><div className="lc-br-note">{props.hint}</div></div>
}
return wrap(mode => (
<>
<rich.RichText text={node.text ?? ''} mode={mode} />
<div className="lc-br-note">{props.hint}</div>
</div>
)
</>
))
}
if (conv.kind === 'assistant' && Array.isArray(conv.blocks)) {
return (
<div className="lc-br-content">
{conv.blocks.map((b, i) => {
// Captured once: the render-prop closure below cannot narrow the prop.
const blocks = conv.blocks
return wrap(mode => (
<>
{blocks.map((b, i) => {
const blk = b as { kind?: string; text?: unknown; name?: unknown; argsRaw?: unknown }
if (blk.kind === 'text' && typeof blk.text === 'string') {
return <pre key={i} className="lc-br-pre">{blk.text}</pre>
return <rich.RichText key={i} text={blk.text} mode={mode} />
}
if (blk.kind === 'reasoning' && typeof blk.text === 'string') {
return <pre key={i} className="lc-br-pre lc-br-dim">{blk.text}</pre>
@@ -307,8 +339,8 @@ function NodeContent(props: { node: SurfaceNode; conv: ConversationNodeLike | un
}
return null
})}
</div>
)
</>
))
}
if (conv.kind === 'tool-result') {
return (
@@ -321,21 +353,21 @@ function NodeContent(props: { node: SurfaceNode; conv: ConversationNodeLike | un
</div>
)
: null}
{Array.isArray(conv.content) ? <RawBlocks blocks={conv.content} /> : null}
{Array.isArray(conv.content) ? <RawBlocks blocks={conv.content} mode="raw" rich={rich} /> : null}
</div>
)
}
if (conv.kind === 'compaction') {
return (
<div className="lc-br-content">
return wrap(mode => (
<>
{typeof conv.summary === 'string' && conv.summary !== ''
? <pre className="lc-br-pre">{conv.summary}</pre>
? <rich.RichText text={conv.summary} mode={mode} />
: null}
</div>
)
</>
))
}
if (Array.isArray(conv.content)) {
return <div className="lc-br-content"><RawBlocks blocks={conv.content} /></div>
return wrap(mode => <RawBlocks blocks={conv.content as unknown[]} mode={mode} rich={rich} />)
}
return <div className="lc-br-content"><div className="lc-br-note">{props.hint}</div></div>
}
@@ -366,6 +398,7 @@ export function makeContextBrowser(
): (props: ContextBrowserProps) => ReactNS.ReactElement {
const { t, fmt, fmtTime, catLabel } = kit
const nodeText = makeNodeText(kit)
const rich = makeRichText(kit)
const catColor: Record<string, string> = {}
for (const c of CATS) catColor[c.key] = c.color
@@ -535,7 +568,7 @@ export function makeContextBrowser(
const system = view.header.system
if (system === undefined) return null
return elemRow('sys', null, system.replace(/\s+/g, ' ').trim().slice(0, 80), breakdown.system, undefined,
<div className="lc-br-content"><pre className="lc-br-pre">{system}</pre></div>)
<rich.RichCard render={mode => <rich.RichText text={system} mode={mode} />} />)
}
if (c === 'tools') {
if (view.header === null) return <div className="lc-br-note">{t(headers === null ? 'browser.noHeader' : 'browser.noEpoch')}</div>
@@ -554,7 +587,7 @@ export function makeContextBrowser(
return view.header.tools.slice().sort((a, b) => b.tokens - a.tokens).map((tool: HeaderTool) => {
return elemRow('tool:' + tool.name, null, tool.name, tool.tokens, undefined,
<div className="lc-br-content">
<ToolSchema description={tool.description} schema={tool.schema} labels={labels} />
<ToolSchema description={tool.description} schema={tool.schema} rich={rich} labels={labels} />
</div>)
})
}
@@ -580,6 +613,7 @@ export function makeContextBrowser(
<NodeContent
node={n}
conv={bySeq.get(n.seq)}
rich={rich}
// This row's body renders only while it is the open element, so
// `awaiting` (open seq missing, pagination armed) means THIS join
// is the one pages are being pulled for.
+108
View File
@@ -0,0 +1,108 @@
/**
* RichText — the raw/markdown view switch shared by the Context browser's
* detail cards (system prompt, tool description, user/assistant message and
* injection bodies).
*
* `RichCard` owns the per-card mode state and places a small segmented
* switch (the same pill chrome as the trend chart's 步骤/轮次 control) at
* the card's top-right corner; `RichText` renders one text block in the
* handed-down mode — rendered markdown via the harness's shared
* MarkdownText renderer (the default; GFM, sanitized, resolved from the
* platform module table — zero plugin-side markdown dependency) or the raw
* <pre>. The mode crosses component boundaries explicitly through render
* props/props (no React context), and each card keeps its own state so two
* open details switch independently.
*/
import type * as ReactNS from 'react'
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { React } from '../react'
import type { ViewKit } from '../viewkit'
/** Detail-body view mode: raw source text or rendered markdown. */
export type RichMode = 'raw' | 'md'
export interface RichKit {
/** One text block in the given mode: raw <pre> or rendered markdown. */
RichText: (props: {
text: string
mode: RichMode
/** Chrome of the raw block (defaults to the browser's `lc-br-pre`). */
rawClass?: string
/** Chrome of the markdown box (defaults to `lc-br-md`). */
mdClass?: string
}) => ReactNS.ReactElement
/**
* Detail card wrapper: renders the mode switch as a small segmented pill
* on the card's top-right edge and hands the current mode to its `render`
* prop (an explicit prop, not function-children, so the mode hand-off is
* visible in the props object).
*/
RichCard: (props: {
className?: string
render: (mode: RichMode) => ReactNS.ReactNode
}) => ReactNS.ReactElement
/** The segmented switch alone, for cards that place it in their own head row. */
RichSwitch: (props: { mode: RichMode; onPick: (mode: RichMode) => void; inHead?: boolean }) => ReactNS.ReactElement
/** Per-card mode state for bodies that place the switch themselves. */
useRichMode: () => [RichMode, (mode: RichMode) => void]
}
export function makeRichText(kit: ViewKit): RichKit {
const { t } = kit
function useRichMode(): [RichMode, (mode: RichMode) => void] {
// Markdown is the default view: the detail cards hold prose (prompts,
// descriptions, messages), which reads better rendered; raw stays one
// click away for exact source inspection.
const [mode, setMode] = React.useState<RichMode>('md')
return [mode, setMode]
}
// Two-option pill mirroring the trend chart's 步骤/轮次 control: one
// segment per view, the active segment highlighted; each segment's
// tooltip spells its view out (localized).
function RichSwitch(props: { mode: RichMode; onPick: (mode: RichMode) => void; inHead?: boolean }): ReactNS.ReactElement {
const seg = (m: RichMode, label: string, tip: string) => (
<button
type="button"
className={'lc-rich-seg-btn' + (props.mode === m ? ' lc-rich-seg-on' : '')}
title={tip}
onClick={() => { props.onPick(m) }}
>{label}</button>
)
return (
<span className={'lc-rich-seg' + (props.inHead === true ? ' lc-rich-seg-head' : '')}>
{seg('raw', t('rich.raw'), t('rich.toRaw'))}
{seg('md', t('rich.md'), t('rich.toMd'))}
</span>
)
}
function RichText(props: {
text: string
mode: RichMode
rawClass?: string
mdClass?: string
}): ReactNS.ReactElement {
if (props.mode === 'md') {
return <div className={props.mdClass ?? 'lc-br-md'}><MarkdownText text={props.text} /></div>
}
return <pre className={props.rawClass ?? 'lc-br-pre'}>{props.text}</pre>
}
function RichCard(props: {
className?: string
render: (mode: RichMode) => ReactNS.ReactNode
}): ReactNS.ReactElement {
const [mode, setMode] = useRichMode()
return (
<div className={props.className ?? 'lc-br-content'}>
<RichSwitch mode={mode} onPick={setMode} />
{props.render(mode)}
</div>
)
}
return { RichText, RichCard, RichSwitch, useRichMode }
}
+12
View File
@@ -88,6 +88,12 @@ export const DICT_ZH: Record<string, string> = {
'tool.paramsEmpty': '(无参数)',
'tool.jsonToggle': '查看原始 JSON',
'tool.jsonHide': '收起',
// Detail-card view switch (system prompt / tool description / message and
// injection bodies): a segmented pill — segment labels + tooltips.
'rich.raw': '原文',
'rich.md': 'Markdown',
'rich.toMd': '按 Markdown 渲染查看',
'rich.toRaw': '查看原始文本',
}
export const DICT_EN: Record<string, string> = {
@@ -173,4 +179,10 @@ export const DICT_EN: Record<string, string> = {
'tool.paramsEmpty': '(no parameters)',
'tool.jsonToggle': 'View Raw JSON',
'tool.jsonHide': 'Collapse',
// Detail-card view switch (system prompt / tool description / message and
// injection bodies): a segmented pill — segment labels + tooltips.
'rich.raw': 'Raw',
'rich.md': 'Markdown',
'rich.toMd': 'View as Markdown',
'rich.toRaw': 'View Raw Text',
}
+31
View File
@@ -293,4 +293,35 @@ export const STYLES = [
'.lc-ts-json { display: flex; flex-direction: column; gap: 4px; }',
'.lc-ts-json-toggle { align-self: flex-start; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; padding: 0; cursor: pointer; }',
'.lc-ts-json-toggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }',
// ---- Raw/markdown view switch (detail cards: system prompt, tool
// description, user/assistant message + injection bodies). A segmented
// pill mirroring the trend chart's 步骤/轮次 control (`lc-gran`), riding
// the card's top-right corner as a self-end item of the content column
// (no absolute overlay, so it never covers text); the `in-head` variant
// sits at the right end of the tool description card's title row. The
// markdown box reuses the raw <pre> chrome minus the pre whitespace. ----
'.lc-rich-seg { align-self: flex-end; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }',
'.lc-rich-seg-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }',
'.lc-rich-seg-btn:hover { color: var(--dsw-alias-label-primary); }',
'.lc-rich-seg-on, .lc-rich-seg-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }',
'.lc-rich-seg-head { align-self: auto; margin-left: auto; }',
'.lc-br-md { padding: 4px 10px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }',
'.lc-ts-desc-md { padding: 4px 10px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; word-break: break-word; max-height: 160px; overflow-y: auto; scrollbar-width: thin; }',
// The shared markdown renderer sizes its root/headings for chat bubbles;
// inside a detail card the base size sits one step ABOVE the raw text
// (13px vs 12px — a proportional face at 13px matches the visual weight
// of the raw view's 12px monospace) and the heading scale collapses to a
// weight-only bump (specificity beats the renderer's one-class rules).
// Inline code keeps the FULL size too — the renderer shrinks it to
// 0.875em (!important) and table code to 13px, both visibly off. Its
// `font` shorthand also drags in a ~22px line-height, TALLER than the
// 13px/1.55 paragraph line (~20px), so a code chip wrapping onto the
// next line sticks to the neighbor line's chip; pin the chip just under
// the paragraph line (1.3em + 1px padding ≈ 19px) to reopen the gap.
'.lc-br-md > div, .lc-ts-desc-md > div { font-size: 13px; line-height: 1.55; }',
'.lc-br-md div :is(h1, h2, h3, h4, h5, h6), .lc-ts-desc-md div :is(h1, h2, h3, h4, h5, h6) { font-size: 13px; font-weight: 600; margin: 10px 0 4px; }',
'.lc-br-md div p, .lc-ts-desc-md div p { margin: 6px 0; }',
'.lc-br-md div pre, .lc-ts-desc-md div pre { margin: 6px 0; }',
'.lc-br-md div :not(pre) > code, .lc-ts-desc-md div :not(pre) > code { font-size: 1em !important; line-height: 1.3; padding: 1px 5px; box-decoration-break: clone; }',
'.lc-br-md div table code, .lc-ts-desc-md div table code { font-size: inherit; }',
].join('\n')
+58 -3
View File
@@ -47,10 +47,13 @@ globalThis.document = fakeDoc
// The module table answers the platform seeds this bundle uses ('react' and
// the shared primitives singleton); everything else rides ctx or is inlined.
// Icons are stubbed as inert elements (glyph rendering is never asserted).
// Icons are stubbed as inert elements (glyph rendering is never asserted);
// MarkdownText resolves to a plain div carrying its source text, so the
// raw/markdown toggle assertions can find it by class and text.
const fakePrimitives = {
IconPlusOutline16: (p) => ({ kind: 'icon', name: 'IconPlusOutline16', props: p }),
IconBranchOutline16: (p) => ({ kind: 'icon', name: 'IconBranchOutline16', props: p }),
MarkdownText: (p) => ({ kind: 'element', args: ['div', { className: 'lc-md-stub' }, p.text] }),
}
const require = (spec) => {
assert.ok(spec === 'react' || spec === '@deepseek-ai/dsh-client-ui-primitives',
@@ -230,7 +233,7 @@ const m2 = { exports: {} }
const pluginExports2 = factory(requireStateful, m2, globalThis.window, fakeDoc)
const DICT_FOR_TEST = { 'tab': 'Context', 'loading': '…', 'error': 'x', 'detail.step': 'Turn {t} · Step {s}', 'gran.step': 'Step', 'gran.turn': 'Turn', 'detail.turn': 'Turn {t} · {n} steps', 'detail.lastStep': 'last step', 'overview.used': 'of context used', 'overview.free': 'Free window', 'events.at': 'Turn {t} · Step {s}', 'events.range': 'Turn {t} · Step {a}→{b}', 'events.rangeTo': 'Turn {a} · Step {as} → Turn {b} · Step {bs}', 'stats.recycleSub': '{c} compactions · {p} prunes', 'tip.step': 'Turn {t} · Step {s}', 'tip.turn': 'Turn {t} · {n} steps', 'tip.total': 'total ≈ {n}', 'tip.actual': ' (actual {n})', 'trend.title': 'History', 'trend.empty': 'empty trend', 'cmd.close': 'Close', 'cat.user': 'User', 'browser.live': 'Live (next request)', 'browser.liveNow': 'Live · next request', 'browser.items': '{n} items', 'browser.noContent': 'outside the loaded window', 'browser.loading': 'loading older history', 'browser.preview': 'preview', 'browser.noHeader': 'older plugin build',
'browser.deltaHint': 'vs previous turn', 'overview.compactReserve': 'compact reserve {pct}%', 'tool.desc': 'Description', 'tool.params': 'Parameters', 'tool.paramsEmpty': '(no parameters)', 'tool.jsonToggle': 'View Raw JSON', 'tool.jsonHide': 'Collapse' }
'browser.deltaHint': 'vs previous turn', 'overview.compactReserve': 'compact reserve {pct}%', 'tool.desc': 'Description', 'tool.params': 'Parameters', 'tool.paramsEmpty': '(no parameters)', 'tool.jsonToggle': 'View Raw JSON', 'tool.jsonHide': 'Collapse', 'rich.raw': 'Raw', 'rich.md': 'Markdown', 'rich.toMd': 'View as Markdown', 'rich.toRaw': 'View Raw Text' }
let viewComponent = null
let modalComponent = null
let modalSource = null
@@ -913,6 +916,27 @@ tr = renderView()
assert.equal(byClass(tr, 'lc-br-content').length, 1, 'element content area open')
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /first message/, 'content falls back to the node preview')
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /outside the loaded window/, 'window note follows the fallback preview')
// The message detail card carries the raw/markdown switch too (a segmented
// pill like the trend chart's Step/Turn), defaulting to MARKDOWN: the
// preview renders through MarkdownText while the window note stays.
const msgSeg = byClass(byClass(tr, 'lc-br-content')[0], 'lc-rich-seg')[0]
assert.ok(msgSeg, 'message detail card carries the raw/markdown switch')
const msgSegBtns = byClass(msgSeg, 'lc-rich-seg-btn')
assert.deepEqual(msgSegBtns.map(b => textOf(b)), ['Raw', 'Markdown'], 'one segment per view')
assert.match(msgSegBtns[1].args[1].className, /lc-rich-seg-on/, 'markdown segment is active by default')
assert.equal(msgSegBtns[0].args[1].title, 'View Raw Text', 'raw segment tooltip')
assert.equal(msgSegBtns[1].args[1].title, 'View as Markdown', 'markdown segment tooltip')
assert.equal(textOf(byClass(tr, 'lc-md-stub')[0]), 'first message', 'markdown view carries the message text')
assert.match(textOf(byClass(tr, 'lc-br-content')[0]), /outside the loaded window/, 'window note stays in markdown mode')
// The Raw segment restores the raw <pre>; picking Markdown again flips back.
msgSegBtns[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 0, 'raw mode drops the markdown renderer')
assert.match(textOf(byClass(tr, 'lc-br-pre')[0]), /first message/, 'raw view carries the message text')
assert.match(byClass(tr, 'lc-rich-seg-btn')[0].args[1].className, /lc-rich-seg-on/, 'raw segment lights up')
byClass(tr, 'lc-rich-seg-btn')[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 1, 'switching back restores the markdown view')
// Pick step seq 2 (Turn 1 · Step 1): the reconstruction includes the archived
// node (gone 3 > 2) — the accordion resets on picking, so reopen the category.
@@ -938,6 +962,24 @@ tr = renderView()
brSlots[2][1]('sys')
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /SYSTEM-PROMPT-TEXT/, 'system section shows the full prompt')
// Raw/markdown view switch on the system-prompt detail card, defaulting to
// MARKDOWN; the Raw segment restores the raw <pre> and Markdown flips back.
assert.ok(byClass(tr, 'lc-rich-seg').length >= 1, 'system detail card carries the raw/markdown switch')
const sysSegBtns = () => byClass(byClass(tr, 'lc-rich-seg')[0], 'lc-rich-seg-btn')
assert.match(sysSegBtns()[1].args[1].className, /lc-rich-seg-on/, 'markdown segment is active by default')
const sysMd = byClass(tr, 'lc-md-stub')
assert.equal(sysMd.length, 1, 'markdown mode renders the prompt through MarkdownText')
assert.equal(textOf(sysMd[0]), 'SYSTEM-PROMPT-TEXT', 'markdown view carries the same source text')
assert.equal(byClass(tr, 'lc-br-body')[0] && byClass(byClass(tr, 'lc-br-body')[0], 'lc-br-pre').length, 0,
'markdown mode drops the raw <pre>')
sysSegBtns()[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 0, 'raw mode drops the markdown renderer')
assert.match(textOf(byClass(tr, 'lc-br-pre')[0]), /SYSTEM-PROMPT-TEXT/, 'raw view shows the prompt')
sysSegBtns()[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-md-stub').length, 1, 'switching back restores the markdown view')
brSlots[1][1]('tools')
tr = renderView()
assert.match(textOf(byClass(tr, 'lc-br-body')[0]), /bash/, 'tools section lists the schema rows')
@@ -956,7 +998,20 @@ const descCards = byClass(tr, 'lc-ts-card').filter(c => {
return head !== undefined && textOf(head).includes('Description')
})
assert.equal(descCards.length, 1, 'description is rendered inside a titled card')
assert.match(textOf(byClass(descCards[0], 'lc-ts-desc-body')[0]), /run a command/, 'description card body carries the prose')
// The description card head carries the same raw/markdown switch, also
// defaulting to MARKDOWN; the Raw segment restores the raw body (and back).
const descSeg = byClass(descCards[0], 'lc-rich-seg')[0]
assert.ok(descSeg, 'description card head carries the raw/markdown switch')
const descMd = byClass(descCards[0], 'lc-ts-desc-md')
assert.equal(descMd.length, 1, 'description defaults to the markdown view')
assert.equal(textOf(byClass(descMd[0], 'lc-md-stub')[0]), 'run a command', 'markdown view carries the description text')
byClass(descSeg, 'lc-rich-seg-btn')[0].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-ts-desc-md').length, 0, 'raw mode drops the markdown body')
assert.match(textOf(byClass(tr, 'lc-ts-desc-body')[0]), /run a command/, 'raw view carries the prose')
byClass(tr, 'lc-rich-seg-btn')[1].args[1].onClick()
tr = renderView()
assert.equal(byClass(tr, 'lc-ts-desc-md').length, 1, 'switching back restores the markdown view')
// Parsed parameter table sits above the (still-collapsed) raw JSON: one row
// per declared property, type labels carry the JSON-Schema type, required
// ones marked with ✓, descriptions shown on a second line.