refactor(client): unify Context browser detail rendering into one section-card system
Every expanded element body is now a uniform stack of Section cards (labeled head + body): prose carries the Raw/MD switch in the head, tool calls/params/images/raw JSON share the same frame, and one blocks→sections pipeline serves user/assistant/tool-result/injection bodies. Replaces the floating-switch, bare-pre, and per-kind card idioms; drops the dead lc-br-blk/lc-br-pre/lc-br-md/lc-br-rich-inline styles and the unused RichCard/inHead/dim parameters. Element-row tool tags lose their inverted chip and share the subtle injection-form tag style. Test bed: function components now receive props.children like real React.
This commit is contained in:
1 parent
af8f695854
commit
271f4e3395
8 files changed
+274
-335
No files matched your search
+200
-210
@@ -6,8 +6,9 @@
|
||||
* step picker + composition bar
|
||||
* → six category sections (accordion: dot, label, element count, tokens)
|
||||
* → element rows (one per message / prompt / tool schema)
|
||||
* → the element's ACTUAL content, rendered per kind (text, reasoning,
|
||||
* tool call + result, injection notice, system prompt, JSON schema).
|
||||
* → the element's ACTUAL content as a uniform stack of SECTION cards
|
||||
* (Section: labeled head + body) — prose, tool calls, parameter
|
||||
* rows, image grids, and raw JSON all share that one frame.
|
||||
*
|
||||
* Data sources: the `contextTimeline` projection (structure + token prices,
|
||||
* per-step reconstruction via the removed-node archive), the `contextHeaders`
|
||||
@@ -29,7 +30,7 @@ import { makeNodeText } from './nodes'
|
||||
import { imageRefOf, makeImageCard } from './images'
|
||||
import type { ImageKit } from './images'
|
||||
import { makeRichText } from './richText'
|
||||
import type { RichKit, RichMode } from './richText'
|
||||
import type { RichKit } from './richText'
|
||||
import type { StackedBarProps } from './stackedBar'
|
||||
import type { ImageLoader, ImageRefLike } from '../services'
|
||||
|
||||
@@ -187,19 +188,73 @@ function ParamRow(props: {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Section — the ONE detail chrome of the Context browser: a labeled card
|
||||
* (`lc-ts-card`) whose head carries the section title on the left and the
|
||||
* extras on the right (a count badge and/or the Raw/MD switch), with the
|
||||
* body below. Every expanded element renders as a stack of these — prose,
|
||||
* tool calls, parameter rows, image grids, and raw JSON all share the same
|
||||
* frame, so the reader scans one repeating anatomy instead of a different
|
||||
* layout per content kind.
|
||||
*/
|
||||
function Section(props: {
|
||||
label: string
|
||||
/** Head class for identifier-style titles (tool-call names: mono, larger). */
|
||||
labelClass?: string
|
||||
/** Count badge at the head's right edge (omitted when undefined). */
|
||||
count?: number
|
||||
/** Head actions (the Raw/MD switch), between the title and the count. */
|
||||
actions?: ReactNS.ReactNode
|
||||
children: ReactNS.ReactNode
|
||||
}): ReactNS.ReactElement {
|
||||
return (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b className={props.labelClass}>{props.label}</b>
|
||||
{props.actions ?? null}
|
||||
{props.count !== undefined ? <span className="lc-ts-card-count">{props.count}</span> : null}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* TextSection — a prose Section (system prompt, message/injection/summary
|
||||
* body, thinking, answer, tool description): the head's right edge carries
|
||||
* the Raw/MD switch and the body follows the per-card mode.
|
||||
*/
|
||||
function TextSection(props: { label: string; text: string; rich: RichKit }): ReactNS.ReactElement {
|
||||
const { rich } = props
|
||||
const [mode, setMode] = rich.useRichMode()
|
||||
return (
|
||||
<Section label={props.label} actions={<rich.RichSwitch mode={mode} onPick={setMode} />}>
|
||||
<rich.RichText text={props.text} mode={mode} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
/** RawSection — a dimmed text Section without a view switch (structured payloads: tool results, raw JSON). */
|
||||
function RawSection(props: { label: string; text: string }): ReactNS.ReactElement {
|
||||
return (
|
||||
<Section label={props.label}>
|
||||
<pre className="lc-ts-desc-body lc-br-dim">{props.text}</pre>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. The description card carries the shared raw/markdown view
|
||||
* toggle in its head (per-card state via `rich.useRichMode`).
|
||||
* independent.
|
||||
*/
|
||||
function ToolSchema(props: {
|
||||
description: string | undefined
|
||||
schema: unknown
|
||||
/** Rich-text toggle kit (description card: raw <pre> vs markdown). */
|
||||
/** Rich-text toggle kit (description section: raw <pre> vs markdown). */
|
||||
rich: RichKit
|
||||
/** Localized labels for the card titles, empty-state line, and toggle. */
|
||||
/** Localized labels for the section titles, empty-state line, and toggle. */
|
||||
labels: {
|
||||
desc: string
|
||||
title: string
|
||||
@@ -210,7 +265,6 @@ function ToolSchema(props: {
|
||||
}): 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).
|
||||
@@ -237,27 +291,12 @@ function ToolSchema(props: {
|
||||
return (
|
||||
<>
|
||||
{props.description !== undefined ? (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b>{props.labels.desc}</b>
|
||||
<rich.RichSwitch mode={descMode} onPick={setDescMode} inHead />
|
||||
</div>
|
||||
<rich.RichText
|
||||
text={props.description}
|
||||
mode={descMode}
|
||||
rawClass="lc-ts-desc-body"
|
||||
mdClass="lc-ts-desc-md"
|
||||
/>
|
||||
</div>
|
||||
<TextSection label={props.labels.desc} text={props.description} rich={rich} />
|
||||
) : null}
|
||||
{params !== null && rows.length > 0 ? (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b>{props.labels.title}</b>
|
||||
<span className="lc-ts-card-count">{rows.length}</span>
|
||||
</div>
|
||||
<Section label={props.labels.title} count={rows.length}>
|
||||
{rows.map(r => <ParamRow key={r.name} name={r.name} schema={r.schema} required={r.required} />)}
|
||||
</div>
|
||||
</Section>
|
||||
) : params !== null ? (
|
||||
<div className="lc-ts-params-empty">{props.labels.empty}</div>
|
||||
) : null}
|
||||
@@ -268,63 +307,99 @@ function ToolSchema(props: {
|
||||
className="lc-ts-json-toggle"
|
||||
onClick={() => { setJsonOpen(o => !o) }}
|
||||
>{(jsonOpen ? '▾ ' : '▸ ') + (jsonOpen ? props.labels.hide : props.labels.show)}</button>
|
||||
{jsonOpen ? <pre className="lc-br-pre lc-br-dim">{schemaJson}</pre> : null}
|
||||
{jsonOpen ? <pre className="lc-ts-desc-body lc-br-dim">{schemaJson}</pre> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** One raw content block (text/reasoning/tool-result/image/…), rendered defensively. */
|
||||
function RawBlocks(props: { blocks: readonly unknown[]; mode: RichMode; rich: RichKit; img: ImageKit }): ReactNS.ReactElement {
|
||||
const { rich, img } = props
|
||||
return (
|
||||
<>
|
||||
{props.blocks.map((b, i) => {
|
||||
const blk = b !== null && typeof b === 'object' ? b as { type?: string; text?: unknown; content?: unknown } : null
|
||||
if (blk !== null
|
||||
&& (blk.type === 'text' || blk.type === 'reasoning') && typeof blk.text === 'string') {
|
||||
// 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} />
|
||||
}
|
||||
const image = imageRefOf(b)
|
||||
if (image !== null) return <img.Card key={i} attachment={image} load={img.load} />
|
||||
if (blk !== null && blk.type === 'tool-result' && Array.isArray(blk.content)) {
|
||||
return <RawBlocks key={i} blocks={blk.content as unknown[]} mode={props.mode} rich={rich} img={img} />
|
||||
}
|
||||
return <pre key={i} className="lc-br-pre lc-br-dim">{JSON.stringify(b, null, 2)}</pre>
|
||||
})}
|
||||
</>
|
||||
)
|
||||
/** Localized section labels for one element body. */
|
||||
interface DetailLabels {
|
||||
thinking: string
|
||||
answer: string
|
||||
content: string
|
||||
result: string
|
||||
summary: string
|
||||
images: string
|
||||
other: string
|
||||
}
|
||||
|
||||
/**
|
||||
* One assistant-message block (thinking / answer) as its own card: the
|
||||
* title row carries the block label plus the block's OWN raw/markdown
|
||||
* switch (per-card state via `rich.useRichMode`, mirroring the tool
|
||||
* description card), so a mixed reply's parts toggle independently.
|
||||
* Render one message's content blocks as a uniform Section stack. Both
|
||||
* block vocabularies normalize here — raw durable blocks (`type`: text /
|
||||
* reasoning / tool-call / tool-result / image, arguments) and the
|
||||
* conversation snapshot's assistant blocks (`kind`: text / reasoning /
|
||||
* tool-call / image, argsRaw). Consecutive images group into one grid
|
||||
* section; `richable` bodies (messages, injections) carry the Raw/MD
|
||||
* switch while tool-result payloads stay raw (structured data, not prose)
|
||||
* and dim; nested tool-result blocks flatten into the same flow.
|
||||
*/
|
||||
function BlockCard(props: { label: string; text: string; rich: RichKit }): ReactNS.ReactElement {
|
||||
const { rich } = props
|
||||
const [mode, setMode] = rich.useRichMode()
|
||||
return (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b>{props.label}</b>
|
||||
<rich.RichSwitch mode={mode} onPick={setMode} inHead />
|
||||
</div>
|
||||
<rich.RichText
|
||||
text={props.text}
|
||||
mode={mode}
|
||||
rawClass="lc-ts-desc-body"
|
||||
mdClass="lc-ts-desc-md"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
function BlocksBody(props: {
|
||||
blocks: readonly unknown[]
|
||||
richable: boolean
|
||||
/** Section label for prose text blocks (answer / content / result). */
|
||||
textLabel: string
|
||||
rich: RichKit
|
||||
img: ImageKit
|
||||
labels: DetailLabels
|
||||
}): ReactNS.ReactElement {
|
||||
const { rich, img, labels } = props
|
||||
const out: ReactNS.ReactNode[] = []
|
||||
let images: ImageRefLike[] = []
|
||||
const flushImages = (): void => {
|
||||
if (images.length === 0) return
|
||||
const group = images
|
||||
images = []
|
||||
out.push(
|
||||
<Section key={'img' + String(out.length)} label={labels.images} count={group.length}>
|
||||
<div className="lc-att-grid">
|
||||
{group.map((a, i) => <img.Card key={`${a.attachmentId}:${i}`} attachment={a} load={img.load} />)}
|
||||
</div>
|
||||
</Section>,
|
||||
)
|
||||
}
|
||||
for (const b of props.blocks) {
|
||||
const image = imageRefOf(b)
|
||||
if (image !== null) { images.push(image); continue }
|
||||
flushImages()
|
||||
const blk = b !== null && typeof b === 'object'
|
||||
? b as { type?: unknown; kind?: unknown; text?: unknown; name?: unknown; argsRaw?: unknown; arguments?: unknown; content?: unknown }
|
||||
: null
|
||||
const blockKind = blk !== null
|
||||
? typeof blk.type === 'string' ? blk.type : typeof blk.kind === 'string' ? blk.kind : ''
|
||||
: ''
|
||||
if ((blockKind === 'text' || blockKind === 'reasoning') && typeof blk?.text === 'string') {
|
||||
const label = blockKind === 'reasoning' ? labels.thinking : props.textLabel
|
||||
out.push(props.richable
|
||||
? <TextSection key={out.length} label={label} text={blk.text} rich={rich} />
|
||||
: <RawSection key={out.length} label={label} text={blk.text} />)
|
||||
continue
|
||||
}
|
||||
if (blockKind === 'tool-call') {
|
||||
out.push(<ToolCallCard
|
||||
key={out.length}
|
||||
name={typeof blk?.name === 'string' ? blk.name : '?'}
|
||||
argsRaw={blk?.argsRaw ?? blk?.arguments}
|
||||
/>)
|
||||
continue
|
||||
}
|
||||
if (blockKind === 'tool-result' && Array.isArray(blk?.content)) {
|
||||
out.push(<BlocksBody
|
||||
key={out.length}
|
||||
blocks={blk.content as unknown[]}
|
||||
richable={false}
|
||||
textLabel={labels.result}
|
||||
rich={rich}
|
||||
img={img}
|
||||
labels={labels}
|
||||
/>)
|
||||
continue
|
||||
}
|
||||
out.push(<RawSection key={out.length} label={labels.other} text={JSON.stringify(b, null, 2)} />)
|
||||
}
|
||||
flushImages()
|
||||
return <>{out}</>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,27 +460,27 @@ function blockSummaryOf(conv: ConversationNodeLike | undefined): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* One tool call as its own card, mirroring the tool-definition parameter
|
||||
* card: the head names the call target (with the parsed argument count),
|
||||
* the body lists the arguments as name/value rows. Arguments that are not
|
||||
* a parseable JSON object fall back to the raw payload inside the same
|
||||
* card. Shared by assistant tool-call blocks (`→`) and the call half of a
|
||||
* tool result (`←`).
|
||||
* One tool call as a Section, mirroring the tool-definition parameter
|
||||
* card: the head names the call target (mono, with the parsed argument
|
||||
* count), the body lists the arguments as name/value rows. Arguments that
|
||||
* are not a parseable JSON object fall back to the raw payload inside the
|
||||
* same card. Shared by assistant tool-call blocks (`→`) and the call half
|
||||
* of a tool result (`←`).
|
||||
*/
|
||||
function ToolCallCard(props: { name: string; argsRaw: unknown; arrow?: string }): ReactNS.ReactElement {
|
||||
const args = React.useMemo(() => parseArgs(props.argsRaw), [props.argsRaw])
|
||||
return (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b className="lc-ts-call-name">{(props.arrow ?? '→') + ' ' + props.name}</b>
|
||||
{args !== null ? <span className="lc-ts-card-count">{Object.keys(args).length}</span> : null}
|
||||
</div>
|
||||
<Section
|
||||
label={(props.arrow ?? '→') + ' ' + props.name}
|
||||
labelClass="lc-ts-call-name"
|
||||
count={args !== null ? Object.keys(args).length : undefined}
|
||||
>
|
||||
{args !== null
|
||||
? Object.keys(args).map(k => <CallArgRow key={k} name={k} value={args[k]} />)
|
||||
: typeof props.argsRaw === 'string' && props.argsRaw !== ''
|
||||
? <pre className="lc-br-pre lc-br-dim">{props.argsRaw}</pre>
|
||||
? <pre className="lc-ts-desc-body lc-br-dim">{props.argsRaw}</pre>
|
||||
: null}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -423,138 +498,57 @@ function CallArgRow(props: { name: string; value: unknown }): ReactNS.ReactEleme
|
||||
)
|
||||
}
|
||||
|
||||
/** The actual content of one surface element, joined from the conversation snapshot. */
|
||||
/**
|
||||
* The actual content of one surface element, joined from the conversation
|
||||
* snapshot — rendered as a uniform Section stack (see BlocksBody); the
|
||||
* element row's open body wraps the stack in `lc-br-content`.
|
||||
*/
|
||||
function NodeContent(props: {
|
||||
node: SurfaceNode
|
||||
conv: ConversationNodeLike | undefined
|
||||
hint: string
|
||||
rich: RichKit
|
||||
img: ImageKit
|
||||
labels: { thinking: string; answer: string; images: string; other: string }
|
||||
labels: DetailLabels
|
||||
}): ReactNS.ReactElement {
|
||||
const { node, conv, rich, img } = props
|
||||
// The raw/markdown toggle serves the prose bodies (user / injection text,
|
||||
// compaction summaries); assistant thinking/answer blocks each carry
|
||||
// their own per-card switch (BlockCard); 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>
|
||||
|
||||
const { node, conv, rich, img, labels } = props
|
||||
if (conv === undefined) {
|
||||
// The join missed (node outside the loaded window): the 80-char preview
|
||||
// still shows as a plain content section, with the window note below.
|
||||
if (node.text === undefined || node.text === '') {
|
||||
return <div className="lc-br-content"><div className="lc-br-note">{props.hint}</div></div>
|
||||
return <div className="lc-br-note">{props.hint}</div>
|
||||
}
|
||||
return wrap(mode => (
|
||||
return (
|
||||
<>
|
||||
<rich.RichText text={node.text ?? ''} mode={mode} />
|
||||
<TextSection label={labels.content} text={node.text} rich={rich} />
|
||||
<div className="lc-br-note">{props.hint}</div>
|
||||
</>
|
||||
))
|
||||
)
|
||||
}
|
||||
if (conv.kind === 'assistant' && Array.isArray(conv.blocks)) {
|
||||
// Thinking, answer, and tool-call blocks render as SEPARATE cards (the
|
||||
// prose cards own a raw/markdown switch, the call card parses its
|
||||
// arguments into rows); images keep their raw form.
|
||||
return (
|
||||
<div className="lc-br-content">
|
||||
{conv.blocks.map((b, i) => {
|
||||
const blk = b as { kind?: string; text?: unknown; name?: string; argsRaw?: unknown }
|
||||
if (blk.kind === 'text' && typeof blk.text === 'string') {
|
||||
return <BlockCard key={i} label={props.labels.answer} text={blk.text} rich={rich} />
|
||||
}
|
||||
if (blk.kind === 'reasoning' && typeof blk.text === 'string') {
|
||||
return <BlockCard key={i} label={props.labels.thinking} text={blk.text} rich={rich} />
|
||||
}
|
||||
if (blk.kind === 'tool-call') {
|
||||
return <ToolCallCard key={i} name={blk.name ?? '?'} argsRaw={blk.argsRaw} />
|
||||
}
|
||||
const image = imageRefOf(b)
|
||||
if (image !== null) return <img.Card key={i} attachment={image} load={img.load} />
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
return <BlocksBody blocks={conv.blocks} richable textLabel={labels.answer} rich={rich} img={img} labels={labels} />
|
||||
}
|
||||
if (conv.kind === 'tool-result') {
|
||||
return (
|
||||
<div className="lc-br-content">
|
||||
<>
|
||||
{conv.call != null
|
||||
? <ToolCallCard arrow="←" name={conv.call.name} argsRaw={conv.call.argsRaw} />
|
||||
: null}
|
||||
{Array.isArray(conv.content) ? <RawBlocks blocks={conv.content} mode="raw" rich={rich} img={img} /> : null}
|
||||
</div>
|
||||
{Array.isArray(conv.content)
|
||||
? <BlocksBody blocks={conv.content} richable={false} textLabel={labels.result} rich={rich} img={img} labels={labels} />
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (conv.kind === 'compaction') {
|
||||
return wrap(mode => (
|
||||
<>
|
||||
{typeof conv.summary === 'string' && conv.summary !== ''
|
||||
? <rich.RichText text={conv.summary} mode={mode} />
|
||||
: null}
|
||||
</>
|
||||
))
|
||||
return typeof conv.summary === 'string' && conv.summary !== ''
|
||||
? <TextSection label={labels.summary} text={conv.summary} rich={rich} />
|
||||
: <></>
|
||||
}
|
||||
if (Array.isArray(conv.content)) {
|
||||
const blocks = conv.content as unknown[]
|
||||
// User messages get the card layout: prose in the toggle-served text
|
||||
// card, durable image refs as attachment cards (thumbnail + name + dims
|
||||
// + size), and anything else as a raw JSON card. A text-only message
|
||||
// renders exactly as before — one rich card and nothing else.
|
||||
if (node.cat !== 'user') {
|
||||
return wrap(mode => <RawBlocks blocks={blocks} mode={mode} rich={rich} img={img} />)
|
||||
}
|
||||
const texts: string[] = []
|
||||
const images: ImageRefLike[] = []
|
||||
const others: unknown[] = []
|
||||
for (const b of blocks) {
|
||||
const blk = b as { type?: string; text?: unknown } | null
|
||||
if (blk !== null && typeof blk === 'object' && blk.type === 'text' && typeof blk.text === 'string') {
|
||||
texts.push(blk.text)
|
||||
continue
|
||||
}
|
||||
const image = imageRefOf(b)
|
||||
if (image !== null) { images.push(image); continue }
|
||||
others.push(b)
|
||||
}
|
||||
return (
|
||||
<div className="lc-br-content">
|
||||
{texts.length > 0 ? (
|
||||
// Un-padded RichCard: the outer lc-br-content already owns the
|
||||
// body indent, so the text card aligns with the attachment cards
|
||||
// instead of sitting one indent deeper.
|
||||
<rich.RichCard className="lc-br-rich-inline" render={mode => (
|
||||
<>
|
||||
{texts.map((text, i) => <rich.RichText key={i} text={text} mode={mode} />)}
|
||||
</>
|
||||
)} />
|
||||
) : null}
|
||||
{images.length > 0 ? (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b>{props.labels.images}</b>
|
||||
<span className="lc-ts-card-count">{images.length}</span>
|
||||
</div>
|
||||
<div className="lc-att-grid">
|
||||
{images.map((a, i) => <img.Card key={`${a.attachmentId}:${i}`} attachment={a} load={img.load} />)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{others.length > 0 ? (
|
||||
<div className="lc-ts-card">
|
||||
<div className="lc-ts-card-head">
|
||||
<b>{props.labels.other}</b>
|
||||
<span className="lc-ts-card-count">{others.length}</span>
|
||||
</div>
|
||||
{others.map((b, i) => <pre key={i} className="lc-br-pre lc-br-dim">{JSON.stringify(b, null, 2)}</pre>)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
return <BlocksBody blocks={conv.content} richable textLabel={labels.content} rich={rich} img={img} labels={labels} />
|
||||
}
|
||||
return <div className="lc-br-content"><div className="lc-br-note">{props.hint}</div></div>
|
||||
return <div className="lc-br-note">{props.hint}</div>
|
||||
}
|
||||
|
||||
/** Group an assembled surface's nodes by category (for per-category counts). */
|
||||
@@ -731,23 +725,23 @@ export function makeContextBrowser(
|
||||
}
|
||||
const toggleElem = (key: string) => { setOpenElem(openElem === key ? null : key) }
|
||||
|
||||
/** One expandable element row (preview line; content when open). */
|
||||
/** One expandable element row (preview line; the open body is the
|
||||
* uniform indented section stack — `lc-br-content`). */
|
||||
const elemRow = (
|
||||
key: string, tag: string | null, preview: string,
|
||||
tokens: number, time: number | undefined, body: ReactNS.ReactNode,
|
||||
inv = false,
|
||||
) => {
|
||||
const open = openElem === key
|
||||
return (
|
||||
<div key={key} className={'lc-br-elem' + (open ? ' lc-br-elem-on' : '')}>
|
||||
<button type="button" className="lc-br-elem-row" onClick={() => { toggleElem(key) }}>
|
||||
<span className={'lc-br-chev' + (open ? ' lc-br-chev-on' : '')}>{'▸'}</span>
|
||||
{tag !== null ? <span className={'lc-br-tag' + (inv ? ' lc-br-tag-inv' : '')}>{tag}</span> : null}
|
||||
{tag !== null ? <span className="lc-br-tag">{tag}</span> : null}
|
||||
<span className="lc-br-preview">{preview}</span>
|
||||
{time !== undefined ? <span className="lc-br-time">{fmtTime(time)}</span> : null}
|
||||
<span className="lc-br-tokens">{'≈' + fmt(tokens)}</span>
|
||||
</button>
|
||||
{open ? body : null}
|
||||
{open ? <div className="lc-br-content">{body}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -758,7 +752,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,
|
||||
<rich.RichCard render={mode => <rich.RichText text={system} mode={mode} />} />)
|
||||
<TextSection label={catLabel('system')} text={system} rich={rich} />)
|
||||
}
|
||||
if (c === 'tools') {
|
||||
if (view.header === null) return <div className="lc-br-note">{t(headers === null ? 'browser.noHeader' : 'browser.noEpoch')}</div>
|
||||
@@ -776,9 +770,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} rich={rich} labels={labels} />
|
||||
</div>)
|
||||
<ToolSchema description={tool.description} schema={tool.schema} rich={rich} labels={labels} />)
|
||||
})
|
||||
}
|
||||
// Surface-node categories carry per-element timestamps; list them
|
||||
@@ -786,16 +778,12 @@ export function makeContextBrowser(
|
||||
const nodes = (byCat[c as Category] ?? []).slice().reverse()
|
||||
return nodes.map((n) => {
|
||||
// Tag/preview split: the compact chip carries the compact fact (tool
|
||||
// name, injection form), the preview line carries the text — each
|
||||
// fact shown once. Tool-bearing rows (tool results, assistant turns
|
||||
// with calls) lead with an INVERTED breadcrumb chip naming the
|
||||
// tool(s) so they scan apart from prose rows.
|
||||
// name, injection form) — one shared subtle chip style — and the
|
||||
// preview line carries the text, each fact shown once.
|
||||
let tag: string | null = null
|
||||
let inv = false
|
||||
let preview = nodeText(n)
|
||||
if (n.cat === 'tool') {
|
||||
tag = (n.tool ?? '?') + (n.err ? ' ⚠' : '')
|
||||
inv = true
|
||||
// A call that summarizes itself (bash's `description`, the path
|
||||
// of an edit/read call) previews with that line in the collapsed
|
||||
// row; the generic result label only when the call says nothing.
|
||||
@@ -805,7 +793,6 @@ export function makeContextBrowser(
|
||||
// then carries the reply text, or the first call's own summary
|
||||
// (description / target path) for a text-less turn.
|
||||
tag = n.calls.join(' › ')
|
||||
inv = true
|
||||
preview = (n.text !== undefined && n.text !== '' ? n.text : null)
|
||||
?? blockSummaryOf(bySeq.get(n.seq))
|
||||
?? t('node.empty')
|
||||
@@ -825,12 +812,15 @@ export function makeContextBrowser(
|
||||
conv={bySeq.get(n.seq)}
|
||||
rich={rich}
|
||||
img={{ Card: ImageCard, load: props.loadImage }}
|
||||
// Localized block/section titles (thinking / answer / image and
|
||||
// other-content cards), handed in by the parent so the body
|
||||
// stays a pure function of props.
|
||||
// Localized section titles (thinking / answer / content /
|
||||
// result / summary / image and other-content cards), handed in
|
||||
// by the parent so the body stays a pure function of props.
|
||||
labels={{
|
||||
thinking: t('block.thinking'),
|
||||
answer: t('block.answer'),
|
||||
content: t('block.content'),
|
||||
result: t('block.result'),
|
||||
summary: t('block.summary'),
|
||||
images: t('attach.images'),
|
||||
other: t('attach.other'),
|
||||
}}
|
||||
@@ -840,7 +830,7 @@ export function makeContextBrowser(
|
||||
hint={bySeq.get(n.seq) === undefined && awaiting
|
||||
? t('browser.loading')
|
||||
: t('browser.noContent')}
|
||||
/>, inv)
|
||||
/>)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
/**
|
||||
* RichText — the raw/markdown view switch shared by the Context browser's
|
||||
* detail cards (system prompt, tool description, user/assistant message and
|
||||
* injection bodies).
|
||||
* RichText — the raw/markdown view shared by the Context browser's detail
|
||||
* section cards (system prompt, tool description, 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.
|
||||
* Every detail section wears the same card chrome (`lc-ts-card` + head —
|
||||
* see browser.tsx's Section); the Raw/MD segmented switch always sits at
|
||||
* the head's right edge (`RichSwitch`, per-card state via `useRichMode`),
|
||||
* and `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>. Both bodies share the
|
||||
* section text chrome (`lc-ts-desc-body` / `lc-ts-desc-md`).
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
@@ -24,27 +23,10 @@ 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. */
|
||||
RichText: (props: { text: string; mode: RichMode }) => ReactNS.ReactElement
|
||||
/** The segmented switch, placed at a section head's right edge. */
|
||||
RichSwitch: (props: { mode: RichMode; onPick: (mode: RichMode) => void }) => ReactNS.ReactElement
|
||||
/** Per-card mode state for the section that places the switch. */
|
||||
useRichMode: () => [RichMode, (mode: RichMode) => void]
|
||||
}
|
||||
|
||||
@@ -62,7 +44,7 @@ export function makeRichText(kit: ViewKit): RichKit {
|
||||
// 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 {
|
||||
function RichSwitch(props: { mode: RichMode; onPick: (mode: RichMode) => void }): ReactNS.ReactElement {
|
||||
const seg = (m: RichMode, label: string, tip: string) => (
|
||||
<button
|
||||
type="button"
|
||||
@@ -72,37 +54,19 @@ export function makeRichText(kit: ViewKit): RichKit {
|
||||
>{label}</button>
|
||||
)
|
||||
return (
|
||||
<span className={'lc-rich-seg' + (props.inHead === true ? ' lc-rich-seg-head' : '')}>
|
||||
<span className="lc-rich-seg">
|
||||
{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 {
|
||||
function RichText(props: { text: string; mode: RichMode }): ReactNS.ReactElement {
|
||||
if (props.mode === 'md') {
|
||||
return <div className={props.mdClass ?? 'lc-br-md'}><MarkdownText text={props.text} /></div>
|
||||
return <div className="lc-ts-desc-md"><MarkdownText text={props.text} /></div>
|
||||
}
|
||||
return <pre className={props.rawClass ?? 'lc-br-pre'}>{props.text}</pre>
|
||||
return <pre className="lc-ts-desc-body">{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 }
|
||||
return { RichText, RichSwitch, useRichMode }
|
||||
}
|
||||
@@ -105,6 +105,7 @@ export const DICT_ZH: Record<string, string> = {
|
||||
// mirroring the harness's own block + locale vocabulary).
|
||||
'block.thinking': '思考',
|
||||
'block.answer': '回答',
|
||||
'block.content': '内容', 'block.result': '结果', 'block.summary': '摘要',
|
||||
// Attachment cards in a message detail: image refs render as thumbnail
|
||||
// cards (name + one labeled row per known fact — raw dims, sent dims +
|
||||
// size, estimated token cost), anything unrecognized falls into the
|
||||
@@ -221,6 +222,7 @@ export const DICT_EN: Record<string, string> = {
|
||||
// mirroring the harness's own block + locale vocabulary).
|
||||
'block.thinking': 'Reasoning',
|
||||
'block.answer': 'Response',
|
||||
'block.content': 'Content', 'block.result': 'Result', 'block.summary': 'Summary',
|
||||
// Attachment cards in a message detail: image refs render as thumbnail
|
||||
// cards (name + one labeled row per known fact — raw dims, sent dims +
|
||||
// size, estimated token cost), anything unrecognized falls into the
|
||||
|
||||
+27
-45
@@ -350,30 +350,20 @@
|
||||
.lc-br-elem-on { background: var(--dsw-alias-interactive-bg-active); }
|
||||
.lc-br-elem-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 5px 6px; cursor: pointer; text-align: left; border-radius: 6px; transition: background-color 120ms ease; }
|
||||
.lc-br-elem-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
||||
/* The ONE element-row chip (tool name, injection form): subtle secondary
|
||||
label on the layer-1 surface — the same tag for every row kind. */
|
||||
.lc-br-tag { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/* Inverted tool-name breadcrumb (tool results + assistant turns with
|
||||
calls): label/surface tokens swap, so it reads black-on-white in light
|
||||
mode and white-on-black in dark mode; mono — these are identifiers. */
|
||||
.lc-br-tag-inv { color: var(--dsw-alias-bg-layer-1); background: var(--dsw-alias-label-primary); border-color: transparent; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.lc-br-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lc-br-time { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; }
|
||||
.lc-br-content { padding: 2px 6px 8px 26px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.lc-br-pre { margin: 0; padding: 8px 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; white-space: pre-wrap; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }
|
||||
.lc-br-dim { color: var(--dsw-alias-label-secondary); }
|
||||
/*
|
||||
Assistant-message block title: a slim label line above each block
|
||||
(thinking / answer) so a mixed reply stays scannable — same weight and
|
||||
tone as the tool-schema card heads, without an extra bordered card.
|
||||
*/
|
||||
.lc-br-blk { display: flex; flex-direction: column; gap: 4px; }
|
||||
.lc-br-blk-label { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); }
|
||||
/*
|
||||
---- Tool schema body (browser card, tools category). The description
|
||||
and parameter table share one "card with a small title" chrome — a
|
||||
labeled bordered block so a reader can scan three stacked sections
|
||||
without losing track of which is which. The raw JSON sits below as a
|
||||
collapsed toggle (no card chrome — it's a debug view, not a primary
|
||||
section). ----
|
||||
---- Detail sections (the expanded element's content stack). EVERYTHING in
|
||||
an open element body is a Section: the same card with a labeled head
|
||||
(title left; count badge / Raw-MD switch right) over a body — prose, tool
|
||||
calls, parameter rows, image grids, and raw JSON share this one frame.
|
||||
The raw-schema JSON sits below the cards as a collapsed toggle (no card
|
||||
chrome — it's a debug view, not a primary section). ----
|
||||
*/
|
||||
.lc-ts-card { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; }
|
||||
.lc-ts-card-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
||||
@@ -382,7 +372,10 @@
|
||||
line (mono: it is an identifier, not prose). */
|
||||
.lc-ts-call-name { font-size: 13px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.lc-ts-card-count { margin-left: auto; }
|
||||
.lc-ts-desc-body { margin: 0; padding: 8px 10px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto; scrollbar-width: thin; }
|
||||
/* The ONE text body chrome (raw <pre> and rendered markdown alike): plain
|
||||
padded text on the section card, internally scrolled past the cap. `dim`
|
||||
tones down non-prose payloads (reasoning traces, raw JSON). */
|
||||
.lc-ts-desc-body { margin: 0; padding: 8px 10px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }
|
||||
/*
|
||||
Grid: name + type + required chip + description. Columns wrap on narrow
|
||||
rows; the description cell spans the full second row so a long blurb
|
||||
@@ -418,21 +411,16 @@
|
||||
.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. ----
|
||||
---- Raw/markdown view switch: a segmented pill mirroring the trend
|
||||
chart's 步骤/轮次 control (`lc-gran`), always at a section head's right
|
||||
edge. The markdown body shares the raw text 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 { margin-left: auto; 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; }
|
||||
.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: 320px; 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
|
||||
@@ -446,26 +434,20 @@
|
||||
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; }
|
||||
.lc-ts-desc-md > div { font-size: 13px; line-height: 1.55; }
|
||||
.lc-ts-desc-md div :is(h1, h2, h3, h4, h5, h6) { font-size: 13px; font-weight: 600; margin: 10px 0 4px; }
|
||||
.lc-ts-desc-md div p { margin: 6px 0; }
|
||||
.lc-ts-desc-md div pre { margin: 6px 0; }
|
||||
.lc-ts-desc-md div :not(pre) > code { font-size: 1em !important; line-height: 1.3; padding: 1px 5px; box-decoration-break: clone; }
|
||||
.lc-ts-desc-md div table code { font-size: inherit; }
|
||||
/*
|
||||
Un-padded RichCard variant: used when the text card is one section
|
||||
among several (attachment cards) inside an already-indented body.
|
||||
*/
|
||||
.lc-br-rich-inline { display: flex; flex-direction: column; gap: 6px; }
|
||||
/*
|
||||
---- Attachment cards (image refs in a message detail). The card chrome
|
||||
reuses the tool-schema sections' `lc-ts-card`/`lc-ts-card-head`; only the
|
||||
image grid and tile are new. The grid is two EQUAL-WIDTH columns
|
||||
---- Attachment cards (image refs in a message detail): tiles live in the
|
||||
shared Section frame (`lc-ts-card`/`lc-ts-card-head`); only the grid and
|
||||
tile are new. The grid is two EQUAL-WIDTH columns
|
||||
(minmax(0,…) so long names shrink instead of stretching the track); each
|
||||
image is a 64px cover tile (the chat history's tile size) beside a name +
|
||||
dims + size + estimated token cost column. ----
|
||||
*/
|
||||
.lc-ts-card .lc-br-pre { margin: 8px 10px; }
|
||||
.lc-att-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 8px 10px; }
|
||||
.lc-att-item { display: flex; gap: 8px; align-items: center; min-width: 0; }
|
||||
.lc-att-thumb { width: 64px; height: 64px; flex: none; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; background: var(--dsw-alias-bg-layer-1); cursor: pointer; font: inherit; }
|
||||
|
||||
@@ -63,7 +63,7 @@ test('assistant blocks: thinking/answer/tool-call split into cards, prose switch
|
||||
const badCard = cards[3]
|
||||
assert.match(textOf(byClass(badCard, 'lc-ts-card-head')[0]), /→ write/, 'fallback card names the tool')
|
||||
assert.equal(byClass(badCard, 'lc-ts-arg-row').length, 0, 'fallback card has no argument rows')
|
||||
assert.match(textOf(byClass(badCard, 'lc-br-pre')[0]), /\{bad json/, 'fallback card shows the raw payload')
|
||||
assert.match(textOf(byClass(badCard, 'lc-ts-desc-body')[0]), /\{bad json/, 'fallback card shows the raw payload')
|
||||
|
||||
// Flip the THINKING card to raw: only its own view changes.
|
||||
byClass(segsOf()[0], 'lc-rich-seg-btn')[0].args[1].onClick()
|
||||
@@ -142,7 +142,7 @@ test('tool result: the call half renders as a card with name/value argument rows
|
||||
assert.doesNotMatch(rowText, /node\.toolResult/, 'generic result label replaced')
|
||||
const toolTag = byClass(toolRow, 'lc-br-tag')[0]
|
||||
assert.equal(textOf(toolTag), 'bash', 'tool row leads with the tool-name chip')
|
||||
assert.match(toolTag.args[1].className, /lc-br-tag-inv/, 'tool-name chip is inverted')
|
||||
assert.ok(!/lc-br-tag-inv/.test(toolTag.args[1].className), 'tool-name chip uses the shared subtle tag style')
|
||||
|
||||
brSlots[2][1]('n3') // expand the tool-result element
|
||||
tr = renderView()
|
||||
@@ -157,8 +157,8 @@ test('tool result: the call half renders as a card with name/value argument rows
|
||||
assert.equal(byClass(callCard, 'lc-ts-param-type').length, 0, 'no type column')
|
||||
assert.match(textOf(tr), /RESULT-TEXT/, 'the result payload still renders below the card')
|
||||
|
||||
// The text-less ASSISTANT turns preview the same way: an inverted
|
||||
// tool-name breadcrumb chip leads, then the call's summary (bash's
|
||||
// The text-less ASSISTANT turns preview the same way: a tool-name
|
||||
// breadcrumb chip leads, then the call's summary (bash's
|
||||
// description, an edit call's target path) — while a turn WITH text
|
||||
// keeps the text preview (no chip: it made no calls).
|
||||
brSlots[1][1]('assistant')
|
||||
@@ -167,7 +167,7 @@ test('tool result: the call half renders as a card with name/value argument rows
|
||||
assert.equal(asstRows.length, 3, 'all three assistant turns listed (newest first)')
|
||||
const editTag = byClass(asstRows[0], 'lc-br-tag')[0]
|
||||
assert.equal(textOf(editTag), 'edit', 'edit turn leads with its tool-name chip')
|
||||
assert.match(editTag.args[1].className, /lc-br-tag-inv/, 'assistant chip is inverted too')
|
||||
assert.ok(!/lc-br-tag-inv/.test(editTag.args[1].className), 'assistant chip shares the subtle tag style too')
|
||||
assert.ok(textOf(asstRows[0]).includes('/tmp/b.ts'), 'edit turn previews the target path')
|
||||
const asstTag = byClass(asstRows[1], 'lc-br-tag')[0]
|
||||
assert.equal(textOf(asstTag), 'bash', 'assistant row leads with the tool-name chip')
|
||||
|
||||
@@ -93,7 +93,7 @@ test('context browser: picker, accordion, reconstruction, header content, deltas
|
||||
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(textOf(byClass(tr, 'lc-ts-desc-body')[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()
|
||||
@@ -132,12 +132,12 @@ test('context browser: picker, accordion, reconstruction, header content, deltas
|
||||
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,
|
||||
assert.equal(byClass(tr, 'lc-br-body')[0] && byClass(byClass(tr, 'lc-br-body')[0], 'lc-ts-desc-body').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')
|
||||
assert.match(textOf(byClass(tr, 'lc-ts-desc-body')[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')
|
||||
@@ -189,7 +189,7 @@ test('context browser: picker, accordion, reconstruction, header content, deltas
|
||||
'array parameters render their element type')
|
||||
// Raw JSON is collapsed by default — the toggle is visible but the schema
|
||||
// string does NOT appear in the rendered text yet.
|
||||
assert.equal(byClass(tr, 'lc-br-pre').filter(n => /"parameters"/.test(textOf(n))).length, 0,
|
||||
assert.equal(byClass(tr, 'lc-ts-desc-body').filter(n => /"parameters"/.test(textOf(n))).length, 0,
|
||||
'raw JSON stays collapsed behind the toggle by default')
|
||||
const toggle = byClass(tr, 'lc-ts-json-toggle')[0]
|
||||
assert.ok(toggle, 'JSON toggle button is rendered')
|
||||
@@ -197,12 +197,12 @@ test('context browser: picker, accordion, reconstruction, header content, deltas
|
||||
// Expanding the toggle reveals the schema; clicking again collapses it.
|
||||
toggle.args[1].onClick()
|
||||
tr = renderView()
|
||||
assert.match(textOf(byClass(tr, 'lc-br-pre')[0]), /"parameters"/, 'expanding reveals the raw JSON')
|
||||
assert.match(textOf(byClass(tr, 'lc-ts-desc-body')[0]), /"parameters"/, 'expanding reveals the raw JSON')
|
||||
const collapseToggle = byClass(tr, 'lc-ts-json-toggle')[0]
|
||||
assert.match(textOf(collapseToggle), /Collapse|收起/, 'toggle label flips to the hide label')
|
||||
collapseToggle.args[1].onClick()
|
||||
tr = renderView()
|
||||
assert.equal(byClass(tr, 'lc-br-pre').length, 0, 'collapsing the toggle removes the JSON block')
|
||||
assert.equal(byClass(tr, 'lc-ts-desc-body').length, 0, 'collapsing the toggle removes the JSON block')
|
||||
|
||||
// Without the contextHeaders key (older host), those sections degrade to a note.
|
||||
bed.headersValue = undefined
|
||||
|
||||
@@ -244,14 +244,14 @@ export async function bootViewBed() {
|
||||
if (node === null || typeof node !== 'object') return node
|
||||
if (node.kind === 'element') {
|
||||
const [type, props, ...children] = node.args
|
||||
// The fake createElement keeps children in args; real React puts them
|
||||
// on props.children (single child as-is, several as an array) — merge
|
||||
// them back for BOTH component kinds, exactly what React would pass.
|
||||
const childrenOf = children.length === 0 ? {} : children.length === 1 ? { children: children[0] } : { children }
|
||||
if (typeof type === 'function' && type.prototype !== undefined && typeof type.prototype.render === 'function') {
|
||||
const key = path + '/' + (type.name || 'anon') + '#' + fnIdx
|
||||
// The fake createElement keeps children in args, not props; real React
|
||||
// puts them on props.children (single child as-is, several as an
|
||||
// array), which the class instance's render() reads. Constructing with
|
||||
// `new` runs the real constructor, so props AND the initial state are
|
||||
// exactly what React would set.
|
||||
const childrenOf = children.length === 0 ? {} : children.length === 1 ? { children: children[0] } : { children }
|
||||
// Constructing with `new` runs the real constructor, so props AND
|
||||
// the initial state are exactly what React would set.
|
||||
const inst = new type(Object.assign({}, props || {}, childrenOf))
|
||||
bed.classInstances.set(key, inst)
|
||||
try {
|
||||
@@ -272,7 +272,7 @@ export async function bootViewBed() {
|
||||
bed.hookStates.set(key, currentHooks)
|
||||
}
|
||||
hookCursor = 0
|
||||
return evaluate(type(props), key)
|
||||
return evaluate(type(Object.assign({}, props || {}, childrenOf)), key)
|
||||
}
|
||||
const kids = []
|
||||
let f = 0
|
||||
|
||||
@@ -33,13 +33,14 @@ test('image attachment cards: card layout, metadata degradation, resolveImage th
|
||||
brSlots[1][1]('user') // openCat('user')
|
||||
brSlots[2][1]('n1') // expand the user element
|
||||
let trImg = renderView()
|
||||
// Metadata-only degradation (no conversation service armed): two attachment
|
||||
// cards (images + other), two image items, placeholder tiles, no <img>.
|
||||
assert.equal(byClass(trImg, 'lc-ts-card').length, 2, 'images card + other-content card rendered')
|
||||
// Metadata-only degradation (no conversation service armed): three section
|
||||
// cards (content text + images + other), two image items, placeholder
|
||||
// tiles, no <img>.
|
||||
assert.equal(byClass(trImg, 'lc-ts-card').length, 3, 'content + images + other-content cards rendered')
|
||||
assert.equal(byClass(trImg, 'lc-att-item').length, 2, 'one item per image ref')
|
||||
assert.equal(byClass(trImg, 'lc-att-thumb').length, 2, 'one tile per image')
|
||||
assert.equal(byClass(trImg, 'lc-att-ph').length, 2, 'placeholder tiles without the loader')
|
||||
const imgCardText = textOf(byClass(trImg, 'lc-ts-card')[0])
|
||||
const imgCardText = textOf(byClass(trImg, 'lc-ts-card')[1])
|
||||
assert.match(imgCardText, /screenshot\.png/, 'named image shows its display name')
|
||||
assert.match(imgCardText, /Raw6000×4000/, 'pre-normalization dims shown when present')
|
||||
assert.match(imgCardText, /Sent2048×1365 · 153\.6 kB/, 'normalized dims + stored byte size shown')
|
||||
@@ -52,7 +53,7 @@ test('image attachment cards: card layout, metadata degradation, resolveImage th
|
||||
// Labeled rows: raw + sent + token on the first card, sent + token on the
|
||||
// second (no originalDimensions recorded → no Raw row).
|
||||
assert.equal(byClass(trImg, 'lc-att-row').length, 5, 'one labeled row per known fact')
|
||||
assert.match(textOf(byClass(trImg, 'lc-ts-card')[1]), /file:\/\/x/, 'unrecognized blocks keep the raw JSON card')
|
||||
assert.match(textOf(byClass(trImg, 'lc-ts-card')[2]), /file:\/\/x/, 'unrecognized blocks keep the raw JSON card')
|
||||
assert.match(textOf(byClass(trImg, 'lc-br-content')[0]), /WHAT-IS-THIS/, 'prose stays in the text card')
|
||||
assert.equal(byClass(trImg, 'lc-att-thumb')[0].args.slice(2).filter(c => c !== null && typeof c === 'object' && c.args[0] === 'img').length, 0, 'no thumbnail img without the loader')
|
||||
// Arm the conversation service: the loader resolves a display URL per ref.
|
||||
|
||||
Reference in new issue
Block a user