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')}