Context Compaction River

Context window

Context compaction drawn as a meandering river: live turns are points on one curving channel, and a compacted run of turns necks off into a reopenable, re-injectable oxbow lake sitting 12px off the channel.

Install
npx shadcn add https://design.helpmarq.com/r/context-compaction-river.json
Source
registry/core/context-compaction-river/component.tsx
"use client";

import { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";

// OxbowTurn — the live conversation drawn as one meandering channel in a
// side rail. Every live turn is a point the channel passes through; the
// curve itself is decorative (aria-hidden SVG) and carries no information a
// screen reader needs, because the same facts live as plain text in a real
// list right below it ("Live context, N turns" / "Compacted: turns 3-9,
// summarized 2 minutes ago").
//
// When turns get folded into a summary, the component doesn't just drop a
// row — the compaction keeps its own place in the sequence, and the channel
// simply stops routing through it: the surviving points ease toward their
// new (tighter) spacing over ~300ms, which is what "the river gets shorter"
// looks like here, while the folded turns settle 12px off the channel as a
// closed oxbow-lake shape carrying a token-count chip. That chip is a real,
// focusable button (never aria-hidden) — opening it reveals the summary and
// a re-inject action in a role=menu popover. Re-inject animates the lake
// back onto the channel before the prop change actually removes it, so nothing
// is left to imagination between "click" and "the turns are back".
//
// The trigger is deliberately open-only (a second click does not close it —
// only Escape, an outside click, or choosing the menu item does). That is
// what keeps a synthetic "press" pass (which clicks the first control once,
// unconditionally) and a later scripted "open this" check from fighting over
// the same toggle.
//
// Positions are computed from each item's index in the full sequence (turn
// or compaction alike), so a compaction never needs its own bookkeeping of
// "where it used to be" — it already has a slot. Turn dots and the channel
// path animate via a direct rAF loop (setAttribute only, no React state on
// the hot path, sleeps once settled) exactly like this repo's other
// spring-driven diagrams. Everything else (the oxbow's 12px settle, its
// entrance/exit) is a plain CSS transform+opacity transition, which reduced
// motion turns off wholesale in favor of an instant fade-and-move.

export type OxbowTurnEntry = {
  id: string;
  /** short label, e.g. a turn number — used to build "turns 3-9" range text */
  label: string;
};

export type OxbowTurnItem =
  | { kind: "turn"; id: string; label: string; preview?: string }
  | {
      kind: "compaction";
      id: string;
      /** the turns folded into this oxbow, oldest -> newest */
      turns: OxbowTurnEntry[];
      summary: string;
      tokenCount: number;
      /** precomputed, e.g. "2 minutes ago" — this component never touches the clock */
      compactedAgo: string;
    };

export interface OxbowTurnProps {
  /** the full ordered sequence: live turns and compaction (oxbow) entries, interleaved */
  items: OxbowTurnItem[];
  /** fired once a re-inject has finished animating back onto the channel */
  onReinject?: (compactionId: string) => void;
  ariaLabel?: string;
  className?: string;
}

const ROW_H = 38;
const PAD_Y = 18;
const CX = 30; // channel centerline, px from the rail's left edge
const AMP1 = 11;
const AMP2 = 5;
const OFFSET = 12; // brief's "12px off-channel"
const DOT_R = 3;
const EASE_RATE = 0.24; // per-frame lerp toward target, ~300ms settle at 60fps
const SETTLE_EPS = 0.04;
const RAIL_W = 236;

type Pt = { x: number; y: number };

function channelX(row: number): number {
  return CX + AMP1 * Math.sin(row * 0.85) + AMP2 * Math.sin(row * 1.9 + 1.3);
}

function rowY(row: number): number {
  return PAD_Y + row * ROW_H;
}

/** Catmull-Rom through `pts`, converted to cubic beziers (tension/6 form). */
function smoothPath(pts: Pt[]): string {
  if (pts.length === 0) return "";
  if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
  let d = `M ${pts[0].x} ${pts[0].y}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i - 1] ?? pts[i];
    const p1 = pts[i];
    const p2 = pts[i + 1];
    const p3 = pts[i + 2] ?? p2;
    const c1x = p1.x + (p2.x - p0.x) / 6;
    const c1y = p1.y + (p2.y - p0.y) / 6;
    const c2x = p2.x - (p3.x - p1.x) / 6;
    const c2y = p2.y - (p3.y - p1.y) / 6;
    d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;
  }
  return d;
}

function formatTok(n: number): string {
  return Math.round(Math.max(0, n)).toLocaleString("en-US");
}

function turnsLabel(turns: OxbowTurnEntry[]): string {
  if (turns.length === 0) return "no turns";
  if (turns.length === 1) return `turn ${turns[0].label}`;
  return `turns ${turns[0].label}–${turns[turns.length - 1].label}`;
}

type TurnPoint = { id: string; x: number; y: number; label: string; preview?: string; isLast: boolean };
type LakeRow = { id: string; item: Extract<OxbowTurnItem, { kind: "compaction" }>; x: number; y: number };

function layout(items: OxbowTurnItem[]): { turns: TurnPoint[]; lakes: LakeRow[] } {
  const turns: TurnPoint[] = [];
  const lakes: LakeRow[] = [];
  items.forEach((it, row) => {
    const x = channelX(row);
    const y = rowY(row);
    if (it.kind === "turn") {
      turns.push({ id: it.id, x, y, label: it.label, preview: it.preview, isLast: false });
    } else {
      lakes.push({ id: it.id, item: it, x, y });
    }
  });
  if (turns.length > 0) turns[turns.length - 1].isLast = true;
  return { turns, lakes };
}

export function OxbowTurn({
  items,
  onReinject,
  ariaLabel = "Context compaction history",
  className = "",
}: OxbowTurnProps) {
  const rawId = useId();
  const fid = rawId.replace(/[^a-zA-Z0-9-]/g, "");

  const [reduced, setReduced] = useState(false);
  const [openId, setOpenId] = useState<string | null>(null);
  const [enterState, setEnterState] = useState<Record<string, "start" | "end">>({});
  const [exitingIds, setExitingIds] = useState<Record<string, true>>({});
  const [snap, setSnap] = useState(false);
  const [liveMsg, setLiveMsg] = useState("");

  const wrapRef = useRef<HTMLDivElement | null>(null);
  const popRef = useRef<HTMLDivElement | null>(null);
  const triggerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
  const pathElRef = useRef<SVGPathElement | null>(null);
  const dotElsRef = useRef<Map<string, SVGCircleElement>>(new Map());
  const dotWrapElsRef = useRef<Map<string, HTMLDivElement>>(new Map());
  const posRef = useRef<Map<string, Pt>>(new Map());
  const prevCompactionIdsRef = useRef<Set<string>>(new Set());
  const cleanupsRef = useRef<Array<() => void>>([]);

  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    setReduced(mq.matches);
    const onChange = () => setReduced(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);

  useEffect(() => () => {
    for (const cancel of cleanupsRef.current) cancel();
    cleanupsRef.current = [];
  }, []);

  const { turns: turnTargets, lakes: lakeRows } = useMemo(() => layout(items), [items]);
  const H = Math.max(PAD_Y * 2, PAD_Y * 2 + Math.max(0, items.length - 1) * ROW_H);

  const [initialD] = useState(() => smoothPath(layout(items).turns.map((t) => ({ x: t.x, y: t.y }))));

  // channel + turn-dot animation: direct DOM writes, no React state on the hot path
  useEffect(() => {
    const targetMap = new Map(turnTargets.map((t) => [t.id, { x: t.x, y: t.y }]));
    for (const id of Array.from(posRef.current.keys())) {
      if (!targetMap.has(id)) posRef.current.delete(id);
    }

    if (reduced) {
      for (const t of turnTargets) {
        posRef.current.set(t.id, { x: t.x, y: t.y });
        const el = dotElsRef.current.get(t.id);
        el?.setAttribute("cx", String(t.x));
        el?.setAttribute("cy", String(t.y));
        const wrapEl = dotWrapElsRef.current.get(t.id);
        if (wrapEl) {
          wrapEl.style.left = `${t.x}px`;
          wrapEl.style.top = `${t.y}px`;
        }
      }
      pathElRef.current?.setAttribute("d", smoothPath(turnTargets.map((t) => ({ x: t.x, y: t.y }))));
      return;
    }

    let raf = 0;
    const step = () => {
      let moving = false;
      const pts: Pt[] = [];
      for (const t of turnTargets) {
        const cur = posRef.current.get(t.id) ?? { x: t.x, y: t.y };
        const nx = cur.x + (t.x - cur.x) * EASE_RATE;
        const ny = cur.y + (t.y - cur.y) * EASE_RATE;
        if (Math.abs(t.x - nx) > SETTLE_EPS || Math.abs(t.y - ny) > SETTLE_EPS) moving = true;
        posRef.current.set(t.id, { x: nx, y: ny });
        pts.push({ x: nx, y: ny });
        const el = dotElsRef.current.get(t.id);
        el?.setAttribute("cx", String(nx));
        el?.setAttribute("cy", String(ny));
        const wrapEl = dotWrapElsRef.current.get(t.id);
        if (wrapEl) {
          wrapEl.style.left = `${nx}px`;
          wrapEl.style.top = `${ny}px`;
        }
      }
      pathElRef.current?.setAttribute("d", smoothPath(pts));
      if (moving) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [turnTargets, reduced]);

  // compaction enter + aria-live announcements
  useEffect(() => {
    const curIds = new Set(lakeRows.map((l) => l.id));
    const prevIds = prevCompactionIdsRef.current;
    const added = lakeRows.filter((l) => !prevIds.has(l.id));
    const removedCount = Array.from(prevIds).filter((id) => !curIds.has(id)).length;

    if (added.length > 0) {
      if (!reduced) {
        setEnterState((s) => {
          const next = { ...s };
          for (const l of added) next[l.id] = "start";
          return next;
        });
        const raf1 = requestAnimationFrame(() => {
          const raf2 = requestAnimationFrame(() => {
            setEnterState((s) => {
              const next = { ...s };
              for (const l of added) if (next[l.id] === "start") next[l.id] = "end";
              return next;
            });
          });
          cleanupsRef.current.push(() => cancelAnimationFrame(raf2));
        });
        cleanupsRef.current.push(() => cancelAnimationFrame(raf1));
      }
      const first = added[0];
      setLiveMsg(
        `Compacted ${turnsLabel(first.item.turns)} into a ${formatTok(first.item.tokenCount)}-token summary.`
      );
      if (!reduced) {
        setSnap(true);
        const t = window.setTimeout(() => setSnap(false), 320);
        cleanupsRef.current.push(() => window.clearTimeout(t));
      }
    } else if (removedCount > 0 && added.length === 0) {
      // a removal this component didn't itself animate (e.g. reset) — still announce
      setLiveMsg("Re-injected turns back into live context.");
    }
    prevCompactionIdsRef.current = curIds;
  }, [lakeRows, reduced]);

  // outside click closes the open popover
  useEffect(() => {
    if (!openId) return;
    const onDown = (e: PointerEvent) => {
      const target = e.target as Node;
      if (popRef.current?.contains(target)) return;
      if (triggerRefs.current.get(openId)?.contains(target)) return;
      setOpenId(null);
    };
    document.addEventListener("pointerdown", onDown);
    return () => document.removeEventListener("pointerdown", onDown);
  }, [openId]);

  // focus the summary when a popover opens
  useEffect(() => {
    if (!openId) return;
    const raf = requestAnimationFrame(() => {
      popRef.current?.querySelector<HTMLElement>("[data-oxbow-summary]")?.focus({ preventScroll: true });
    });
    return () => cancelAnimationFrame(raf);
  }, [openId]);

  const closeAndReturnFocus = (id: string) => {
    setOpenId(null);
    triggerRefs.current.get(id)?.focus({ preventScroll: true });
  };

  const handleReinject = (l: LakeRow) => {
    if (reduced) {
      setOpenId(null);
      onReinject?.(l.id);
      return;
    }
    setOpenId(null);
    setExitingIds((s) => ({ ...s, [l.id]: true }));
    const t = window.setTimeout(() => {
      onReinject?.(l.id);
      setExitingIds((s) => {
        const next = { ...s };
        delete next[l.id];
        return next;
      });
    }, 260);
    cleanupsRef.current.push(() => window.clearTimeout(t));
  };

  const onTriggerKeyDown = (l: LakeRow) => (e: ReactKeyboardEvent<HTMLButtonElement>) => {
    if (e.key === "ArrowDown" && openId !== l.id) {
      e.preventDefault();
      setOpenId(l.id);
    }
  };

  const onMenuKeyDown = (l: LakeRow) => (e: ReactKeyboardEvent<HTMLDivElement>) => {
    if (e.key === "Escape") {
      e.preventDefault();
      closeAndReturnFocus(l.id);
      return;
    }
    if (e.key === "Tab") {
      setOpenId(null);
      return;
    }
    const items_ = Array.from(e.currentTarget.querySelectorAll<HTMLElement>('[role="menuitem"]'));
    if (items_.length === 0) return;
    const idx = items_.indexOf(document.activeElement as HTMLElement);
    let next = -1;
    if (e.key === "ArrowDown") next = (idx + 1) % items_.length;
    else if (e.key === "ArrowUp") next = idx < 0 ? items_.length - 1 : (idx - 1 + items_.length) % items_.length;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = items_.length - 1;
    if (next >= 0) {
      e.preventDefault();
      items_[next]?.focus({ preventScroll: true });
    }
  };

  const turnCount = turnTargets.length;

  return (
    <div ref={wrapRef} role="group" aria-label={ariaLabel} className={`ns-oxbow relative ${className}`}>
      <style>{`
.ns-oxbow-dot{animation:ns-oxbow-dot-in 220ms cubic-bezier(.22,1,.36,1)}
.ns-oxbow-dot-current{fill:var(--foreground);animation:ns-oxbow-dot-in 220ms cubic-bezier(.22,1,.36,1),ns-oxbow-breathe 2.6s ease-in-out infinite}
.ns-oxbow-channel{transition:none}
.ns-oxbow-channel.ns-oxbow-snap{animation:ns-oxbow-snap 320ms cubic-bezier(.22,1,.36,1)}
.ns-oxbow-lake-anim,.ns-oxbow-chip-wrap{transition:transform 300ms cubic-bezier(.16,1,.3,1),opacity 220ms ease-out}
.ns-oxbow-lake-anim[data-state="entering"],.ns-oxbow-chip-wrap[data-state="entering"]{transform:translateX(0);opacity:0}
.ns-oxbow-lake-anim[data-state="settled"],.ns-oxbow-chip-wrap[data-state="settled"]{transform:translateX(${OFFSET}px);opacity:1}
.ns-oxbow-lake-anim[data-state="exiting"],.ns-oxbow-chip-wrap[data-state="exiting"]{transform:translateX(0);opacity:0;transition:transform 240ms cubic-bezier(.4,0,.9,.4),opacity 200ms ease-in}
.ns-oxbow-chip-tip,.ns-context-compaction-river-tip{opacity:0;transition:opacity 150ms ease-out}
[data-oxbow-trigger]:hover+.ns-oxbow-chip-tip,[data-oxbow-trigger]:focus+.ns-oxbow-chip-tip,[data-oxbow-trigger]:focus-visible+.ns-oxbow-chip-tip{opacity:1}
[data-context-compaction-river-btn]:hover+.ns-context-compaction-river-tip,[data-context-compaction-river-btn]:focus+.ns-context-compaction-river-tip,[data-context-compaction-river-btn]:focus-visible+.ns-context-compaction-river-tip{opacity:1}
@keyframes ns-oxbow-dot-in{from{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}}
@keyframes ns-oxbow-breathe{0%,100%{opacity:.75}50%{opacity:1}}
@keyframes ns-oxbow-snap{0%{stroke-width:1.5}35%{stroke-width:2.6}100%{stroke-width:1.5}}
@media (prefers-reduced-motion: reduce){
  .ns-oxbow-dot,.ns-oxbow-dot-current{animation:none !important;opacity:1 !important;transform:none !important}
  .ns-oxbow-channel{animation:none !important}
  .ns-oxbow-lake-anim,.ns-oxbow-chip-wrap{transition:opacity 160ms ease-out !important;transform:translateX(${OFFSET}px) !important}
  .ns-oxbow-lake-anim[data-state="entering"],.ns-oxbow-chip-wrap[data-state="entering"]{opacity:0 !important}
  .ns-oxbow-lake-anim[data-state="exiting"],.ns-oxbow-chip-wrap[data-state="exiting"]{opacity:0 !important}
  .ns-oxbow-chip-tip,.ns-context-compaction-river-tip{transition:none !important}
}
`}</style>

      <div className="relative" style={{ width: RAIL_W, height: H }}>
        <svg
          aria-hidden
          width={RAIL_W}
          height={H}
          viewBox={`0 0 ${RAIL_W} ${H}`}
          className="pointer-events-none absolute inset-0 block overflow-visible"
        >
          <path
            ref={pathElRef}
            d={initialD}
            className={`ns-oxbow-channel${snap ? " ns-oxbow-snap" : ""}`}
            fill="none"
            stroke="var(--muted)"
            strokeWidth={1.5}
            strokeLinecap="round"
          />

          {lakeRows.map((l) => {
            const state = exitingIds[l.id] ? "exiting" : enterState[l.id] === "start" ? "entering" : "settled";
            return (
              <g key={l.id} transform={`translate(${l.x} ${l.y})`}>
                {/* animated as one rigid unit: at rest (translateX 12) this spans exactly
                    anchor -> ellipse edge; the stub's own local span is kept constant so
                    sliding the group is what "grows the neck into place" looks like */}
                <g className="ns-oxbow-lake-anim" data-state={state}>
                  <path
                    d={`M ${-OFFSET} 0 Q ${-OFFSET * 0.5} -6 0 0`}
                    fill="none"
                    stroke="var(--muted)"
                    strokeWidth={1}
                    strokeOpacity={0.55}
                  />
                  <ellipse
                    cx={15}
                    cy={0}
                    rx={15}
                    ry={9}
                    fill="color-mix(in srgb, var(--border) 6%, transparent)"
                    stroke="var(--border)"
                    strokeWidth={1}
                  />
                </g>
              </g>
            );
          })}

          {turnTargets.map((t) => (
            <circle
              key={t.id}
              ref={(el) => {
                if (el) {
                  dotElsRef.current.set(t.id, el);
                  if (!posRef.current.has(t.id)) {
                    el.setAttribute("cx", String(t.x));
                    el.setAttribute("cy", String(t.y));
                    posRef.current.set(t.id, { x: t.x, y: t.y });
                  }
                } else {
                  dotElsRef.current.delete(t.id);
                }
              }}
              r={t.isLast ? DOT_R + 1 : DOT_R}
              className={t.isLast ? "ns-oxbow-dot-current" : "ns-oxbow-dot"}
              fill={t.isLast ? undefined : "var(--muted)"}
            >
              <title>{t.preview ? `${t.label}: ${t.preview}` : t.label}</title>
            </circle>
          ))}
        </svg>

        {/* real interactive targets for the (decorative, aria-hidden) turn dots above:
            positioned by the same rAF loop that drives the SVG circles (dotWrapElsRef),
            so the hit target and its tooltip track the easing motion instead of a
            React-controlled left/top that would snap ahead of the circle it sits on */}
        {turnTargets.map((t) => {
          const turnTipId = `${fid}-turn-tip-${t.id}`;
          const hasPreview = Boolean(t.preview);
          const accName = hasPreview ? `Turn ${t.label}: ${t.preview}` : `Turn ${t.label}`;
          return (
            <div
              key={t.id}
              ref={(el) => {
                if (el) {
                  dotWrapElsRef.current.set(t.id, el);
                  const cur = posRef.current.get(t.id) ?? { x: t.x, y: t.y };
                  el.style.left = `${cur.x}px`;
                  el.style.top = `${cur.y}px`;
                } else {
                  dotWrapElsRef.current.delete(t.id);
                }
              }}
              className="absolute -translate-x-1/2 -translate-y-1/2"
            >
              <button
                type="button"
                data-context-compaction-river-btn
                aria-label={accName}
                aria-describedby={hasPreview ? turnTipId : undefined}
                className="block h-4 w-4 cursor-default rounded-full bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
              />
              {hasPreview && (
                <span
                  id={turnTipId}
                  role="tooltip"
                  className="ns-context-compaction-river-tip pointer-events-none absolute left-[calc(100%+8px)] top-1/2 z-20 block w-max max-w-[200px] -translate-y-1/2 whitespace-normal rounded-md border border-border px-2 py-1 font-mono text-[10px] leading-relaxed text-foreground shadow-lg"
                  style={{ background: "color-mix(in srgb, var(--foreground) 4%, var(--background))" }}
                >
                  Turn {t.label}: {t.preview}
                </span>
              )}
            </div>
          );
        })}

        {lakeRows.map((l) => {
          const state = exitingIds[l.id] ? "exiting" : enterState[l.id] === "start" ? "entering" : "settled";
          const isOpen = openId === l.id;
          const menuId = `${fid}-menu-${l.id}`;
          const tipId = `${fid}-tip-${l.id}`;
          return (
            <div
              key={l.id}
              className="ns-oxbow-chip-wrap absolute"
              data-state={state}
              style={{ left: l.x, top: l.y - 12 }}
            >
              <button
                ref={(el) => {
                  if (el) triggerRefs.current.set(l.id, el);
                  else triggerRefs.current.delete(l.id);
                }}
                type="button"
                data-oxbow-trigger
                aria-haspopup="menu"
                aria-expanded={isOpen}
                aria-controls={isOpen ? menuId : undefined}
                aria-describedby={!isOpen ? tipId : undefined}
                aria-label={`Compacted ${turnsLabel(l.item.turns)}, ${formatTok(l.item.tokenCount)} tokens. Open summary and re-inject.`}
                onClick={() => setOpenId(l.id)}
                onKeyDown={onTriggerKeyDown(l)}
                className="flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border border-border px-2.5 py-1 font-mono text-[10px] tracking-wide text-muted transition-colors duration-150 hover:border-foreground/30 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
                style={{ background: "color-mix(in srgb, var(--foreground) 4%, var(--background))" }}
              >
                <span aria-hidden>{l.item.turns.length}↩</span>
                <span aria-hidden>{formatTok(l.item.tokenCount)} tok</span>
              </button>

              {!isOpen && (
                <span
                  id={tipId}
                  role="tooltip"
                  className="ns-oxbow-chip-tip pointer-events-none absolute bottom-[calc(100%+8px)] left-0 z-20 block w-max max-w-[220px] whitespace-normal rounded-md border border-border px-2 py-1 font-mono text-[10px] leading-relaxed text-foreground shadow-lg"
                  style={{ background: "color-mix(in srgb, var(--foreground) 4%, var(--background))" }}
                >
                  {turnsLabel(l.item.turns)} pinched off the channel · re-injectable
                </span>
              )}

              {isOpen && (
                <div
                  ref={popRef}
                  id={menuId}
                  className="absolute left-0 top-[calc(100%+8px)] z-10 w-60 rounded-md border border-border p-3 shadow-lg"
                  style={{ background: "color-mix(in srgb, var(--foreground) 3%, var(--background))" }}
                >
                  <p
                    data-oxbow-summary
                    tabIndex={-1}
                    className="text-xs leading-relaxed text-foreground outline-none"
                  >
                    {l.item.summary}
                  </p>
                  <dl className="mt-2.5 space-y-1 font-mono text-[10px] uppercase tracking-wide text-muted">
                    <div className="flex justify-between gap-2">
                      <dt>Turns</dt>
                      <dd className="text-foreground">{turnsLabel(l.item.turns)}</dd>
                    </div>
                    <div className="flex justify-between gap-2">
                      <dt>Summarized</dt>
                      <dd className="text-foreground">{l.item.compactedAgo}</dd>
                    </div>
                    <div className="flex justify-between gap-2">
                      <dt>Tokens</dt>
                      <dd className="text-foreground">{formatTok(l.item.tokenCount)}</dd>
                    </div>
                  </dl>
                  <div
                    role="menu"
                    aria-label={`Actions for compacted ${turnsLabel(l.item.turns)}`}
                    onKeyDown={onMenuKeyDown(l)}
                    className="mt-3 border-t border-border pt-2.5"
                  >
                    <button
                      role="menuitem"
                      type="button"
                      data-oxbow-reinject
                      onClick={() => handleReinject(l)}
                      className="w-full cursor-pointer rounded-sm px-2 py-1.5 text-left text-xs text-foreground transition-colors hover:bg-[color-mix(in_srgb,var(--foreground)_6%,transparent)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent"
                    >
                      Re-inject into live context
                    </button>
                  </div>
                </div>
              )}
            </div>
          );
        })}
      </div>

      <ul className="mt-3 space-y-1 font-mono text-[11px] leading-relaxed text-muted">
        <li>
          Live context, {turnCount} turn{turnCount === 1 ? "" : "s"}
        </li>
        {lakeRows.map((l) => (
          <li key={l.id}>
            Compacted: {turnsLabel(l.item.turns)}, summarized {l.item.compactedAgo}
          </li>
        ))}
      </ul>

      <p aria-live="polite" className="sr-only">
        {liveMsg}
      </p>
    </div>
  );
}
Build spec

OxbowTurn draws the live conversation as one gently meandering channel in a side rail: a controlled `items` prop is a single ordered sequence of turn entries and compaction entries, and every entry — of either kind — gets one row slot, spaced evenly down the rail. Turn entries are plotted as points on the channel (x from a deterministic two-harmonic sine so the curve reads as organic without any randomness or physics sim); a fresh Catmull-Rom-to-bezier pass through only the turn points draws the channel path. Compaction entries do not contribute a channel point at all, so when a run of turns gets folded into one compaction, the channel simply stops routing through that stretch — the surviving points ease toward their new, tighter spacing over roughly 300ms (a direct rAF loop writing cx/cy/d via setAttribute, no React state on the hot path, sleeping once settled, in the same idiom as this registry's other spring-driven diagrams), which is what 'compaction shortens the river' looks like here. The folded turns settle as a closed oxbow-lake shape — a small filled loop (`color-mix(in srgb, var(--border) 6%, transparent)` fill, `--border` stroke) plus a short connecting stub, both purely decorative and `aria-hidden` — 12px off the channel, animated in on a 300ms cubic-bezier(0.16,1,0.3,1) transform+opacity transition (ease-out-expo) the instant the compaction first appears in `items`. Carried on the lake is a real, always-focusable Geist Mono token-count chip (`{turnsFolded}↩ {tokenCount} tok`, never `aria-hidden`) — clicking it opens a small popover with the plain-language summary, a turns/summarized-at/token-count `<dl>`, and a `role=menu` containing one `role=menuitem`: 're-inject into live context'. Choosing it drifts the lake back onto the channel (reverse transition, ~260ms) before the `onReinject(id)` callback actually fires and the consumer's state update splices the folded turns back into `items` as live turns again — nothing is removed from view until the return trip has finished playing, and a brief stroke-width pulse on the channel marks the splice. The chip trigger is deliberately open-only: a second click while already open is a no-op (only Escape, an outside click, or choosing the menu item closes it), so a scripted 'click the first control' pass and a later 'now click this same control and expect it open' check never fight over the same toggle. Hovering or focusing the chip (without clicking) also reveals a small, token-styled tooltip above it — 'turns {a}–{b} pinched off the channel · re-injectable' — so a user can preview a branch point before committing to opening the full menu; it's wired via `aria-describedby` and hides once the menu is open. The drawing (channel, dots, lake shapes) is entirely `aria-hidden` and carries zero information a screen reader needs beyond redundancy — the actual facts live as plain, always-visible text in a real list right below it: 'Live context, {n} turns' plus one 'Compacted: turns {a}–{b}, summarized {when}' line per oxbow, and every compaction/re-injection additionally announces itself through an `aria-live=polite` region ('Compacted turns 3–9 into a 1,840-token summary.' / re-inject's own announcement). Each turn dot also gets a real, invisible interactive target overlaid on top of it (an unstyled focusable `<button>` positioned by the same rAF loop that eases the decorative circle, so the hit target tracks the dot through every re-layout instead of snapping ahead of it) — hovering or Tab-focusing it reveals a token-styled tooltip with that turn's preview (`aria-describedby`), and its accessible name is 'Turn {label}: {preview}' (or just 'Turn {label}' when no preview was supplied); the newest turn renders slightly larger and gently breathes to mark 'currently live'. `prefers-reduced-motion` replaces every transition with an instant fade-and-move (channel positions snap directly to target, no rAF loop runs at all, lake enter/exit is a 160ms opacity-only cross-fade already sitting at its settled 12px offset) — every state stays fully legible and functional, just static. Props: `items` (`OxbowTurnItem[]`, a union of `{kind:'turn', id, label, preview?}` and `{kind:'compaction', id, turns, summary, tokenCount, compactedAgo}` — fully controlled, the component holds no business state, only UI state for which popover is open and the enter/exit animation phase), `onReinject`, `ariaLabel`, `className`. Dragging an oxbow back onto the channel was considered as an enhancement on top of the menu path but deliberately not built for v1 — the menu's re-inject item is the only path, already fully keyboard operable, and the brief is explicit that drag must never be the *only* way in. Demo: an agent-session card seeded with 10 live turns and two already-settled oxbows (turns 3–9 and turns 14–16) so the resting screenshot already shows the channel visibly shortened at two separate folds, each carrying its own independently-openable chip — legible as multiple branches, not a one-off — plus ADD TURN (streams one more live turn), COMPACT OLDEST (folds the next 2–4 oldest live turns into a fresh oxbow and plays the pinch), and RESET SESSION controls.

Tags
context-windowcompactionagentllmsvgdiagramhistorymonomenuundoaccessibility