From 568d837f8040c3f89b815b7ba496cf84a2ff9257 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Mon, 7 Sep 2026 23:58:34 +0800 Subject: [PATCH] feat(client): add a DNA mode to the Context browser's composition bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A title-row toggle redraws the composition bar as one band per context item — the system prompt, every tool schema, every surface message — in prompt order (the header prefix first, then the message flow by seq), the order the model reads the context and the order items joined it. The header epoch's own seq is bookkeeping (dsh logs request/header at dispatch of the first request using it), never an ordering key: a mid-session refresh must not park the schemas behind the tool results they describe. Bands floor at 0.35% of the occupied region so a tiny item stays a hoverable filament (tooltips still report true shares); hovering names the item with its time, a mirrored category hover lights the category's bands as a group, and clicking a band opens its category + element row below, scrolling it into view. --- src/client/categories.ts | 7 + src/client/components/browser.tsx | 74 +++++++++- src/client/components/stackedBar.tsx | 73 +++++++--- src/client/dna.ts | 55 +++++++ src/client/i18n.ts | 4 + src/client/styles/browser.css | 5 + src/client/styles/stackedBar.css | 2 + tests/client/components/browser.spec.ts | 161 +++++++++++++++++++++ tests/client/components/stackedBar.spec.ts | 92 +++++++++++- tests/client/dna.spec.ts | 78 ++++++++++ 10 files changed, 519 insertions(+), 32 deletions(-) create mode 100644 src/client/dna.ts create mode 100644 tests/client/dna.spec.ts diff --git a/src/client/categories.ts b/src/client/categories.ts index 24a77ac..312d330 100644 --- a/src/client/categories.ts +++ b/src/client/categories.ts @@ -20,6 +20,10 @@ export interface PartsPart { value: number /** Heuristic count shown by the legend and tooltips (defaults to value). */ raw?: number + /** Tooltip display name (defaults to the category label of `key`) — DNA mode names individual items. */ + label?: string + /** Shared-hover group: an incoming hover key equal to a part's group lights it (DNA bands light per category). */ + group?: string } export const CATS: { key: Category | 'system' | 'tools'; color: string }[] = [ @@ -31,6 +35,9 @@ export const CATS: { key: Category | 'system' | 'tools'; color: string }[] = [ { key: 'tool', color: '#14b8a6' }, ] +/** Category key → bar color, for per-item bands (the browser's DNA mode) that bypass the CATS-order part builders. */ +export const CAT_COLOR = Object.fromEntries(CATS.map(c => [c.key, c.color])) as Record + const MESSAGE_CATS: readonly (Category | 'system' | 'tools')[] = ['user', 'inject', 'assistant', 'tool'] export function partsOf(breakdown: Snapshot['current'] | RequestRecord): PartsPart[] { diff --git a/src/client/components/browser.tsx b/src/client/components/browser.tsx index bc53768..a1f0bad 100644 --- a/src/client/components/browser.tsx +++ b/src/client/components/browser.tsx @@ -2,7 +2,9 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ChangeEvent import { UNKNOWN_TOOL_SOURCE, type Category, type ContextHeaders, type ContextTimeline, type HeaderTool, type RequestRecord, type SurfaceNode } from '../../shared/types' import { assemble } from '../assemble' import type { Assembled } from '../assemble' -import { CATS, partsOf } from '../categories' +import { CATS, CAT_COLOR, partsOf } from '../categories' +import { dnaOf } from '../dna' +import type { DnaItem } from '../dna' import type { ContentFetcher, ConversationNodeLike, HeaderFetcher } from '../services' import type { ViewKit } from '../viewkit' import { blockSummaryOf, callSummaryOf, parseCallArgs } from '../callSummary' @@ -564,6 +566,12 @@ function lastOfTurn(requests: RequestRecord[], turn: number): RequestRecord | nu return null } +/** + * DNA bands keep at least this share of the occupied region, so a tiny item (a 25-token user message in a 40k + * context) stays a hoverable/clickable filament instead of a sub-pixel sliver. Tooltips still report true shares. + */ +const DNA_MIN_BAND = 0.35 + export function makeContextBrowser( kit: ViewKit, StackedBar: (props: StackedBarProps) => ReactElement, @@ -589,6 +597,9 @@ export function makeContextBrowser( // compares epochs. const [rowQuery, setRowQuery] = useState('') const [toolSort, setToolSort] = useState<'size' | 'name'>('size') + // DNA mode: the composition bar redraws as ONE band per context item in prompt order (dna.ts), hovered/clicked per item. + const [dna, setDna] = useState(false) + const [dnaKey, setDnaKey] = useState(null) // Every open-category change (toggle, step pick, pin, brief reveal) reports outward so the Context tab // can focus the trend chart on the open category. const onOpenCat = props.onOpenCat @@ -707,6 +718,41 @@ export function makeContextBrowser( const byCat = byCatOf(view) + // DNA mode: per-item bands in prompt order (dna.ts). The band label names the item the way its accordion row would + // (skill name, tool name, injection form, else the category label), with the item's time appended. + const dnaLabel = (it: DnaItem): string => { + let base: string + if (!('node' in it)) { + base = it.cat === 'system' ? catLabel('system') : it.key.slice('tool:'.length) + } else { + const n = it.node + base = n.skill !== undefined ? t('node.skillTag', { name: n.skill }) + : n.cat === 'tool' ? (n.tool ?? '?') + : n.cat === 'inject' ? t('form.' + (n.form || 'context')) + : catLabel(n.cat) + } + return it.time !== undefined ? base + ' · ' + fmtTime(it.time) : base + } + const dnaItems = dna ? dnaOf(view) : null + const dnaByKey = new Map(dnaItems?.map(it => [it.key, it] as const) ?? []) + const dnaParts = dnaItems?.map(it => ({ + key: it.key, + color: CAT_COLOR[it.cat], + value: it.tokens, + label: dnaLabel(it), + group: it.cat, + })) ?? null + // A band click opens its category + element row below (the same reveal the step brief uses) and scrolls it into view. + const pickDna = (key: string): void => { + const it = dnaByKey.get(key) + /* v8 ignore next 1 -- the bar only reports keys of the parts it was handed; defensive. */ + if (it === undefined) return + setCat(it.cat) + setRowQuery('') + setOpenElem(key) + focusScrollRef.current = true + } + const toolCount = (c: string): number => countOf(view, byCat, c) // A category holding exactly one item opens that row with the category, so @@ -960,6 +1006,15 @@ export function makeContextBrowser(
{t('browser.title')} + + + {t('browser.deltaHint')}