Context Attachments
The context list hanging off a conversation — files, links, snippets and selections with their token cost, under a budget meter that names exactly which entries get dropped when the window overflows.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/context-attachments.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ContextAttachments" component (zod +
lucide-react). It renders the context list attached to one conversation turn —
files, links, snippets and selections — under a budget meter for the model's
context window. It NEVER tokenises anything: counts arrive as props, because
tokenisation is model-specific and belongs next to the model client. What the
component owns is the arithmetic on top of those counts, and that arithmetic is
printed on screen so it can be checked by eye.
Contract
- A zod schema in a sibling contract file is the single source of truth:
ContextAttachment = { id (unique, never an array index), kind: "file" | "url"
| "snippet" | "selection", name, source (full path / URL / "File.swift:120-186"),
bytes?: number, tokens: number | null, pinned?: boolean, digest?: string }.
`contextAttachmentsSchema` adds { status: "loading" | "empty" | "error" |
"ready", items, limit, reservedTokens?, reserveLabel? }.
- `tokens` is THREE-VALUED on purpose: a number is a settled count and `null`
means "still being counted" (a large file is being read). There is no fourth
"stale" value to fall back to, which is what makes it structurally impossible
for a row to show 0 — or the previous number — while the truth is unknown.
Non-finite / negative counts fold into `null`, never into 0: a fake zero would
silently shrink the budget, the one failure this panel exists to prevent.
- `items` is PRIORITY ORDER, highest first. The order is the eviction order;
reordering (drag, recency, relevance score) stays with the consumer.
- Two schema-level refinements: ids must be unique, and reservedTokens must not
exceed limit. Each one guards its own inputs (Array.isArray, typeof) before
dereferencing, because zod runs EVERY refinement — a failure in the first does
not stop the second — so an unguarded `data.items[0].id` on a ragged payload
throws a TypeError out of safeParse instead of returning { success: false }.
- Props extend HTMLAttributes<HTMLDivElement> and add: status, items, limit,
reservedTokens = 0, reserveLabel = "System prompt", locale = "en-US",
label = "Context", onRemove?(id), onRestore?(item, index),
onTogglePin?(id, pinned), onRetry?, emptyState?, skeletonRows? (clamped 1-12).
forwardRef to the root, cn() merges className, rest spreads.
- Affordances are handler-gated: no onTogglePin, no pin buttons; no onRemove, no
remove buttons; no onRetry, no button in the error branch.
Behavior
- THE ARITHMETIC IS THE PRODUCT. reserve + sum(billed rows) = used;
used / limit = the percentage. Those three numbers appear in four places (the
headline figure, the percentage, the legend line "System prompt 8K +
attachments 122K = 130K used · 30K over", and the bar's segment widths) and
they are all derived from one memo, so they cannot disagree. Every compact
figure carries the exact grouped number in `title`, and the meter repeats the
exact pair in aria-valuetext.
- Duplicates: two entries with the same `digest` (or, without one, the same
kind + source) are the same context. The first is billed, later repeats are
flagged "Duplicate", struck through, shown with their own count marked "not
counted", excluded from `used`, and summarised in a notice. Attaching the same
file twice must never cost twice.
- Eviction, not just a red bar: when used > limit, walk the list from the BOTTOM
up, skipping pinned rows, marking rows for eviction until the remainder fits.
Each marked row gets a struck-through name plus a "Will be dropped" badge, and
a notice says how many are going and what is left ("leaving 91.5K of 100K
sent"). Pinning a marked row takes it out of the walk and pushes the next
unpinned row out instead — the plan re-computes from the same memo, so the
numbers never lag the badges. If even dropping every unpinned row leaves the
request too big, say THAT instead ("pinned context alone doesn't fit").
- Rows still being counted are excluded from the walk (their cost is unknown, so
dropping them cannot be shown to help) and the totals are prefixed "≥" with a
note that they are a floor. `<1%` is already a bound and never gets a second one.
- Removal is reversible by construction. With onRestore, removing stashes
{ item, index } and shows an undo bar ("Removed report.md · 24.2K tokens
freed") whose Undo puts the row back at its old index; the bar retires itself
when the item reappears in `items`, so the consumer restoring it another way
needs no effect. WITHOUT onRestore the remove button degrades to two-step
confirmation (trash icon -> "Remove?" -> gone). One of the two always applies:
a big file can never leave on a single stray click.
- Compact notation may not merge distinct numbers. Pick the SMALLEST fraction
digit count whose compact label lands within 0.5% of the true value: at one
digit both 1,050,000 and 1,100,000 print "1.1M", which is a 4.8% lie, so the
first is forced up to "1.05M". Below 10,000 print the exact grouped number.
- The bar's track spans max(used, limit): under budget the fill stops short,
over budget the fill reaches the right edge and the limit LINE moves left
instead. Nothing is ever painted past the track. Three segments in order —
reserve, attachments that fit, attachments that would be dropped — and their
widths are the same three numbers over the same scale.
- Four first-class branches on `status`: loading (skeleton reusing the real
row's geometry + one sr-only role="status"), empty (replaceable, and it still
states the window size and the reserve), error (message + optional retry),
ready. No silent caps: N items render N rows, no max-height clipping.
Rendering & styling
- Semantic tokens only: bg-card root with border and rounded-lg, bg-muted track,
bg-primary for the attachments segment, bg-primary/45 + a --card-mixed stripe
gradient for the reserved slice, bg-destructive/25 + a --destructive hatch for
the doomed slice, bg-destructive for the limit line, bg-muted/40 for rows that
will not reach the model, text-destructive for over-budget figures and the
"Will be dropped" badge, text-muted-foreground for sources and captions. No
hex / rgb / oklch literals, and no chart tokens on text. Texture, strike-through
and badges carry the meaning as well as colour does, so the panel survives a
monochrome palette.
- Semantics: a real `ul` whose only children are `li`; role="meter" with
aria-valuemin/max/now (clamped to max, since valuenow above valuemax is
invalid) and aria-valuetext carrying the real over-budget sentence;
role="group" + aria-labelledby rather than a `section` (a named section is a
landmark and several of them pollute landmark navigation); every icon button
names its row ("Remove report.md from context", "Pin report.md so it is never
dropped" with aria-pressed); one always-mounted sr-only aria-live line
announces removals and restores.
- Long paths use `wrap-anywhere` (NOT `break-words`, which does not lower the
min-content width) plus `min-w-0` on the shrinking column, so a 200-character
unbroken path wraps instead of widening the panel.
- Density adapts to the COMPONENT's width with @container, not viewport
breakpoints; numbers are tabular-nums; the bar animates transition-[width]
with motion-reduce:transition-none and the counting pulse carries
motion-reduce:animate-none.
Customization levers
- Eviction policy: the bottom-up walk is one loop. Swap it for "oldest first",
"cheapest first" or a real knapsack by changing the traversal — the badges,
the notice and the doomed segment all read from the resulting id set.
- Reserve: reservedTokens + reserveLabel model "system prompt + expected reply".
Set it to 0 for a raw window, or raise it to reserve room for tool schemas.
- Counting: keep `tokens: null` while your counter runs and let the panel show
the floor. If your transport can report partial counts, add a `tokensLower`
field rather than writing a guess into `tokens`.
- Removal safety: pass onRestore for undo, omit it for two-step confirmation.
Pick per surface; do not add a third "just delete it" path.
- Compact precision: COMPACT_TOLERANCE (0.5%) and COMPACT_FLOOR (10,000) are the
two knobs. Tighten the tolerance for billing-grade figures; raise the floor if
your window is small enough that every number is short anyway.
- Identity: `digest` is the honest duplicate key. Without it the fallback is
kind + source, which catches "same path attached twice" but not "same bytes
under two names" — hash on the way in if that matters.
- Density: px-3 py-2.5 rows, a size-4 kind icon and an h-2.5 bar read as
comfortable; py-1.5 / h-1.5 gives a sidebar-tight variant without touching the
arithmetic.Concepts
- The arithmetic is on screen — the legend spells out
reserve + attachments = usednext to the headline figure and the percentage, and the bar's three segment widths are those same numbers over the same scale. One memo produces all of them, so there is no arrangement of props that makes two of the four disagree. - Eviction plan, not an alarm — being over budget is not a red bar, it is a named set of rows that will not be sent, computed by walking the list bottom-up and skipping pinned entries. Pinning one row is therefore a real lever: it leaves the set and the next unpinned row takes its place.
- Counting is a value, not a gap —
tokens: number | nullmakes "we don't know yet" a first-class state, so a row can never display0(or the previous file's number) while a 2 MB log is still being tokenised; the totals wear a≥until it lands. - Duplicate by identity, billed once — a repeat of the same
digest(or the same kind + source) is shown, struck through and explained, but excluded from the sum, because the failure mode being prevented is a silent doubling of cost, not the presence of a second row. - Undo or confirm, never neither — passing
onRestoreturns removal into an undoable action that puts the row back at its old index; omitting it degrades the remove button to two-step confirmation. There is no configuration where a 24K-token file leaves on one click. - Compact labels that do not lie — the formatter picks the fewest fraction digits whose compact label lands within 0.5% of the truth, so
1,050,000prints1.05Minstead of joining1,100,000at1.1M; the exact grouped figure is always onetitle(and one screen-reader announcement) away.
Agent Steps
A live agent run whose plan is still being written — steps stream in without moving what is already on screen, failures retry in place, and a replan cancels the rest of the plan instead of failing it.
Prompt Library
A saved-prompt library with fill-in-the-blank templates — search across title, body and tags, faceted categories, a variable form ordered by first appearance, and a preview that is character-for-character what gets inserted.