@workerdeck/ui 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.d.mts CHANGED
@@ -1907,6 +1907,18 @@ interface SessionBrowserProps {
1907
1907
  * double-click would have already left the page.
1908
1908
  */
1909
1909
  onRename?: (row: SessionRow, title: string) => void;
1910
+ /**
1911
+ * Clear the conversation in place, from the row's eraser. Same session, empty
1912
+ * context — **not** a delete: the engine keeps the old conversation and it
1913
+ * stays resumable, which is why the row's copy never says "deleted".
1914
+ *
1915
+ * Rendered only where the session's own capability record allows it
1916
+ * (`capabilities.clearContext`, absent = false), so a row that cannot be
1917
+ * cleared has no eraser rather than one that errors. Omit the prop to have
1918
+ * none at all — a host with no way to send a session command has nothing to
1919
+ * hang it on (this is a WS command, not a REST route).
1920
+ */
1921
+ onClearContext?: (row: SessionRow) => void;
1910
1922
  /** Open a session *at* one of its sub-agents, when the host can scroll a
1911
1923
  * transcript to a `Task` row. Absent, the sub-agent list still expands and a
1912
1924
  * step just opens its session — see `SessionRowItemProps`. */
@@ -1962,6 +1974,7 @@ declare function SessionBrowser({
1962
1974
  onSelect,
1963
1975
  onDelete,
1964
1976
  onRename,
1977
+ onClearContext,
1965
1978
  onSelectSubagent,
1966
1979
  emptyState,
1967
1980
  showControls,
package/build/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { $ as toolIcon, A as PERMISSION_MODES, At as MenuSeparator, B as skillPr
2
2
  import { _ as formatRelativeTime, a as modelLabel, b as rateLimitWindowSeconds, d as formatBytes, f as formatCost, g as formatRateLimitWindowLong, h as formatRateLimitWindow, i as meterSeverity, l as windowLabel, m as formatDuration, o as statusPresentation, p as formatCountdown, r as meterColorClass, s as tightestWindow, t as contextSeverity, u as formatAgoPrecise, v as formatTokens, x as toolInputPreview, y as friendlyModel } from "./status-BE-zg88x.mjs";
3
3
  import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
- import { Anchor, ArrowRight, Atom, Beaker, BellRing, Bike, Binary, Blocks, Bot, Box, Brain, Briefcase, Brush, Bug, Building2, Cable, Camera, Car, Cat, Check, ChevronDown, ChevronRight, CircleAlert, CircleSlash, CircuitBoard, Cloud, Code, Coffee, Cog, Compass, Container, Cpu, CreditCard, Database, Diamond, Dog, Dot, Feather, Film, Flame, FlaskConical, Folder, Gamepad2, Gauge, Gem, Ghost, Gift, GitBranch, Globe, GraduationCap, Hammer, Hexagon, House, Image, Key, Landmark, Laptop, Layers, Leaf, Library, Lightbulb, Lock, Mail, Map as Map$1, MessageCircle, Mic, Monitor, Moon, Mountain, Music, Network, Newspaper, Notebook, Orbit, Package, Palette, PauseCircle, PenTool, Pencil, Phone, PiggyBank, Plane, Plug, Puzzle, Radar, Radio, Receipt, Rocket, Ruler, Satellite, Scale, Scissors, Search, SearchX, Server, Shield, Ship, ShoppingCart, Signal, Smartphone, Sparkles, Sprout, Square, Star, Sun, Table, Tag, Target, Telescope, Terminal, TestTube, Trash2, TreePine, Trophy, Truck, Umbrella, Users, Wallet, Wand, Waves, Webhook, Wifi, Wrench, X, Zap } from "lucide-react";
5
+ import { Anchor, ArrowRight, Atom, Beaker, BellRing, Bike, Binary, Blocks, Bot, Box, Brain, Briefcase, Brush, Bug, Building2, Cable, Camera, Car, Cat, Check, ChevronDown, ChevronRight, CircleAlert, CircleSlash, CircuitBoard, Cloud, Code, Coffee, Cog, Compass, Container, Cpu, CreditCard, Database, Diamond, Dog, Dot, Eraser, Feather, Film, Flame, FlaskConical, Folder, Gamepad2, Gauge, Gem, Ghost, Gift, GitBranch, Globe, GraduationCap, Hammer, Hexagon, House, Image, Key, Landmark, Laptop, Layers, Leaf, Library, Lightbulb, Lock, Mail, Map as Map$1, MessageCircle, Mic, Monitor, Moon, Mountain, Music, Network, Newspaper, Notebook, Orbit, Package, Palette, PauseCircle, PenTool, Pencil, Phone, PiggyBank, Plane, Plug, Puzzle, Radar, Radio, Receipt, Rocket, Ruler, Satellite, Scale, Scissors, Search, SearchX, Server, Shield, Ship, ShoppingCart, Signal, Smartphone, Sparkles, Sprout, Square, Star, Sun, Table, Tag, Target, Telescope, Terminal, TestTube, Trash2, TreePine, Trophy, Truck, Umbrella, Users, Wallet, Wand, Waves, Webhook, Wifi, Wrench, X, Zap } from "lucide-react";
6
6
  import { AlertDialog as AlertDialog$1 } from "@base-ui/react/alert-dialog";
7
7
  import { Toaster as Toaster$1, toast } from "sonner";
8
8
  import { STATE_LABELS, STATE_ORDER, adaptersOf, clearFilters, filterRows, groupRows, hasFacetFilter, isAgentRecord, projectLabel, projectSubpath, projectsOf, sessionLabel, subagentLabel, subsetSummary } from "@workerdeck/protocol";
@@ -760,7 +760,7 @@ function StepIcon({ state, kind }) {
760
760
  function rowShapeClass(active) {
761
761
  return cn("px-2 py-1.5 hover:bg-row-hover", active ? "mr-1 ml-0 rounded-r-md border-l-4 border-l-accent" : "mx-1 rounded-md");
762
762
  }
763
- function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelect, onDelete, onRename, onSelectSubagent, emptyState, showControls = true, projectIcons, className }) {
763
+ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelect, onDelete, onRename, onClearContext, onSelectSubagent, emptyState, showControls = true, projectIcons, className }) {
764
764
  const visible = useMemo(() => filterRows(rows, config, scope), [
765
765
  rows,
766
766
  config,
@@ -966,6 +966,7 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
966
966
  onSelect,
967
967
  onDelete,
968
968
  onRename,
969
+ onClearContext,
969
970
  onSelectSubagent
970
971
  }, `${row.hostId}:${row.info.id}`))]
971
972
  }, group.key))
@@ -980,7 +981,7 @@ function iconSrcOf(row, icons) {
980
981
  const icon = row?.info.project?.icon;
981
982
  return icon?.type === "image" ? icons?.[icon.hash] : void 0;
982
983
  }
983
- function SessionRowItem({ row, active, showGateway, showProject = true, projectIcons, onSelect, onDelete, onRename, onSelectSubagent }) {
984
+ function SessionRowItem({ row, active, showGateway, showProject = true, projectIcons, onSelect, onDelete, onRename, onClearContext, onSelectSubagent }) {
984
985
  const { info } = row;
985
986
  const [editing, setEditing] = useState(false);
986
987
  const [expanded, setExpanded] = useState(false);
@@ -1077,6 +1078,18 @@ function SessionRowItem({ row, active, showGateway, showProject = true, projectI
1077
1078
  },
1078
1079
  children: /* @__PURE__ */ jsx(Pencil, { className: "size-3 text-fg-3" })
1079
1080
  }) : null,
1081
+ onClearContext && info.capabilities?.clearContext ? /* @__PURE__ */ jsx(Button, {
1082
+ variant: "ghost",
1083
+ size: "icon-sm",
1084
+ "aria-label": "Clear context",
1085
+ title: "Clear the conversation — the session keeps running and the old conversation stays resumable",
1086
+ className: "size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100",
1087
+ onClick: (e) => {
1088
+ e.stopPropagation();
1089
+ onClearContext(row);
1090
+ },
1091
+ children: /* @__PURE__ */ jsx(Eraser, { className: "size-3 text-fg-3" })
1092
+ }) : null,
1080
1093
  onDelete ? /* @__PURE__ */ jsx(Button, {
1081
1094
  variant: "ghost",
1082
1095
  size: "icon-sm",
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["AlertDialogPrimitive","SonnerToaster","Map"],"sources":["../src/components/ui/Card.tsx","../src/components/ui/Textarea.tsx","../src/components/ui/AlertDialog.tsx","../src/components/ui/Sonner.tsx","../src/components/ui/Empty.tsx","../src/components/prompt-area/segment-helpers.ts","../src/components/terminal/StatusLine.tsx","../src/components/agent/SessionList.tsx","../src/components/agent/ContextRing.tsx","../src/components/agent/EngineIcon.tsx","../src/components/agent/ProjectIcon.tsx","../src/components/agent/SessionSteps.tsx","../src/components/agent/SessionBrowser.tsx"],"sourcesContent":["import { type HTMLAttributes } from 'react'\nimport { cn } from '../../lib/utils.ts'\n\nexport function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n data-slot='card'\n className={cn('rounded-lg border border-border bg-surface', className)}\n {...props}\n />\n )\n}\n\nexport function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn('flex items-start justify-between gap-3 px-4 pt-3.5 pb-3', className)}\n {...props}\n />\n )\n}\n\nexport function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {\n return <h3 className={cn('text-heading-3 font-semibold text-text', className)} {...props} />\n}\n\nexport function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('px-4 pb-4', className)} {...props} />\n}\n","import { type TextareaHTMLAttributes, forwardRef } from 'react'\nimport { cn } from '../../lib/utils.ts'\n\nexport const Textarea = forwardRef<\n HTMLTextAreaElement,\n TextareaHTMLAttributes<HTMLTextAreaElement>\n>(({ className, ...props }, ref) => (\n <textarea\n ref={ref}\n data-slot='textarea'\n className={cn(\n 'w-full resize-none rounded-md border border-border bg-bg px-2.5 py-2 text-body-sm text-text',\n 'placeholder:text-fg-4 transition-colors outline-none',\n 'hover:border-border-strong focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40',\n 'disabled:pointer-events-none disabled:opacity-50',\n className,\n )}\n {...props}\n />\n))\nTextarea.displayName = 'Textarea'\n","import { type FunctionComponent } from 'react'\nimport { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'\nimport { cn } from '../../lib/utils.ts'\n\nexport const AlertDialog = AlertDialogPrimitive.Root\nexport const AlertDialogTrigger = AlertDialogPrimitive.Trigger\nexport const AlertDialogClose = AlertDialogPrimitive.Close\n\nexport const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Props> = ({\n className,\n children,\n ...props\n}) => (\n <AlertDialogPrimitive.Portal>\n <AlertDialogPrimitive.Backdrop\n className={cn(\n 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',\n 'transition-opacity duration-(--motion-base)',\n 'data-starting-style:opacity-0 data-ending-style:opacity-0',\n )}\n />\n <AlertDialogPrimitive.Popup\n data-slot='alert-dialog-content'\n className={cn(\n 'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',\n 'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',\n 'transition-[opacity,transform] duration-(--motion-base)',\n 'data-starting-style:scale-95 data-starting-style:opacity-0',\n 'data-ending-style:scale-95 data-ending-style:opacity-0',\n className,\n )}\n {...props}>\n {children}\n </AlertDialogPrimitive.Popup>\n </AlertDialogPrimitive.Portal>\n)\n\nexport const AlertDialogTitle: FunctionComponent<AlertDialogPrimitive.Title.Props> = ({\n className,\n ...props\n}) => (\n <AlertDialogPrimitive.Title\n className={cn('text-heading-3 font-semibold text-text', className)}\n {...props}\n />\n)\n\nexport const AlertDialogDescription: FunctionComponent<AlertDialogPrimitive.Description.Props> = ({\n className,\n ...props\n}) => (\n <AlertDialogPrimitive.Description\n className={cn('mt-1.5 text-body-sm text-muted-foreground', className)}\n {...props}\n />\n)\n","import { Toaster as SonnerToaster, toast } from 'sonner'\n\nexport { toast }\n\n/** Token-themed toaster; relies on the [data-theme] swap, so no `theme` prop needed. */\nexport function Toaster() {\n return (\n <SonnerToaster\n position='bottom-right'\n toastOptions={{\n style: {\n background: 'var(--surface)',\n color: 'var(--text)',\n border: '1px solid var(--border)',\n boxShadow: 'var(--shadow-md)',\n fontFamily: 'var(--cw-font-sans)',\n fontSize: 'var(--text-body-sm)',\n },\n }}\n />\n )\n}\n","import type { ReactNode } from 'react'\nimport { Button } from './Button.tsx'\n\n/**\n * A view with nothing in it: icon, title, description, and at most one action.\n *\n * Centered and given room, because an empty view is the first thing a new\n * install shows and a flush-left sentence in the top corner reads like a bug\n * report. It lives here rather than in one client because every panel that can\n * be empty should be empty the same way — the extension's views and the\n * dashboard's four sidebars are the same shape of thing.\n *\n * **At most one action, and never one the view header already offers.** Creating\n * a session and adding a gateway are the `+` in the native title bar, exclusively\n * — so those empty states point at it in words rather than growing a second\n * button that does the same thing two inches lower. A button here is for the way\n * out of a state the header has no answer to: clearing a filter, widening a\n * scope.\n */\nexport function Empty({\n icon,\n title,\n description,\n action,\n onAction,\n}: {\n icon: ReactNode\n title: string\n description?: ReactNode\n action?: string\n onAction?: () => void\n}) {\n return (\n <div className='flex flex-col items-center gap-2 px-5 py-8 text-center'>\n <div className='text-fg-4 opacity-60 [&_svg]:size-7'>{icon}</div>\n <p className='text-body-sm font-medium text-fg-2'>{title}</p>\n {description ? (\n <p className='max-w-[26ch] text-balance text-label leading-relaxed text-fg-4'>\n {description}\n </p>\n ) : null}\n {action && onAction ? (\n <Button variant='outline' size='sm' className='mt-1' onClick={onAction}>\n {action}\n </Button>\n ) : null}\n </div>\n )\n}\n\n/** A key or icon named inline in an empty state's description — \"use the + above\". */\nexport function EmptyKey({ children }: { children: ReactNode }) {\n return (\n <span className='rounded border border-border px-1 font-mono text-fg-3'>{children}</span>\n )\n}\n","/**\n * Convenience helpers for creating and inspecting Segments.\n *\n * These reduce boilerplate when building AI chat UIs that work with the\n * PromptArea document model.\n *\n * @example\n * ```ts\n * import { text, chip, isSegmentsEmpty, segmentsToPlainText } from './segment-helpers.ts'\n *\n * const greeting = [text('Hello '), chip({ trigger: '@', value: 'u1', displayText: 'Alice' })]\n * isSegmentsEmpty(greeting) // false\n * segmentsToPlainText(greeting) // \"Hello @Alice\"\n * ```\n */\n\nimport type { Segment, TextSegment, ChipSegment } from './types.ts'\nimport { segmentsToPlainText, plainTextToSegments } from './prompt-area-engine.ts'\n\n// Re-export serialization utilities so consumers have a single import.\nexport { segmentsToPlainText, plainTextToSegments }\n\n// ---------------------------------------------------------------------------\n// Factories\n// ---------------------------------------------------------------------------\n\n/** Create a text segment. */\nexport function text(value: string): TextSegment {\n return { type: 'text', text: value }\n}\n\n/** Create a chip segment. */\nexport function chip(opts: Omit<ChipSegment, 'type'>): ChipSegment {\n return { type: 'chip', ...opts }\n}\n\n// ---------------------------------------------------------------------------\n// Predicates\n// ---------------------------------------------------------------------------\n\n/** Returns `true` when the segment array is empty or contains only whitespace text. */\nexport function isSegmentsEmpty(segments: Segment[]): boolean {\n if (segments.length === 0) return true\n return segments.every((seg) => seg.type === 'text' && seg.text.trim() === '')\n}\n\n/** Returns `true` when the segment array contains at least one chip. */\nexport function hasChips(segments: Segment[]): boolean {\n return segments.some((seg) => seg.type === 'chip')\n}\n\n/** Extracts all chip segments from a segment array. */\nexport function getChips(segments: Segment[]): ChipSegment[] {\n return segments.filter((seg): seg is ChipSegment => seg.type === 'chip')\n}\n\n/** Extracts chips matching a specific trigger character. */\nexport function getChipsByTrigger(segments: Segment[], trigger: string): ChipSegment[] {\n return segments.filter(\n (seg): seg is ChipSegment => seg.type === 'chip' && seg.trigger === trigger,\n )\n}\n","import type { RateLimitInfo } from '@workerdeck/protocol'\nimport type { ConnectionState, TranscriptState } from '@workerdeck/react'\nimport { formatCost, formatTokens } from '../../lib/format.ts'\nimport {\n contextSeverity,\n meterSeverity,\n modelLabel,\n statusPresentation,\n tightestWindow,\n windowLabel,\n type StatusSeverity,\n} from '../../lib/status.ts'\nimport { Ink, Row, type Tone } from './row.tsx'\n\n/**\n * The session's readings, as one line.\n *\n * The styled bar puts each of these in its own affordance — a progress ring, a\n * badge, a tooltip — which is right for a dashboard and wrong here for the usual\n * reason: a terminal has one line height, and five widgets of five different\n * heights is five different rhythms in a row that is supposed to be quiet. So\n * they are words, `·`-separated, exactly as the CLI's own status line writes\n * them.\n *\n * The readings themselves come from the same helpers the styled bar uses\n * (`lib/status.ts`), and that matters more than it looks: `statusPresentation`\n * is where the rule lives that **a dropped socket outranks the session status**,\n * because a status held over a dead socket is a stale reading being presented as\n * a live one. Re-deriving that here would be a second answer to a question that\n * already has one.\n */\n\n/** Severity → tone. The 80/95 thresholds are `lib/status.ts`'s, not new ones. */\nconst SEVERITY_TONE: Record<StatusSeverity, Tone> = {\n none: 'dim',\n warning: 'yellow',\n error: 'red',\n}\n\nexport interface TerminalStatusLineProps {\n state: TranscriptState\n /** Plan windows to read from, when they should not be the session's own — the\n * gateway's per-profile state merged over this transcript's. See\n * {@link StatusBarProps.rateLimits}. Absent = `state.rateLimits`. */\n rateLimits?: Record<string, RateLimitInfo>\n /** How the client is doing at reaching the gateway. Wins the status slot when\n * the socket is down — see above. */\n connection?: ConnectionState\n /** Opens the panel that answers each reading's own question. Omit and the\n * reading is text rather than something to press. */\n onOpenStatus?: () => void\n onOpenContext?: () => void\n onOpenUsage?: () => void\n}\n\n/** One reading. A button only when it leads somewhere — a segment that looks\n * pressable and does nothing is worse than plain text. */\nfunction Reading({\n tone,\n onPress,\n label,\n children,\n}: {\n tone?: Tone\n onPress?: () => void\n label?: string\n children: React.ReactNode\n}) {\n if (!onPress) return <Ink tone={tone}>{children}</Ink>\n return (\n <button type='button' className='term-reading' onClick={onPress} title={label}>\n <Ink tone={tone}>{children}</Ink>\n </button>\n )\n}\n\nexport function TerminalStatusLine({\n state,\n rateLimits,\n connection,\n onOpenStatus,\n onOpenContext,\n onOpenUsage,\n}: TerminalStatusLineProps) {\n const presentation = statusPresentation({ status: state.status, connection })\n const usage = state.contextUsage\n const window = tightestWindow(rateLimits ?? state.rateLimits)\n const model = modelLabel({ model: state.model, models: state.models ?? [] })\n\n const parts: React.ReactNode[] = [\n <Reading\n key='status'\n tone={SEVERITY_TONE[presentation.severity]}\n onPress={onOpenStatus}\n label='Session status'>\n {presentation.label}\n </Reading>,\n ]\n\n if (usage) {\n parts.push(\n <Reading\n key='context'\n tone={SEVERITY_TONE[contextSeverity(usage)]}\n onPress={onOpenContext}\n label='Context window'>\n {Math.round(usage.percentage)}% ctx ({formatTokens(usage.totalTokens)})\n </Reading>,\n )\n }\n\n if (window) {\n parts.push(\n <Reading\n key='usage'\n tone={SEVERITY_TONE[meterSeverity(window.info.utilization)]}\n onPress={onOpenUsage}\n label='Plan usage'>\n {Math.round(window.info.utilization ?? 0)}% {windowLabel(window.key)}\n </Reading>,\n )\n }\n\n // Cost last of the numbers: it is the reading you check, never the one you act\n // on, and nothing about a turn changes because of it.\n if (state.totalCostUsd > 0) {\n parts.push(\n <Ink key='cost' tone='faint'>\n {formatCost(state.totalCostUsd)}\n </Ink>,\n )\n }\n\n if (model) {\n parts.push(\n <Ink key='model' tone='faint'>\n {model}\n </Ink>,\n )\n }\n if (state.permissionMode && state.permissionMode !== 'default') {\n parts.push(\n <Ink key='mode' tone='yellow'>\n {state.permissionMode}\n </Ink>,\n )\n }\n\n return (\n <Row data-slot='status-line' tone='dim'>\n {parts.map((part, index) => (\n <span key={index}>\n {index > 0 ? <Ink tone='faint'> · </Ink> : null}\n {part}\n </span>\n ))}\n </Row>\n )\n}\n","import type { SessionInfo } from '@workerdeck/protocol'\nimport { Trash2 } from 'lucide-react'\nimport { Badge } from '../ui/Badge.tsx'\nimport { Button } from '../ui/Button.tsx'\nimport { cn } from '../../lib/utils.ts'\nimport { formatCost, formatRelativeTime } from '../../lib/format.ts'\nimport { STATUS_META } from './status.ts'\n\nexport interface SessionListItemProps {\n session: SessionInfo\n active?: boolean\n onSelect?: (id: string) => void\n onDelete?: (id: string) => void\n}\n\nexport function SessionListItem({ session, active, onSelect, onDelete }: SessionListItemProps) {\n const meta = STATUS_META[session.status]\n return (\n <div\n data-slot='session-list-item'\n data-active={active || undefined}\n className={cn(\n 'group flex w-full items-center gap-2 rounded-md border border-transparent px-2.5 py-2 text-left transition-colors',\n active ? 'border-border bg-surface' : 'hover:bg-surface-hover',\n )}>\n <button\n type='button'\n onClick={() => onSelect?.(session.id)}\n className='min-w-0 flex-1 text-left outline-none'>\n <div className='flex items-center gap-2'>\n <span className='truncate text-body-sm font-medium text-fg-1'>\n {session.title ?? session.id.slice(0, 8)}\n </span>\n <Badge variant={meta.variant} dot className='shrink-0'>\n {meta.label}\n </Badge>\n </div>\n <div className='mt-0.5 flex items-center gap-2 font-mono text-label text-fg-4'>\n <span className='truncate'>{session.cwd}</span>\n {session.profile ? <span className='shrink-0'>@{session.profile}</span> : null}\n <span className='shrink-0'>{formatCost(session.totalCostUsd)}</span>\n <span className='shrink-0'>{formatRelativeTime(session.lastActivityAt ?? session.createdAt)}</span>\n </div>\n </button>\n {onDelete ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Close session'\n className='opacity-0 transition-opacity group-hover:opacity-100'\n onClick={() => onDelete(session.id)}>\n <Trash2 className='size-3.5 text-fg-3' />\n </Button>\n ) : null}\n </div>\n )\n}\n\nexport interface SessionListProps {\n sessions: SessionInfo[]\n activeId?: string\n onSelect?: (id: string) => void\n onDelete?: (id: string) => void\n emptyText?: string\n className?: string\n}\n\nexport function SessionList({\n sessions,\n activeId,\n onSelect,\n onDelete,\n emptyText = 'No sessions yet.',\n className,\n}: SessionListProps) {\n return (\n <div data-slot='session-list' className={cn('flex flex-col gap-1', className)}>\n {sessions.length === 0 ? (\n <div className='px-2.5 py-6 text-center text-body-sm text-fg-4'>{emptyText}</div>\n ) : (\n sessions.map((session) => (\n <SessionListItem\n key={session.id}\n session={session}\n active={session.id === activeId}\n onSelect={onSelect}\n onDelete={onDelete}\n />\n ))\n )}\n </div>\n )\n}\n","import type { ContextReading } from '@workerdeck/protocol'\nimport { ProgressRing } from '../ui/ProgressRing.tsx'\nimport { cn } from '../../lib/utils.ts'\nimport { formatTokens } from '../../lib/format.ts'\nimport { meterColorClass } from '../../lib/status.ts'\n\n/**\n * How full a session's context window is, sized for a **list row**.\n *\n * The reading the session screen has always drawn, moved somewhere it answers a\n * question the session screen cannot: *which* of these thirty sessions is\n * bloating. That is also why it is a ring and not a number — across a list, a\n * ring is read at a glance and `71%` has to be read one row at a time.\n *\n * **Absent draws nothing, and absent is not zero.** A promptless session, a\n * parked one from before the field existed, or an engine that reports no window\n * has no reading; an empty ring would claim its context is empty rather than\n * unknown. The title carries the exact numbers for anyone who wants them.\n *\n * Shared rather than spelled per client: the dashboard's row and the VS Code\n * sidebar's card both draw it, and two copies would be two sets of thresholds\n * the day one of them is nudged.\n */\nexport function ContextRing({\n usage,\n size = 11,\n className,\n}: {\n usage: ContextReading | undefined\n size?: number\n className?: string\n}) {\n if (usage === undefined) return null\n return (\n <span\n title={`Context ${usage.percentage.toFixed(0)}% · ${formatTokens(usage.totalTokens)} / ${formatTokens(usage.maxTokens)}`}\n className={cn('shrink-0 leading-none', meterColorClass(usage.percentage), className)}>\n <ProgressRing value={usage.percentage} size={size} strokeWidth={2} />\n </span>\n )\n}\n","import { cn } from '../../lib/utils.ts'\n\n/**\n * The engine's own mark, monochrome.\n *\n * Inlined rather than depended on: the React package these come from\n * (`@lobehub/icons`) pulls antd, `@lobehub/ui` and a second copy of\n * lucide-react — an absurd amount of bundle for a few glyphs in a sidebar.\n * These are paths from `@lobehub/icons-static-svg` (MIT, © LobeHub), already\n * `fill=\"currentColor\"` single-path marks, so they take the row's colour like\n * any other glyph and need no light/dark variants.\n *\n * The marks are trademarks of their owners (Anthropic, OpenAI, Google,\n * DeepSeek, Moonshot), used here only to identify which engine a session runs\n * on.\n *\n * Codex is drawn as OpenAI: the session's engine *is* OpenAI's, and OpenAI's\n * mark is the one a person recognises at 12px.\n */\nconst PATHS: Record<string, { title: string; d: string }> = {\n claude: {\n title: 'Claude',\n d: 'M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z',\n },\n codex: {\n title: 'OpenAI',\n d: 'M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z',\n },\n gemini: {\n title: 'Gemini',\n d: 'M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z',\n },\n deepseek: {\n title: 'DeepSeek',\n d: 'M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z',\n },\n moonshot: {\n title: 'Moonshot',\n d: 'M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z',\n },\n}\n\n/**\n * Which mark a session wears.\n *\n * The two first-party engines are named directly. A `provider` session is\n * whatever the host wired up, so its *model* is the only thing that says whose\n * it is — sniffed loosely on purpose (`gemini-2.5-pro`, `deepseek-chat`,\n * `kimi-k2`), and falling through to no mark rather than guessing wrong.\n */\nexport function engineMark(engine: string, model?: string): string | undefined {\n if (engine === 'claude') return 'claude'\n if (engine === 'codex') return 'codex'\n const id = model?.toLowerCase() ?? ''\n if (!id) return undefined\n if (id.includes('gemini')) return 'gemini'\n if (id.includes('deepseek')) return 'deepseek'\n if (id.includes('moonshot') || id.includes('kimi')) return 'moonshot'\n if (id.includes('claude')) return 'claude'\n if (id.startsWith('gpt') || id.startsWith('o1') || id.startsWith('o3') || id.includes('openai')) {\n return 'codex'\n }\n return undefined\n}\n\n/**\n * Which engines wear their vendor's colour, and **how far it reaches**.\n *\n * `MARK` is the glyph, `TEXT` is the model name beside it. They are the same for\n * Anthropic and deliberately not for OpenAI, because the two brands differ in\n * kind rather than in hue: coral is an accent, and OpenAI's guidelines forbid\n * adding colour to the mark at all, so theirs is pure white/black. At full\n * contrast that is right on a 12px glyph and wrong on an 11px label — a\n * pure-white model name is *brighter than the session title above it*, which\n * inverts the row's whole hierarchy to say something the mark has already said.\n * So OpenAI's name keeps the muted line it always had, which is also the most\n * literal reading of \"don't add any colors\".\n *\n * Anything absent falls through to muted on both counts.\n */\nconst VENDOR_MARK: Record<string, string> = {\n claude: 'text-vendor-claude',\n codex: 'text-vendor-openai',\n}\nconst VENDOR_TEXT: Record<string, string> = {\n claude: 'text-vendor-claude',\n}\n\n/** The colour a session's engine mark wears. Pass it to `EngineIcon`'s\n * `className` rather than to a parent: the svg carries its own `text-fg-3`,\n * and only a class on the element itself is merged over it. */\nexport function vendorMarkClass(engine: string, model?: string): string {\n return VENDOR_MARK[engineMark(engine, model) ?? ''] ?? 'text-fg-3'\n}\n\n/** The colour the model *name* wears beside that mark — muted unless the vendor\n * is one whose accent survives at label size. See `VENDOR_TEXT`.\n *\n * The fallback is `text-fg-3` rather than the metadata line's own `text-fg-4`,\n * so an unbranded model still sits one step above the project and gateway\n * beside it — the order the spec lists them in, held without a colour. */\nexport function vendorTextClass(engine: string, model?: string): string {\n return VENDOR_TEXT[engineMark(engine, model) ?? ''] ?? 'text-fg-3'\n}\n\nexport function EngineIcon({\n engine,\n model,\n className,\n}: {\n engine: string\n model?: string\n className?: string\n}) {\n const mark = PATHS[engineMark(engine, model) ?? '']\n // An engine this build has never heard of still gets a slot, so the second\n // line doesn't reflow around a missing glyph.\n if (!mark) {\n return (\n <span aria-hidden title={engine} className={cn('shrink-0 text-fg-3', className)}>\n ·\n </span>\n )\n }\n return (\n <svg\n viewBox='0 0 24 24'\n fill='currentColor'\n fillRule='evenodd'\n role='img'\n aria-label={mark.title}\n className={cn('size-3 shrink-0 text-fg-3', className)}>\n <title>{mark.title}</title>\n <path d={mark.d} />\n </svg>\n )\n}\n","import type { ProjectIcon as ProjectIconSpec } from '@workerdeck/protocol'\nimport {\n Anchor, Atom, Beaker, Bike, Binary, Blocks, Bot, Box, Brain, Briefcase, Brush, Bug,\n Building2, Cable, Camera, Car, Cat, CircuitBoard, Cloud, Code, Coffee, Cog, Compass,\n Container, Cpu, CreditCard, Database, Diamond, Dog, Feather, Film, Flame, FlaskConical,\n Folder, Gamepad2, Gauge, Gem, Ghost, Gift, GitBranch, Globe, GraduationCap, Hammer,\n Hexagon, House, Image, Key, Landmark, Laptop, Layers, Leaf, Library, Lightbulb, Lock,\n Mail, Map, MessageCircle, Mic, Monitor, Moon, Mountain, Music, Network, Newspaper,\n Notebook, Orbit, Package, Palette, PenTool, Phone, PiggyBank, Plane, Plug, Puzzle,\n Radar, Radio, Receipt, Rocket, Ruler, Satellite, Scale, Scissors, Server, Shield, Ship,\n ShoppingCart, Signal, Smartphone, Sparkles, Sprout, Square, Star, Sun, Table, Tag,\n Target, Telescope, Terminal, TestTube, TreePine, Trophy, Truck, Umbrella, Users, Wallet,\n Wand, Waves, Webhook, Wifi, Wrench, Zap,\n} from 'lucide-react'\nimport type { ComponentType } from 'react'\nimport { cn } from '../../lib/utils.ts'\n\n/**\n * A project's icon — the render side of protocol's `ProjectIcon`, for a list\n * row or a group header.\n *\n * **The glyph arm is a curated set, and that is the design rather than a\n * shortcut.** The gateway validates a glyph name by *shape* only (lucide's\n * lowercase-kebab convention) and explicitly declines to grow an icon catalog,\n * so the contract already says a client must fall back on a name it does not\n * know.\n *\n * The alternative was measured rather than argued, against the VS Code\n * sidebar's own bundle: `sidebar.js` is **31 KB** with one glyph, **77 KB**\n * with the 110 below (~418 bytes each), and **927 KB** with a\n * `import * as` over lucide's ~1,600 — which is what it costs, because a\n * namespace import defeats tree-shaking by construction. (`DynamicIcon` is the\n * third option and worse in a different currency: ~1,600 chunk files shipped\n * inside the `.vsix`.) Twelve times the bundle to render a name nobody has\n * declared yet is not a trade worth making; 46 KB to cover the names people\n * actually use is.\n *\n * The set is chosen to cover what people call a project; an unlisted-but-valid\n * name draws {@link Folder}, which is what the row drew before this feature\n * existed. **Grow it freely** — each addition is ~400 bytes and the fallback\n * means a missing one is a shrug, not a hole.\n *\n * **The image arm draws nothing until its bytes arrive**, and takes them as a\n * resolved `src` rather than fetching: the wire carries only an address\n * (media type, size, content hash), and *who* fetches it differs per client —\n * a VS Code webview cannot reach a gateway at all and must be handed a data\n * URL by its extension host, while a browser can fetch and `createObjectURL`.\n * A component that fetched would be wrong on one of them. Absent `src` renders\n * nothing rather than a placeholder box: an icon is decoration beside a name\n * that is already there, and a box that becomes a picture a beat later is more\n * movement than the picture is worth.\n *\n * One thing an image icon cannot do, and a glyph can: **take the row's\n * colour.** An `<img>`-embedded SVG is its own document, so `currentColor` does\n * not reach it and its own `prefers-color-scheme` resolves against the *OS*\n * rather than the host's theme (measured: it does resolve, which is the\n * surprise — so a monochrome mark tuned for both schemes is right on a dark OS\n * in a dark editor and wrong on a light OS in a dark one). A repo that wants a\n * mark that always matches its row should declare a glyph; one that wants its\n * brand should declare the image and accept that it is a picture.\n */\nconst GLYPHS: Record<string, ComponentType<{ className?: string }>> = {\n anchor: Anchor, atom: Atom, beaker: Beaker, bike: Bike, binary: Binary, blocks: Blocks,\n bot: Bot, box: Box, brain: Brain, briefcase: Briefcase, brush: Brush, bug: Bug,\n 'building-2': Building2, cable: Cable, camera: Camera, car: Car, cat: Cat,\n 'circuit-board': CircuitBoard, cloud: Cloud, code: Code, coffee: Coffee, cog: Cog,\n compass: Compass, container: Container, cpu: Cpu, 'credit-card': CreditCard,\n database: Database, diamond: Diamond, dog: Dog, feather: Feather, film: Film,\n flame: Flame, 'flask-conical': FlaskConical, folder: Folder, 'gamepad-2': Gamepad2,\n gauge: Gauge, gem: Gem, ghost: Ghost, gift: Gift, 'git-branch': GitBranch, globe: Globe,\n 'graduation-cap': GraduationCap, hammer: Hammer, hexagon: Hexagon, house: House,\n image: Image, key: Key, landmark: Landmark, laptop: Laptop, layers: Layers, leaf: Leaf,\n library: Library, lightbulb: Lightbulb, lock: Lock, mail: Mail, map: Map,\n 'message-circle': MessageCircle, mic: Mic, monitor: Monitor, moon: Moon,\n mountain: Mountain, music: Music, network: Network, newspaper: Newspaper,\n notebook: Notebook, orbit: Orbit, package: Package, palette: Palette, 'pen-tool': PenTool,\n phone: Phone, 'piggy-bank': PiggyBank, plane: Plane, plug: Plug, puzzle: Puzzle,\n radar: Radar, radio: Radio, receipt: Receipt, rocket: Rocket, ruler: Ruler,\n satellite: Satellite, scale: Scale, scissors: Scissors, server: Server, shield: Shield,\n ship: Ship, 'shopping-cart': ShoppingCart, signal: Signal, smartphone: Smartphone,\n sparkles: Sparkles, sprout: Sprout, square: Square, star: Star, sun: Sun, table: Table,\n tag: Tag, target: Target, telescope: Telescope, terminal: Terminal, 'test-tube': TestTube,\n 'tree-pine': TreePine, trophy: Trophy, truck: Truck, umbrella: Umbrella, users: Users,\n wallet: Wallet, wand: Wand, waves: Waves, webhook: Webhook, wifi: Wifi, wrench: Wrench,\n zap: Zap,\n}\n\nexport function ProjectIcon({\n icon,\n src,\n name,\n className,\n}: {\n icon: ProjectIconSpec | undefined\n /** Resolved bytes for the `image` arm — an object URL or a data URL. The\n * caller fetches (see the note above); absent draws nothing. */\n src?: string\n /** The project's name, for the alt text. */\n name?: string\n className?: string\n}) {\n if (!icon) return null\n if (icon.type === 'image') {\n if (!src) return null\n return (\n <img\n src={src}\n alt=''\n aria-hidden\n title={name}\n // `object-contain` because a declared icon is whatever aspect the repo\n // checked in, and a squashed logo reads worse than a letterboxed one.\n className={cn('inline-block size-3 shrink-0 object-contain', className)}\n />\n )\n }\n const Glyph = GLYPHS[icon.name] ?? Folder\n return <Glyph className={cn('inline-block size-3 shrink-0', className)} />\n}\n","import {\n ArrowRight,\n Check,\n ChevronDown,\n ChevronRight,\n CircleAlert,\n Dot,\n PauseCircle,\n} from 'lucide-react'\nimport { isAgentRecord, subagentLabel } from '@workerdeck/protocol'\nimport type { SessionInfo, SubagentInfo } from '@workerdeck/protocol'\nimport { Spinner } from '../ui/Spinner.tsx'\nimport { cn } from '../../lib/utils.ts'\n\n/**\n * The work *under* a session row, and the one row shape every client renders it\n * through.\n *\n * This lived inside the VS Code webview first, which is exactly why the\n * dashboard had none of it. Nothing in here is extension-specific: a session's\n * sub-agents are a protocol fact, and a disclosure over them is a list\n * affordance. It sits in `packages/ui` so the sidebar, the dashboard and (in its\n * own idiom) the phone are annotating rows the same way rather than three ways.\n *\n * Today the only source is `SessionInfo.subagents`. The other — the CLI's own\n * **task checklist**, the to-do list it keeps for the current turn — is what the\n * design was drawn from, and it is not built: nothing on the wire carries it yet\n * (see `_docs/features/sub-agent-handling.md`, second thread, which opens with\n * \"check a capture\" rather than \"design a surface\"). When it arrives it is a\n * *source*, not a second row component: checklist when the session has one, its\n * sub-agents otherwise.\n *\n * That is also why `state` has a `pending` arm no sub-agent can produce. A\n * sub-agent record exists only once dispatched, so it is never queued; a to-do\n * is queued for most of its life, and dropping the state would mean widening the\n * union later — the shape is cheaper to state now than to retrofit.\n */\nexport type Step = {\n key: string\n label: string\n /** What one of these is called, for the disclosure's count. */\n noun: string\n /**\n * An **agent** has an identity and work of its own, so it is pressable and\n * wears the sub-agent colour. A **task** is something the model described with\n * no agent behind it (`isAgentRecord`), so it is inert: there is no frame to\n * open, and a row that offered one would show an empty screen.\n *\n * Two values and not a boolean because the checklist source this shape was\n * drawn for (see above) produces the second kind natively — a to-do is a task\n * in exactly this sense, and it will slot in here rather than widening\n * anything.\n */\n kind: 'agent' | 'task'\n state: 'done' | 'running' | 'pending' | 'failed'\n /** A trailing reading — a sub-agent's tool count. Absent draws nothing. */\n detail?: string\n title: string\n onSelect: () => void\n}\n\nexport function sessionSteps(\n info: SessionInfo,\n onSelectSubagent: (toolUseId: string) => void,\n): Step[] {\n // The label is protocol's `subagentLabel`, not a spelling of its own: the\n // dashboard and the phone render the same rows from the same records, and two\n // spellings would be two different answers to \"which agent is this\".\n return (info.subagents ?? []).map((sub) => ({\n key: sub.toolUseId,\n label: subagentLabel(sub),\n noun: 'agent',\n kind: isAgentRecord(sub) ? ('agent' as const) : ('task' as const),\n state: stepState(sub.status),\n detail: sub.toolCount > 0 ? String(sub.toolCount) : undefined,\n title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,\n onSelect: () => onSelectSubagent(sub.toolUseId),\n }))\n}\n\nfunction stepState(status: SubagentInfo['status']): Step['state'] {\n switch (status) {\n case 'running':\n return 'running'\n case 'failed':\n return 'failed'\n default:\n return 'done'\n }\n}\n\n/** How many of these are still going — the live half of the disclosure's count. */\nexport function runningSteps(steps: readonly Step[]): number {\n return steps.filter((s) => s.state === 'running').length\n}\n\n/**\n * The disclosure, which is also the reading: `3 agents` — or `2 of 3 agents`\n * while some have settled, because \"how many are still going\" is the live\n * question and a bare total answers it wrong the moment one finishes.\n *\n * Sub-agents are an annotation on a working row rather than a state of their own\n * (see `runningSubagents` in protocol's `session-list.ts`), so this never\n * competes with the row's status glyph: that still says what the *session* is\n * doing.\n */\nexport function StepToggle({\n expanded,\n running,\n total,\n noun,\n onToggle,\n}: {\n expanded: boolean\n running: number\n total: number\n noun: string\n onToggle: () => void\n}) {\n // Two spellings of one count. The **words** are the honest reading and go in\n // the tooltip and to a screen reader; the line itself gets the digits, because\n // this sits on the second line of a 280px row next to the folder and the age,\n // and `1 of 6 agents` truncated the folder name away to say something the row\n // could say in three characters.\n const label = running > 0 && running < total ? `${running}/${total}` : String(total)\n const words =\n running > 0 && running < total\n ? `${running} of ${total} ${noun}s running`\n : `${total} ${noun}${total === 1 ? '' : 's'}`\n const Chevron = expanded ? ChevronDown : ChevronRight\n return (\n <button\n type='button'\n aria-expanded={expanded}\n aria-label={`${expanded ? 'Hide' : 'Show'} ${words}`}\n title={`${expanded ? 'Hide' : 'Show'} ${words}`}\n onClick={(e) => {\n // The whole row is a button and this one does not mean \"select\" — the\n // same guard the row's overflow needs, and the reason a drag-select\n // inside the row does not toggle it.\n e.stopPropagation()\n onToggle()\n }}\n className={cn(\n 'flex shrink-0 items-center gap-0.5 rounded px-0.5 outline-none',\n 'hover:bg-surface-hover hover:text-fg-2',\n running > 0 ? 'text-info' : 'text-fg-4',\n )}>\n <Chevron className='size-3' />\n <span className='tabular-nums'>{label}</span>\n </button>\n )\n}\n\n/**\n * One step under its session. Pressing an **agent** hands the panel over to that\n * agent's own work — it is not a session and never becomes one, but it does now\n * have a surface (`SessionPanel.openSubagent`). A host that cannot frame one\n * falls back to revealing its `Task` row, which was this row's only meaning\n * before the takeover existed. A **task** is not pressable at all; see\n * {@link Step.kind}.\n *\n * Divided from the row's header and from each other by a rule rather than by\n * indentation: these are a list *inside* the row, and at 11px an indent is not\n * enough to say so. The rule is black at 25% so it darkens whatever the row is\n * filled with, selected or not, without needing a colour per state.\n */\nexport function StepRow({ step, onSelect }: { step: Step; onSelect: () => void }) {\n const agent = step.kind === 'agent'\n // Body colour by *kind*, state carried by the icon — the rule the transcript's\n // own `TaskRow` already follows, where the body is green and the marker holds\n // the beat. Green means sub-agent across this product (it is the one hue\n // nothing else on a session surface claims), so a list that spent blue on\n // \"running\" here would be saying something different from the transcript\n // about the same agent. Failure still outranks it: an alarm is not a category.\n const body = step.state === 'failed' ? 'text-danger' : agent ? 'text-success' : 'text-fg-4'\n const content = (\n <>\n <StepIcon state={step.state} kind={step.kind} />\n <span className='min-w-0 flex-1 truncate'>{step.label}</span>\n {/* The progress reading while it works, and what it cost when it is done.\n Zero draws nothing: `0 tools` beside a thinking agent reads as a stall,\n which is the same call `taskSummary` makes one surface over. */}\n {step.detail ? <span className='shrink-0 tabular-nums text-fg-4'>{step.detail}</span> : null}\n {agent ? <ArrowRight className='size-3 shrink-0 opacity-60' /> : null}\n </>\n )\n const shape =\n 'flex w-full items-center gap-2 border-t border-black/25 py-1 pl-3 pr-2 text-left text-label outline-none'\n\n // A task is not a button, in the markup and not merely in the styling: there\n // is nothing to press, and a disabled-looking button still announces itself as\n // one. It stops the click all the same — the whole session row is pressable\n // underneath, so falling through would open the session from a row that says\n // it does nothing.\n if (!agent) {\n return (\n <div title={step.title} onClick={(e) => e.stopPropagation()} className={cn(shape, body)}>\n {content}\n </div>\n )\n }\n return (\n <button\n type='button'\n title={step.title}\n onClick={(e) => {\n e.stopPropagation()\n onSelect()\n }}\n className={cn(shape, 'hover:bg-surface-hover', body)}>\n {content}\n </button>\n )\n}\n\nfunction StepIcon({ state, kind }: { state: Step['state']; kind: Step['kind'] }) {\n // A task that is neither running nor failed gets a neutral dot rather than a\n // tick: `done` is a claim about work, and nothing here did any.\n if (kind === 'task' && state !== 'running' && state !== 'failed') {\n return <Dot className='size-3 shrink-0' />\n }\n switch (state) {\n case 'running':\n return <Spinner className='size-3 shrink-0' />\n case 'failed':\n return <CircleAlert className='size-3 shrink-0' />\n case 'pending':\n return <PauseCircle className='size-3 shrink-0' />\n default:\n return <Check className='size-3 shrink-0' />\n }\n}\n","import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n BellRing,\n CircleAlert,\n CircleSlash,\n Layers,\n Moon,\n PauseCircle,\n Pencil,\n Search,\n SearchX,\n Trash2,\n X,\n} from 'lucide-react'\nimport {\n STATE_LABELS,\n STATE_ORDER,\n adaptersOf,\n clearFilters,\n filterRows,\n groupRows,\n hasFacetFilter,\n projectLabel,\n projectSubpath,\n projectsOf,\n sessionLabel,\n subsetSummary,\n} from '@workerdeck/protocol'\nimport type {\n GroupBy,\n SessionRow,\n SessionState,\n SortBy,\n ViewConfig,\n WorkspaceScope,\n} from '@workerdeck/protocol'\nimport { Button } from '../ui/Button.tsx'\nimport { Empty } from '../ui/Empty.tsx'\nimport { Input } from '../ui/Input.tsx'\nimport { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'\nimport { Spinner } from '../ui/Spinner.tsx'\nimport { ContextRing } from './ContextRing.tsx'\nimport { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'\nimport { ProjectIcon } from './ProjectIcon.tsx'\nimport { StepRow, StepToggle, runningSteps, sessionSteps } from './SessionSteps.tsx'\nimport { cn } from '../../lib/utils.ts'\nimport { formatCost, formatRelativeTime, friendlyModel } from '../../lib/format.ts'\n\n/**\n * A sessions list with the affordances a list of thirty needs: search, facets,\n * grouping, sorting, unread counts, and one honest line about what is hidden.\n *\n * The *rules* are `@workerdeck/protocol`'s (`filterRows`/`groupRows`/\n * `subsetSummary`), not this component's — the VS Code sidebar renders the same\n * model with workbench chrome, its activity-bar badge counts the same rows this\n * would show, and iOS mirrors them in Swift. What lives here is the styled\n * rendering of that model, so a host that wants the dashboard's look gets it\n * without reimplementing the model behind it.\n *\n * `SessionList` remains beside this for the plain case (a fixed set of rows, no\n * controls); this is what you reach for when the list is the screen.\n */\n\nexport interface SessionBrowserProps {\n rows: SessionRow[]\n config: ViewConfig\n onConfigChange: (config: ViewConfig) => void\n /** The host's own folders, if it has such a notion. Absent — a dashboard, a\n * phone — makes the scope filter genuinely inert rather than empty. */\n scope?: WorkspaceScope\n activeId?: string\n onSelect?: (row: SessionRow) => void\n onDelete?: (row: SessionRow) => void\n /**\n * Rename, from the row's pencil. Empty string restores the derived title. A\n * gateway edit (`PATCH /sessions/:id`), never a local override — every client\n * should see the same name. Omit to make titles read-only.\n *\n * A hover affordance rather than the extension's double-click-the-title,\n * because here a single click on the row navigates: the *first* click of a\n * double-click would have already left the page.\n */\n onRename?: (row: SessionRow, title: string) => void\n /** Open a session *at* one of its sub-agents, when the host can scroll a\n * transcript to a `Task` row. Absent, the sub-agent list still expands and a\n * step just opens its session — see `SessionRowItemProps`. */\n onSelectSubagent?: (row: SessionRow, toolUseId: string) => void\n /** Rendered when nothing at all exists (as opposed to nothing matching). */\n emptyState?: React.ReactNode\n /**\n * Whether the search + facet bar is shown. Defaults to `true` — a list that\n * *is* the screen shows its controls.\n *\n * A host with somewhere better to put the toggle (a view title bar) passes\n * `false` and owns the boolean itself, the way the VS Code extension does: the\n * key lives where the commands do. Two rules come with it, and they are why\n * this hides only the bar and nothing else — **closing the bar never clears\n * the filters**, and the subset line below it renders either way, so a list\n * filtered by a control you can't currently see still says so.\n */\n showControls?: boolean\n /**\n * Resolved project-icon bytes by content hash — `useProjectIcons`' output.\n *\n * Passed in rather than fetched here for the reason `ProjectIcon` states: the\n * wire carries an *address*, and who can fetch it differs per client. Absent,\n * or a hash not in it yet, simply draws no picture; the project's name is\n * already there.\n */\n projectIcons?: Record<string, string>\n className?: string\n}\n\n/**\n * How a list row is drawn, in one place, so `SidebarRow` in `web` matches this\n * exactly rather than approximating it — the dashboard's other three sidebars\n * are that component, and a sessions list that hovered differently from the\n * gateways list beside it would read as a different product.\n *\n * Two rules are load-bearing:\n *\n * - **Fill means hover, and only hover.** It stays on the row whether or not\n * the row is selected, because a selected row still has to answer the\n * pointer. Selection gets the gutter instead.\n * - **`ml-0` on the selected row is not cosmetic.** It hands the accent border\n * the 4px the margin was holding, so the text does not shift sideways as a\n * row becomes the selected one. The squared left corners are what let the bar\n * sit flush against the sidebar edge.\n */\nexport function rowShapeClass(active: boolean): string {\n return cn(\n 'px-2 py-1.5 hover:bg-row-hover',\n active ? 'mr-1 ml-0 rounded-r-md border-l-4 border-l-accent' : 'mx-1 rounded-md',\n )\n}\n\nexport function SessionBrowser({\n rows,\n config,\n onConfigChange,\n scope,\n activeId,\n onSelect,\n onDelete,\n onRename,\n onSelectSubagent,\n emptyState,\n showControls = true,\n projectIcons,\n className,\n}: SessionBrowserProps) {\n const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])\n const groups = useMemo(() => groupRows(visible, config), [visible, config])\n const subset = subsetSummary(config, scope, visible.length, rows.length)\n // Derived, not enumerated: a new engine or a new gateway needs no change here,\n // and a facet with one possible value is not a choice worth showing.\n const adapters = useMemo(() => adaptersOf(rows), [rows])\n const projects = useMemo(() => projectsOf(rows), [rows])\n const gateways = useMemo(() => {\n const seen = new Map<string, string>()\n for (const row of rows) seen.set(row.hostId, row.hostName)\n return [...seen].map(([id, name]) => ({ id, name }))\n }, [rows])\n\n const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })\n\n return (\n <div data-slot='session-browser' className={cn('flex flex-col gap-3', className)}>\n {/* One control per row, label left, input right — the shape VS Code uses\n for its own filter surfaces. A wrapping row of pill-shaped selects\n reflows into an unpredictable number of lines as facets appear and\n disappear; a column of labelled rows is the same height every time and\n reads at sidebar width, which is the only width this has. */}\n <div className={cn('flex flex-col gap-1.5 px-2', !showControls && 'hidden')}>\n <div className='relative'>\n <Search className='pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-fg-4' />\n <Input\n value={config.search}\n onChange={(e) => set({ search: e.target.value })}\n placeholder='Search sessions'\n aria-label='Search sessions'\n className='pl-8'\n />\n </div>\n <FilterRow label='State'>\n <FacetSelect\n label='State'\n value={config.states}\n options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}\n onChange={(states) => set({ states: states as SessionState[] })}\n />\n </FilterRow>\n {adapters.length > 1 ? (\n <FilterRow label='Engine'>\n <FacetSelect\n label='Engine'\n value={config.adapters}\n options={adapters.map((a) => ({ value: a, label: a }))}\n onChange={(adapters) => set({ adapters })}\n />\n </FilterRow>\n ) : null}\n {gateways.length > 1 ? (\n <FilterRow label='Gateway'>\n <FacetSelect\n label='Gateway'\n value={config.gateways}\n options={gateways.map((g) => ({ value: g.id, label: g.name }))}\n onChange={(gateways) => set({ gateways })}\n />\n </FilterRow>\n ) : null}\n {projects.length > 1 ? (\n <FilterRow label='Project'>\n <FacetSelect\n label='Project'\n value={config.projects ?? []}\n options={projects.map((p) => ({ value: p.key, label: p.label }))}\n onChange={(next) => set({ projects: next })}\n />\n </FilterRow>\n ) : null}\n <FilterRow label='Group'>\n <OneOfSelect\n label='Group'\n value={config.groupBy}\n options={[\n { value: 'none', label: 'No grouping' },\n { value: 'state', label: 'By state' },\n { value: 'adapter', label: 'By engine' },\n ...(projects.length > 1 ? [{ value: 'project' as const, label: 'By project' }] : []),\n ...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'By gateway' }] : []),\n ]}\n onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}\n />\n </FilterRow>\n <FilterRow label='Sort'>\n <OneOfSelect\n label='Sort'\n value={config.sortBy}\n options={[\n { value: 'recent', label: 'Recent' },\n { value: 'name', label: 'Name' },\n { value: 'state', label: 'State' },\n ...(projects.length > 1 ? [{ value: 'project' as const, label: 'Project' }] : []),\n ...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'Gateway' }] : []),\n ]}\n onChange={(sortBy) => set({ sortBy: sortBy as SortBy })}\n />\n </FilterRow>\n </div>\n\n {subset ? (\n <div className='flex items-center gap-2 px-3 text-label text-fg-4'>\n <span>\n {subset.shown} of {subset.total}\n {subset.causes.length ? ` · ${subset.causes.join(' · ')}` : null}\n </span>\n <button\n type='button'\n className='text-fg-3 underline underline-offset-2 hover:text-fg-1'\n onClick={() => onConfigChange(clearFilters(config))}>\n Show all\n </button>\n </div>\n ) : null}\n\n {rows.length === 0 ? (\n (emptyState ?? <Empty icon={<Layers />} title='No sessions yet' />)\n ) : visible.length === 0 ? (\n // Two different dead ends, two different ways out. \"Nothing matches\" is\n // a filter someone set; anything else is the state of the world — and\n // only the first has a button, because an action that does nothing is\n // worse than none.\n hasFacetFilter(config) ? (\n <Empty\n icon={<SearchX />}\n title='No matches'\n description='No session matches the current search and filters.'\n action='Clear filters'\n onAction={() => onConfigChange(clearFilters(config))}\n />\n ) : (\n <Empty icon={<Layers />} title='Nothing here' description='No session to show.' />\n )\n ) : (\n <div className='flex flex-col gap-4'>\n {groups.map((group) => (\n <div key={group.key} className='flex flex-col gap-1'>\n {config.groupBy !== 'none' && group.label ? (\n <div className='flex items-center gap-2 px-3 text-label font-medium text-fg-4'>\n {/* Only the project facet has a mark of its own, and a group\n IS one project root, so the first row is a fair source. */}\n {config.groupBy === 'project' ? (\n <ProjectIcon\n icon={group.rows[0]?.info.project?.icon}\n src={iconSrcOf(group.rows[0], projectIcons)}\n name={group.label}\n />\n ) : null}\n <span className='uppercase tracking-wide'>{group.label}</span>\n <span className='text-fg-4/70'>{group.rows.length}</span>\n </div>\n ) : null}\n {group.rows.map((row) => (\n <SessionRowItem\n key={`${row.hostId}:${row.info.id}`}\n row={row}\n active={row.info.id === activeId}\n showGateway={gateways.length > 1 && config.groupBy !== 'gateway'}\n showProject={config.groupBy !== 'project'}\n projectIcons={projectIcons}\n onSelect={onSelect}\n onDelete={onDelete}\n onRename={onRename}\n onSelectSubagent={onSelectSubagent}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n/** The bytes for a row's project icon, if it has an image one and the caller has\n * fetched it yet. Shared by the row and its group header so the two cannot draw\n * different pictures for one project. */\nfunction iconSrcOf(\n row: SessionRow | undefined,\n icons: Record<string, string> | undefined,\n): string | undefined {\n const icon = row?.info.project?.icon\n return icon?.type === 'image' ? icons?.[icon.hash] : undefined\n}\n\ninterface SessionRowItemProps {\n row: SessionRow\n active?: boolean\n showGateway?: boolean\n /** False when the list is already grouped by project — the header has said the\n * name, so the row must not spend its metadata line repeating it. What takes\n * the slot is the *sub-path inside the project* (`projectSubpath`), which is\n * the one thing the header cannot say and the only thing that tells two\n * sessions in the same repo apart; a session sitting at the project root has\n * nothing to add and the slot disappears entirely. The rule `showGateway`\n * follows one facet over. */\n showProject?: boolean\n projectIcons?: Record<string, string>\n onSelect?: (row: SessionRow) => void\n onDelete?: (row: SessionRow) => void\n onRename?: (row: SessionRow, title: string) => void\n /** Open one of a session's sub-agents — which now means handing the session\n * panel over to that agent's own work (`SessionPanel.openSubagent`), and meant\n * \"scroll to its `Task` row\" before that surface existed. Both are honest; a\n * host picks what it can draw. Optional, and its absence is not a missing\n * feature: without it a step just opens the session, which is all a host with\n * neither can offer. Only *agent* steps ever call it — see `Step.kind`. */\n onSelectSubagent?: (row: SessionRow, toolUseId: string) => void\n}\n\nfunction SessionRowItem({\n row,\n active,\n showGateway,\n showProject = true,\n projectIcons,\n onSelect,\n onDelete,\n onRename,\n onSelectSubagent,\n}: SessionRowItemProps) {\n const { info } = row\n const [editing, setEditing] = useState(false)\n // Expansion is the row's own state and deliberately not persisted: a list that\n // reopened yesterday's work on every reload would be showing a settled tail\n // nobody asked for.\n const [expanded, setExpanded] = useState(false)\n\n // What it is and what it has spent, in one line — the same set the extension\n // shows, joined the same way, so the two lists read as one product.\n // protocol's own spelling, so this row, the group header above it and the\n // project facet cannot disagree about what a project is called. Under a\n // project group it is the sub-path instead — see `showProject` — and a session\n // at the root contributes nothing at all rather than a name already on screen.\n const project = showProject ? projectLabel(row) : projectSubpath(row)\n const projectIcon = showProject ? info.project?.icon : undefined\n const engine = info.engine ?? 'claude'\n // Everything after the model. The model itself is drawn separately because it\n // is the one segment that wears a colour — see `vendorTextClass` — and a\n // coloured run inside a joined string would have to be spliced back out.\n const details = [\n project,\n showGateway ? row.hostName : undefined,\n info.profile ? `@${info.profile}` : undefined,\n formatCost(info.totalCostUsd),\n ].filter(Boolean)\n const steps = sessionSteps(info, (toolUseId) =>\n onSelectSubagent ? onSelectSubagent(row, toolUseId) : onSelect?.(row),\n )\n // Where the icon goes: immediately before the project's own name, wherever\n // that landed in the joined line. Split rather than interleaved as nodes,\n // because everything here is one truncating mono run and a flex of pieces\n // would each shrink a little and leave several half-words.\n const cut = project === undefined ? -1 : details.indexOf(project)\n\n return (\n <div\n data-slot='session-row'\n data-active={active || undefined}\n // Selection lives on the whole row, not on the two text buttons inside\n // it: the age, the unread badge and the state glyph sit outside them, and\n // a row where a third of the surface silently does nothing is a row that\n // feels broken. The buttons stay — they are what makes the row reachable\n // by keyboard — but their activation now reaches this handler by\n // bubbling, so there is one code path and no double-fire. Anything that\n // is its own action (rename, close, the name editor) stops the event.\n onClick={() => !editing && onSelect?.(row)}\n className={cn(\n 'group flex cursor-pointer flex-col gap-0.5 text-left transition-colors',\n rowShapeClass(active === true),\n )}>\n {/* Line one: how it is doing, then what you scan the list by, then how\n long since it moved.\n\n State leads. It used to sit last, on the argument that the title is\n what you read and the state is what you glance at — but a glance wants\n a *column*, and a trailing glyph has no fixed x: it lands wherever the\n age and the ring leave it. Leading, every row's state stacks into one\n scannable strip, and the mark on line two lands under it in the same\n gutter. This is the VS Code sidebar's shape, and all three clients are\n on it now. */}\n <div className='flex items-center gap-1.5'>\n <Gutter>\n <SessionStatusIcon row={row} />\n </Gutter>\n {/* The editor replaces the link rather than sitting inside it — an\n input nested in a button is invalid, and disabling the button to\n protect the edit disables the field with it. */}\n {editing && onRename ? (\n <NameEditor\n initial={info.title ?? ''}\n onCommit={(title) => {\n setEditing(false)\n onRename(row, title)\n }}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <button\n type='button'\n className={cn(\n 'min-w-0 flex-1 truncate text-left text-body-sm outline-none',\n active ? 'font-medium text-fg-1' : 'text-fg-2',\n )}>\n {sessionLabel(info)}\n </button>\n )}\n {row.unseen > 0 ? (\n // Transcript rows since this session was last on screen — the same\n // unit the VS Code badge counts, because turns undercount badly.\n <span\n title={`${row.unseen} new`}\n className='shrink-0 rounded-full bg-accent px-1.5 text-label text-accent-fg'>\n {row.unseen}\n </span>\n ) : null}\n <span className='shrink-0 text-label text-fg-4'>\n {formatRelativeTime(info.lastActivityAt ?? info.createdAt)}\n </span>\n <ContextRing usage={info.contextUsage} />\n </div>\n\n {/* Line two: what it is — the engine's mark and its model in the vendor's\n own colour, then where it runs — with the actions at the far right,\n away from the title you are actually reading. */}\n <div className='flex items-center gap-1 text-label text-fg-4'>\n <Gutter>\n {/* The colour goes on the icon, not on this cell: the svg ships its\n own `text-fg-3` and only a class on the element itself merges over\n it. */}\n <EngineIcon\n engine={engine}\n model={info.model}\n className={vendorMarkClass(engine, info.model)}\n />\n </Gutter>\n <button\n type='button'\n tabIndex={-1}\n className='min-w-0 flex-1 truncate text-left font-mono outline-none'>\n <span className={vendorTextClass(engine, info.model)}>\n {/* The model id as a person says it — `claude-opus-5[1m]` is a wire\n value, and a list line has no room for a context-window suffix. */}\n {friendlyModel(info.model)}\n </span>\n {details.length > 0 ? ' · ' : ''}\n {cut < 0 ? (\n details.join(' · ')\n ) : (\n <>\n {details.slice(0, cut).map((part) => `${part} · `)}\n <ProjectIcon\n icon={projectIcon}\n src={iconSrcOf(row, projectIcons)}\n name={project}\n /* Nudged onto the text baseline: a 12px glyph box against an\n 11px line sits a hair proud without it. */\n className='mr-1 align-[-0.2em]'\n />\n {details.slice(cut).join(' · ')}\n </>\n )}\n </button>\n {steps.length > 0 ? (\n <StepToggle\n expanded={expanded}\n running={runningSteps(steps)}\n total={steps.length}\n noun={steps[0]!.noun}\n onToggle={() => setExpanded((v) => !v)}\n />\n ) : null}\n {onRename && !editing ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Rename session'\n className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'\n onClick={(e) => {\n e.stopPropagation()\n setEditing(true)\n }}>\n <Pencil className='size-3 text-fg-3' />\n </Button>\n ) : null}\n {onDelete ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Close session'\n className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'\n onClick={(e) => {\n e.stopPropagation()\n onDelete(row)\n }}>\n <Trash2 className='size-3 text-fg-3' />\n </Button>\n ) : null}\n </div>\n {expanded ? steps.map((step) => <StepRow key={step.key} step={step} onSelect={step.onSelect} />) : null}\n </div>\n )\n}\n\n/**\n * The one 14px cell both lines hang their glyph in.\n *\n * A shared column rather than a leading character: the state and the engine mark\n * are different widths and a bare glyph would let the two text columns start at\n * different x. Centring inside a fixed cell is what makes them agree —\n * `packages/ui`'s terminal-gutter argument at row scale.\n */\nfunction Gutter({ children }: { children: ReactNode }) {\n return <span className='flex w-3.5 shrink-0 items-center justify-center'>{children}</span>\n}\n\n/**\n * State as one glyph — a ringing bell when it wants a human, a spinner while it\n * works, a moon when it is only sleeping. Replaces the text badge: in a sidebar\n * the word costs more room than it earns, and the states that matter are the two\n * you can recognise without reading.\n *\n * **It reads `row.state`, not `info.status`, and that distinction is the whole\n * point of the row model.** `sessionState` already folds in the arm this glyph\n * cannot see for itself: a *background* sub-agent outlives its turn by design, so\n * the turn ends, `status` comes to rest at `idle`, and the agent keeps working.\n * Reading the raw status drew a **moon on a row filed under the \"Working\"\n * header** — the list contradicting itself on one line, which is exactly what a\n * derived view model exists to prevent. The value was in scope and unread.\n *\n * The terminal statuses still come off `info.status`, because `ended` collapses\n * `failed` and `closed` into one bucket and those are worth telling apart here.\n */\nexport function SessionStatusIcon({ row }: { row: SessionRow }) {\n const { info } = row\n if (row.state === 'attention') {\n return <BellRing className='size-3.5 shrink-0 animate-pulse text-warning' />\n }\n if (row.state === 'working') {\n return <Spinner className='size-3.5 shrink-0 text-info' />\n }\n switch (info.status) {\n case 'failed':\n return <CircleAlert className='size-3.5 shrink-0 text-danger' />\n case 'closed':\n return <CircleSlash className='size-3.5 shrink-0 text-fg-4' />\n case 'parked':\n return <PauseCircle className='size-3.5 shrink-0 text-fg-3' />\n default:\n return <Moon className='size-3.5 shrink-0 text-fg-4' />\n }\n}\n\n/**\n * Inline rename. Enter commits, Escape cancels, blur commits — but only a blur\n * that is still inside this document: switching windows must not silently commit,\n * nor kill the editor in the frame it opened.\n */\nfunction NameEditor({\n initial,\n onCommit,\n onCancel,\n}: {\n initial: string\n onCommit: (title: string) => void\n onCancel: () => void\n}) {\n const [value, setValue] = useState(initial)\n const ref = useRef<HTMLInputElement>(null)\n useEffect(() => {\n ref.current?.select()\n }, [])\n return (\n <input\n ref={ref}\n value={value}\n autoFocus\n aria-label='Session name'\n // The row is a button; a click in here must not select the session.\n onClick={(e) => e.stopPropagation()}\n onChange={(e) => setValue(e.target.value)}\n onBlur={() => (document.hasFocus() ? onCommit(value.trim()) : undefined)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') onCommit(value.trim())\n else if (e.key === 'Escape') onCancel()\n e.stopPropagation()\n }}\n className='-my-0.5 min-w-0 flex-1 rounded-sm border border-ring bg-bg px-1 py-0.5 text-body-sm font-medium text-fg-1 outline-none'\n />\n )\n}\n\n/** A multi-select facet. Empty = no filter, which is why the trigger reads the\n * facet's name rather than \"All\": nothing is being excluded. */\n/**\n * One labelled control row. The label column is fixed so every control starts\n * on the same x — the thing that makes a stack of them read as a form rather\n * than as five unrelated widgets.\n */\nfunction FilterRow({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className='flex items-center gap-2'>\n <span aria-hidden className='w-14 shrink-0 truncate text-label text-fg-3'>\n {label}\n </span>\n <div className='min-w-0 flex-1'>{children}</div>\n </div>\n )\n}\n\nfunction FacetSelect({\n label,\n value,\n options,\n onChange,\n}: {\n label: string\n value: string[]\n options: { value: string; label: string }[]\n onChange: (value: string[]) => void\n}) {\n return (\n <div className='flex items-center gap-1'>\n <Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>\n <SelectTrigger aria-label={label} className='min-w-0 flex-1'>\n <SelectValue>\n {/* \"All\" rather than the label, which the row already carries. */}\n {value.length === 0\n ? 'All'\n : value.length === 1\n ? (options.find((o) => o.value === value[0])?.label ?? 'All')\n : `${value.length} selected`}\n </SelectValue>\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n <SelectItemText>{option.label}</SelectItemText>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {value.length > 0 ? (\n <Button variant='ghost' size='icon-sm' aria-label={`Clear ${label}`} onClick={() => onChange([])}>\n <X className='size-3 text-fg-4' />\n </Button>\n ) : null}\n </div>\n )\n}\n\nfunction OneOfSelect({\n label,\n value,\n options,\n onChange,\n}: {\n label: string\n value: string\n options: readonly { value: string; label: string }[]\n onChange: (value: string) => void\n}) {\n return (\n <Select value={value} onValueChange={(v) => onChange(v as string)}>\n <SelectTrigger aria-label={label} className='w-full min-w-0'>\n <SelectValue>{options.find((o) => o.value === value)?.label ?? label}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n <SelectItemText>{option.label}</SelectItemText>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["AlertDialogPrimitive","SonnerToaster","Map"],"sources":["../src/components/ui/Card.tsx","../src/components/ui/Textarea.tsx","../src/components/ui/AlertDialog.tsx","../src/components/ui/Sonner.tsx","../src/components/ui/Empty.tsx","../src/components/prompt-area/segment-helpers.ts","../src/components/terminal/StatusLine.tsx","../src/components/agent/SessionList.tsx","../src/components/agent/ContextRing.tsx","../src/components/agent/EngineIcon.tsx","../src/components/agent/ProjectIcon.tsx","../src/components/agent/SessionSteps.tsx","../src/components/agent/SessionBrowser.tsx"],"sourcesContent":["import { type HTMLAttributes } from 'react'\nimport { cn } from '../../lib/utils.ts'\n\nexport function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n data-slot='card'\n className={cn('rounded-lg border border-border bg-surface', className)}\n {...props}\n />\n )\n}\n\nexport function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n className={cn('flex items-start justify-between gap-3 px-4 pt-3.5 pb-3', className)}\n {...props}\n />\n )\n}\n\nexport function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {\n return <h3 className={cn('text-heading-3 font-semibold text-text', className)} {...props} />\n}\n\nexport function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('px-4 pb-4', className)} {...props} />\n}\n","import { type TextareaHTMLAttributes, forwardRef } from 'react'\nimport { cn } from '../../lib/utils.ts'\n\nexport const Textarea = forwardRef<\n HTMLTextAreaElement,\n TextareaHTMLAttributes<HTMLTextAreaElement>\n>(({ className, ...props }, ref) => (\n <textarea\n ref={ref}\n data-slot='textarea'\n className={cn(\n 'w-full resize-none rounded-md border border-border bg-bg px-2.5 py-2 text-body-sm text-text',\n 'placeholder:text-fg-4 transition-colors outline-none',\n 'hover:border-border-strong focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40',\n 'disabled:pointer-events-none disabled:opacity-50',\n className,\n )}\n {...props}\n />\n))\nTextarea.displayName = 'Textarea'\n","import { type FunctionComponent } from 'react'\nimport { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'\nimport { cn } from '../../lib/utils.ts'\n\nexport const AlertDialog = AlertDialogPrimitive.Root\nexport const AlertDialogTrigger = AlertDialogPrimitive.Trigger\nexport const AlertDialogClose = AlertDialogPrimitive.Close\n\nexport const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Props> = ({\n className,\n children,\n ...props\n}) => (\n <AlertDialogPrimitive.Portal>\n <AlertDialogPrimitive.Backdrop\n className={cn(\n 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',\n 'transition-opacity duration-(--motion-base)',\n 'data-starting-style:opacity-0 data-ending-style:opacity-0',\n )}\n />\n <AlertDialogPrimitive.Popup\n data-slot='alert-dialog-content'\n className={cn(\n 'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',\n 'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',\n 'transition-[opacity,transform] duration-(--motion-base)',\n 'data-starting-style:scale-95 data-starting-style:opacity-0',\n 'data-ending-style:scale-95 data-ending-style:opacity-0',\n className,\n )}\n {...props}>\n {children}\n </AlertDialogPrimitive.Popup>\n </AlertDialogPrimitive.Portal>\n)\n\nexport const AlertDialogTitle: FunctionComponent<AlertDialogPrimitive.Title.Props> = ({\n className,\n ...props\n}) => (\n <AlertDialogPrimitive.Title\n className={cn('text-heading-3 font-semibold text-text', className)}\n {...props}\n />\n)\n\nexport const AlertDialogDescription: FunctionComponent<AlertDialogPrimitive.Description.Props> = ({\n className,\n ...props\n}) => (\n <AlertDialogPrimitive.Description\n className={cn('mt-1.5 text-body-sm text-muted-foreground', className)}\n {...props}\n />\n)\n","import { Toaster as SonnerToaster, toast } from 'sonner'\n\nexport { toast }\n\n/** Token-themed toaster; relies on the [data-theme] swap, so no `theme` prop needed. */\nexport function Toaster() {\n return (\n <SonnerToaster\n position='bottom-right'\n toastOptions={{\n style: {\n background: 'var(--surface)',\n color: 'var(--text)',\n border: '1px solid var(--border)',\n boxShadow: 'var(--shadow-md)',\n fontFamily: 'var(--cw-font-sans)',\n fontSize: 'var(--text-body-sm)',\n },\n }}\n />\n )\n}\n","import type { ReactNode } from 'react'\nimport { Button } from './Button.tsx'\n\n/**\n * A view with nothing in it: icon, title, description, and at most one action.\n *\n * Centered and given room, because an empty view is the first thing a new\n * install shows and a flush-left sentence in the top corner reads like a bug\n * report. It lives here rather than in one client because every panel that can\n * be empty should be empty the same way — the extension's views and the\n * dashboard's four sidebars are the same shape of thing.\n *\n * **At most one action, and never one the view header already offers.** Creating\n * a session and adding a gateway are the `+` in the native title bar, exclusively\n * — so those empty states point at it in words rather than growing a second\n * button that does the same thing two inches lower. A button here is for the way\n * out of a state the header has no answer to: clearing a filter, widening a\n * scope.\n */\nexport function Empty({\n icon,\n title,\n description,\n action,\n onAction,\n}: {\n icon: ReactNode\n title: string\n description?: ReactNode\n action?: string\n onAction?: () => void\n}) {\n return (\n <div className='flex flex-col items-center gap-2 px-5 py-8 text-center'>\n <div className='text-fg-4 opacity-60 [&_svg]:size-7'>{icon}</div>\n <p className='text-body-sm font-medium text-fg-2'>{title}</p>\n {description ? (\n <p className='max-w-[26ch] text-balance text-label leading-relaxed text-fg-4'>\n {description}\n </p>\n ) : null}\n {action && onAction ? (\n <Button variant='outline' size='sm' className='mt-1' onClick={onAction}>\n {action}\n </Button>\n ) : null}\n </div>\n )\n}\n\n/** A key or icon named inline in an empty state's description — \"use the + above\". */\nexport function EmptyKey({ children }: { children: ReactNode }) {\n return (\n <span className='rounded border border-border px-1 font-mono text-fg-3'>{children}</span>\n )\n}\n","/**\n * Convenience helpers for creating and inspecting Segments.\n *\n * These reduce boilerplate when building AI chat UIs that work with the\n * PromptArea document model.\n *\n * @example\n * ```ts\n * import { text, chip, isSegmentsEmpty, segmentsToPlainText } from './segment-helpers.ts'\n *\n * const greeting = [text('Hello '), chip({ trigger: '@', value: 'u1', displayText: 'Alice' })]\n * isSegmentsEmpty(greeting) // false\n * segmentsToPlainText(greeting) // \"Hello @Alice\"\n * ```\n */\n\nimport type { Segment, TextSegment, ChipSegment } from './types.ts'\nimport { segmentsToPlainText, plainTextToSegments } from './prompt-area-engine.ts'\n\n// Re-export serialization utilities so consumers have a single import.\nexport { segmentsToPlainText, plainTextToSegments }\n\n// ---------------------------------------------------------------------------\n// Factories\n// ---------------------------------------------------------------------------\n\n/** Create a text segment. */\nexport function text(value: string): TextSegment {\n return { type: 'text', text: value }\n}\n\n/** Create a chip segment. */\nexport function chip(opts: Omit<ChipSegment, 'type'>): ChipSegment {\n return { type: 'chip', ...opts }\n}\n\n// ---------------------------------------------------------------------------\n// Predicates\n// ---------------------------------------------------------------------------\n\n/** Returns `true` when the segment array is empty or contains only whitespace text. */\nexport function isSegmentsEmpty(segments: Segment[]): boolean {\n if (segments.length === 0) return true\n return segments.every((seg) => seg.type === 'text' && seg.text.trim() === '')\n}\n\n/** Returns `true` when the segment array contains at least one chip. */\nexport function hasChips(segments: Segment[]): boolean {\n return segments.some((seg) => seg.type === 'chip')\n}\n\n/** Extracts all chip segments from a segment array. */\nexport function getChips(segments: Segment[]): ChipSegment[] {\n return segments.filter((seg): seg is ChipSegment => seg.type === 'chip')\n}\n\n/** Extracts chips matching a specific trigger character. */\nexport function getChipsByTrigger(segments: Segment[], trigger: string): ChipSegment[] {\n return segments.filter(\n (seg): seg is ChipSegment => seg.type === 'chip' && seg.trigger === trigger,\n )\n}\n","import type { RateLimitInfo } from '@workerdeck/protocol'\nimport type { ConnectionState, TranscriptState } from '@workerdeck/react'\nimport { formatCost, formatTokens } from '../../lib/format.ts'\nimport {\n contextSeverity,\n meterSeverity,\n modelLabel,\n statusPresentation,\n tightestWindow,\n windowLabel,\n type StatusSeverity,\n} from '../../lib/status.ts'\nimport { Ink, Row, type Tone } from './row.tsx'\n\n/**\n * The session's readings, as one line.\n *\n * The styled bar puts each of these in its own affordance — a progress ring, a\n * badge, a tooltip — which is right for a dashboard and wrong here for the usual\n * reason: a terminal has one line height, and five widgets of five different\n * heights is five different rhythms in a row that is supposed to be quiet. So\n * they are words, `·`-separated, exactly as the CLI's own status line writes\n * them.\n *\n * The readings themselves come from the same helpers the styled bar uses\n * (`lib/status.ts`), and that matters more than it looks: `statusPresentation`\n * is where the rule lives that **a dropped socket outranks the session status**,\n * because a status held over a dead socket is a stale reading being presented as\n * a live one. Re-deriving that here would be a second answer to a question that\n * already has one.\n */\n\n/** Severity → tone. The 80/95 thresholds are `lib/status.ts`'s, not new ones. */\nconst SEVERITY_TONE: Record<StatusSeverity, Tone> = {\n none: 'dim',\n warning: 'yellow',\n error: 'red',\n}\n\nexport interface TerminalStatusLineProps {\n state: TranscriptState\n /** Plan windows to read from, when they should not be the session's own — the\n * gateway's per-profile state merged over this transcript's. See\n * {@link StatusBarProps.rateLimits}. Absent = `state.rateLimits`. */\n rateLimits?: Record<string, RateLimitInfo>\n /** How the client is doing at reaching the gateway. Wins the status slot when\n * the socket is down — see above. */\n connection?: ConnectionState\n /** Opens the panel that answers each reading's own question. Omit and the\n * reading is text rather than something to press. */\n onOpenStatus?: () => void\n onOpenContext?: () => void\n onOpenUsage?: () => void\n}\n\n/** One reading. A button only when it leads somewhere — a segment that looks\n * pressable and does nothing is worse than plain text. */\nfunction Reading({\n tone,\n onPress,\n label,\n children,\n}: {\n tone?: Tone\n onPress?: () => void\n label?: string\n children: React.ReactNode\n}) {\n if (!onPress) return <Ink tone={tone}>{children}</Ink>\n return (\n <button type='button' className='term-reading' onClick={onPress} title={label}>\n <Ink tone={tone}>{children}</Ink>\n </button>\n )\n}\n\nexport function TerminalStatusLine({\n state,\n rateLimits,\n connection,\n onOpenStatus,\n onOpenContext,\n onOpenUsage,\n}: TerminalStatusLineProps) {\n const presentation = statusPresentation({ status: state.status, connection })\n const usage = state.contextUsage\n const window = tightestWindow(rateLimits ?? state.rateLimits)\n const model = modelLabel({ model: state.model, models: state.models ?? [] })\n\n const parts: React.ReactNode[] = [\n <Reading\n key='status'\n tone={SEVERITY_TONE[presentation.severity]}\n onPress={onOpenStatus}\n label='Session status'>\n {presentation.label}\n </Reading>,\n ]\n\n if (usage) {\n parts.push(\n <Reading\n key='context'\n tone={SEVERITY_TONE[contextSeverity(usage)]}\n onPress={onOpenContext}\n label='Context window'>\n {Math.round(usage.percentage)}% ctx ({formatTokens(usage.totalTokens)})\n </Reading>,\n )\n }\n\n if (window) {\n parts.push(\n <Reading\n key='usage'\n tone={SEVERITY_TONE[meterSeverity(window.info.utilization)]}\n onPress={onOpenUsage}\n label='Plan usage'>\n {Math.round(window.info.utilization ?? 0)}% {windowLabel(window.key)}\n </Reading>,\n )\n }\n\n // Cost last of the numbers: it is the reading you check, never the one you act\n // on, and nothing about a turn changes because of it.\n if (state.totalCostUsd > 0) {\n parts.push(\n <Ink key='cost' tone='faint'>\n {formatCost(state.totalCostUsd)}\n </Ink>,\n )\n }\n\n if (model) {\n parts.push(\n <Ink key='model' tone='faint'>\n {model}\n </Ink>,\n )\n }\n if (state.permissionMode && state.permissionMode !== 'default') {\n parts.push(\n <Ink key='mode' tone='yellow'>\n {state.permissionMode}\n </Ink>,\n )\n }\n\n return (\n <Row data-slot='status-line' tone='dim'>\n {parts.map((part, index) => (\n <span key={index}>\n {index > 0 ? <Ink tone='faint'> · </Ink> : null}\n {part}\n </span>\n ))}\n </Row>\n )\n}\n","import type { SessionInfo } from '@workerdeck/protocol'\nimport { Trash2 } from 'lucide-react'\nimport { Badge } from '../ui/Badge.tsx'\nimport { Button } from '../ui/Button.tsx'\nimport { cn } from '../../lib/utils.ts'\nimport { formatCost, formatRelativeTime } from '../../lib/format.ts'\nimport { STATUS_META } from './status.ts'\n\nexport interface SessionListItemProps {\n session: SessionInfo\n active?: boolean\n onSelect?: (id: string) => void\n onDelete?: (id: string) => void\n}\n\nexport function SessionListItem({ session, active, onSelect, onDelete }: SessionListItemProps) {\n const meta = STATUS_META[session.status]\n return (\n <div\n data-slot='session-list-item'\n data-active={active || undefined}\n className={cn(\n 'group flex w-full items-center gap-2 rounded-md border border-transparent px-2.5 py-2 text-left transition-colors',\n active ? 'border-border bg-surface' : 'hover:bg-surface-hover',\n )}>\n <button\n type='button'\n onClick={() => onSelect?.(session.id)}\n className='min-w-0 flex-1 text-left outline-none'>\n <div className='flex items-center gap-2'>\n <span className='truncate text-body-sm font-medium text-fg-1'>\n {session.title ?? session.id.slice(0, 8)}\n </span>\n <Badge variant={meta.variant} dot className='shrink-0'>\n {meta.label}\n </Badge>\n </div>\n <div className='mt-0.5 flex items-center gap-2 font-mono text-label text-fg-4'>\n <span className='truncate'>{session.cwd}</span>\n {session.profile ? <span className='shrink-0'>@{session.profile}</span> : null}\n <span className='shrink-0'>{formatCost(session.totalCostUsd)}</span>\n <span className='shrink-0'>{formatRelativeTime(session.lastActivityAt ?? session.createdAt)}</span>\n </div>\n </button>\n {onDelete ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Close session'\n className='opacity-0 transition-opacity group-hover:opacity-100'\n onClick={() => onDelete(session.id)}>\n <Trash2 className='size-3.5 text-fg-3' />\n </Button>\n ) : null}\n </div>\n )\n}\n\nexport interface SessionListProps {\n sessions: SessionInfo[]\n activeId?: string\n onSelect?: (id: string) => void\n onDelete?: (id: string) => void\n emptyText?: string\n className?: string\n}\n\nexport function SessionList({\n sessions,\n activeId,\n onSelect,\n onDelete,\n emptyText = 'No sessions yet.',\n className,\n}: SessionListProps) {\n return (\n <div data-slot='session-list' className={cn('flex flex-col gap-1', className)}>\n {sessions.length === 0 ? (\n <div className='px-2.5 py-6 text-center text-body-sm text-fg-4'>{emptyText}</div>\n ) : (\n sessions.map((session) => (\n <SessionListItem\n key={session.id}\n session={session}\n active={session.id === activeId}\n onSelect={onSelect}\n onDelete={onDelete}\n />\n ))\n )}\n </div>\n )\n}\n","import type { ContextReading } from '@workerdeck/protocol'\nimport { ProgressRing } from '../ui/ProgressRing.tsx'\nimport { cn } from '../../lib/utils.ts'\nimport { formatTokens } from '../../lib/format.ts'\nimport { meterColorClass } from '../../lib/status.ts'\n\n/**\n * How full a session's context window is, sized for a **list row**.\n *\n * The reading the session screen has always drawn, moved somewhere it answers a\n * question the session screen cannot: *which* of these thirty sessions is\n * bloating. That is also why it is a ring and not a number — across a list, a\n * ring is read at a glance and `71%` has to be read one row at a time.\n *\n * **Absent draws nothing, and absent is not zero.** A promptless session, a\n * parked one from before the field existed, or an engine that reports no window\n * has no reading; an empty ring would claim its context is empty rather than\n * unknown. The title carries the exact numbers for anyone who wants them.\n *\n * Shared rather than spelled per client: the dashboard's row and the VS Code\n * sidebar's card both draw it, and two copies would be two sets of thresholds\n * the day one of them is nudged.\n */\nexport function ContextRing({\n usage,\n size = 11,\n className,\n}: {\n usage: ContextReading | undefined\n size?: number\n className?: string\n}) {\n if (usage === undefined) return null\n return (\n <span\n title={`Context ${usage.percentage.toFixed(0)}% · ${formatTokens(usage.totalTokens)} / ${formatTokens(usage.maxTokens)}`}\n className={cn('shrink-0 leading-none', meterColorClass(usage.percentage), className)}>\n <ProgressRing value={usage.percentage} size={size} strokeWidth={2} />\n </span>\n )\n}\n","import { cn } from '../../lib/utils.ts'\n\n/**\n * The engine's own mark, monochrome.\n *\n * Inlined rather than depended on: the React package these come from\n * (`@lobehub/icons`) pulls antd, `@lobehub/ui` and a second copy of\n * lucide-react — an absurd amount of bundle for a few glyphs in a sidebar.\n * These are paths from `@lobehub/icons-static-svg` (MIT, © LobeHub), already\n * `fill=\"currentColor\"` single-path marks, so they take the row's colour like\n * any other glyph and need no light/dark variants.\n *\n * The marks are trademarks of their owners (Anthropic, OpenAI, Google,\n * DeepSeek, Moonshot), used here only to identify which engine a session runs\n * on.\n *\n * Codex is drawn as OpenAI: the session's engine *is* OpenAI's, and OpenAI's\n * mark is the one a person recognises at 12px.\n */\nconst PATHS: Record<string, { title: string; d: string }> = {\n claude: {\n title: 'Claude',\n d: 'M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z',\n },\n codex: {\n title: 'OpenAI',\n d: 'M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z',\n },\n gemini: {\n title: 'Gemini',\n d: 'M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z',\n },\n deepseek: {\n title: 'DeepSeek',\n d: 'M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z',\n },\n moonshot: {\n title: 'Moonshot',\n d: 'M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z',\n },\n}\n\n/**\n * Which mark a session wears.\n *\n * The two first-party engines are named directly. A `provider` session is\n * whatever the host wired up, so its *model* is the only thing that says whose\n * it is — sniffed loosely on purpose (`gemini-2.5-pro`, `deepseek-chat`,\n * `kimi-k2`), and falling through to no mark rather than guessing wrong.\n */\nexport function engineMark(engine: string, model?: string): string | undefined {\n if (engine === 'claude') return 'claude'\n if (engine === 'codex') return 'codex'\n const id = model?.toLowerCase() ?? ''\n if (!id) return undefined\n if (id.includes('gemini')) return 'gemini'\n if (id.includes('deepseek')) return 'deepseek'\n if (id.includes('moonshot') || id.includes('kimi')) return 'moonshot'\n if (id.includes('claude')) return 'claude'\n if (id.startsWith('gpt') || id.startsWith('o1') || id.startsWith('o3') || id.includes('openai')) {\n return 'codex'\n }\n return undefined\n}\n\n/**\n * Which engines wear their vendor's colour, and **how far it reaches**.\n *\n * `MARK` is the glyph, `TEXT` is the model name beside it. They are the same for\n * Anthropic and deliberately not for OpenAI, because the two brands differ in\n * kind rather than in hue: coral is an accent, and OpenAI's guidelines forbid\n * adding colour to the mark at all, so theirs is pure white/black. At full\n * contrast that is right on a 12px glyph and wrong on an 11px label — a\n * pure-white model name is *brighter than the session title above it*, which\n * inverts the row's whole hierarchy to say something the mark has already said.\n * So OpenAI's name keeps the muted line it always had, which is also the most\n * literal reading of \"don't add any colors\".\n *\n * Anything absent falls through to muted on both counts.\n */\nconst VENDOR_MARK: Record<string, string> = {\n claude: 'text-vendor-claude',\n codex: 'text-vendor-openai',\n}\nconst VENDOR_TEXT: Record<string, string> = {\n claude: 'text-vendor-claude',\n}\n\n/** The colour a session's engine mark wears. Pass it to `EngineIcon`'s\n * `className` rather than to a parent: the svg carries its own `text-fg-3`,\n * and only a class on the element itself is merged over it. */\nexport function vendorMarkClass(engine: string, model?: string): string {\n return VENDOR_MARK[engineMark(engine, model) ?? ''] ?? 'text-fg-3'\n}\n\n/** The colour the model *name* wears beside that mark — muted unless the vendor\n * is one whose accent survives at label size. See `VENDOR_TEXT`.\n *\n * The fallback is `text-fg-3` rather than the metadata line's own `text-fg-4`,\n * so an unbranded model still sits one step above the project and gateway\n * beside it — the order the spec lists them in, held without a colour. */\nexport function vendorTextClass(engine: string, model?: string): string {\n return VENDOR_TEXT[engineMark(engine, model) ?? ''] ?? 'text-fg-3'\n}\n\nexport function EngineIcon({\n engine,\n model,\n className,\n}: {\n engine: string\n model?: string\n className?: string\n}) {\n const mark = PATHS[engineMark(engine, model) ?? '']\n // An engine this build has never heard of still gets a slot, so the second\n // line doesn't reflow around a missing glyph.\n if (!mark) {\n return (\n <span aria-hidden title={engine} className={cn('shrink-0 text-fg-3', className)}>\n ·\n </span>\n )\n }\n return (\n <svg\n viewBox='0 0 24 24'\n fill='currentColor'\n fillRule='evenodd'\n role='img'\n aria-label={mark.title}\n className={cn('size-3 shrink-0 text-fg-3', className)}>\n <title>{mark.title}</title>\n <path d={mark.d} />\n </svg>\n )\n}\n","import type { ProjectIcon as ProjectIconSpec } from '@workerdeck/protocol'\nimport {\n Anchor, Atom, Beaker, Bike, Binary, Blocks, Bot, Box, Brain, Briefcase, Brush, Bug,\n Building2, Cable, Camera, Car, Cat, CircuitBoard, Cloud, Code, Coffee, Cog, Compass,\n Container, Cpu, CreditCard, Database, Diamond, Dog, Feather, Film, Flame, FlaskConical,\n Folder, Gamepad2, Gauge, Gem, Ghost, Gift, GitBranch, Globe, GraduationCap, Hammer,\n Hexagon, House, Image, Key, Landmark, Laptop, Layers, Leaf, Library, Lightbulb, Lock,\n Mail, Map, MessageCircle, Mic, Monitor, Moon, Mountain, Music, Network, Newspaper,\n Notebook, Orbit, Package, Palette, PenTool, Phone, PiggyBank, Plane, Plug, Puzzle,\n Radar, Radio, Receipt, Rocket, Ruler, Satellite, Scale, Scissors, Server, Shield, Ship,\n ShoppingCart, Signal, Smartphone, Sparkles, Sprout, Square, Star, Sun, Table, Tag,\n Target, Telescope, Terminal, TestTube, TreePine, Trophy, Truck, Umbrella, Users, Wallet,\n Wand, Waves, Webhook, Wifi, Wrench, Zap,\n} from 'lucide-react'\nimport type { ComponentType } from 'react'\nimport { cn } from '../../lib/utils.ts'\n\n/**\n * A project's icon — the render side of protocol's `ProjectIcon`, for a list\n * row or a group header.\n *\n * **The glyph arm is a curated set, and that is the design rather than a\n * shortcut.** The gateway validates a glyph name by *shape* only (lucide's\n * lowercase-kebab convention) and explicitly declines to grow an icon catalog,\n * so the contract already says a client must fall back on a name it does not\n * know.\n *\n * The alternative was measured rather than argued, against the VS Code\n * sidebar's own bundle: `sidebar.js` is **31 KB** with one glyph, **77 KB**\n * with the 110 below (~418 bytes each), and **927 KB** with a\n * `import * as` over lucide's ~1,600 — which is what it costs, because a\n * namespace import defeats tree-shaking by construction. (`DynamicIcon` is the\n * third option and worse in a different currency: ~1,600 chunk files shipped\n * inside the `.vsix`.) Twelve times the bundle to render a name nobody has\n * declared yet is not a trade worth making; 46 KB to cover the names people\n * actually use is.\n *\n * The set is chosen to cover what people call a project; an unlisted-but-valid\n * name draws {@link Folder}, which is what the row drew before this feature\n * existed. **Grow it freely** — each addition is ~400 bytes and the fallback\n * means a missing one is a shrug, not a hole.\n *\n * **The image arm draws nothing until its bytes arrive**, and takes them as a\n * resolved `src` rather than fetching: the wire carries only an address\n * (media type, size, content hash), and *who* fetches it differs per client —\n * a VS Code webview cannot reach a gateway at all and must be handed a data\n * URL by its extension host, while a browser can fetch and `createObjectURL`.\n * A component that fetched would be wrong on one of them. Absent `src` renders\n * nothing rather than a placeholder box: an icon is decoration beside a name\n * that is already there, and a box that becomes a picture a beat later is more\n * movement than the picture is worth.\n *\n * One thing an image icon cannot do, and a glyph can: **take the row's\n * colour.** An `<img>`-embedded SVG is its own document, so `currentColor` does\n * not reach it and its own `prefers-color-scheme` resolves against the *OS*\n * rather than the host's theme (measured: it does resolve, which is the\n * surprise — so a monochrome mark tuned for both schemes is right on a dark OS\n * in a dark editor and wrong on a light OS in a dark one). A repo that wants a\n * mark that always matches its row should declare a glyph; one that wants its\n * brand should declare the image and accept that it is a picture.\n */\nconst GLYPHS: Record<string, ComponentType<{ className?: string }>> = {\n anchor: Anchor, atom: Atom, beaker: Beaker, bike: Bike, binary: Binary, blocks: Blocks,\n bot: Bot, box: Box, brain: Brain, briefcase: Briefcase, brush: Brush, bug: Bug,\n 'building-2': Building2, cable: Cable, camera: Camera, car: Car, cat: Cat,\n 'circuit-board': CircuitBoard, cloud: Cloud, code: Code, coffee: Coffee, cog: Cog,\n compass: Compass, container: Container, cpu: Cpu, 'credit-card': CreditCard,\n database: Database, diamond: Diamond, dog: Dog, feather: Feather, film: Film,\n flame: Flame, 'flask-conical': FlaskConical, folder: Folder, 'gamepad-2': Gamepad2,\n gauge: Gauge, gem: Gem, ghost: Ghost, gift: Gift, 'git-branch': GitBranch, globe: Globe,\n 'graduation-cap': GraduationCap, hammer: Hammer, hexagon: Hexagon, house: House,\n image: Image, key: Key, landmark: Landmark, laptop: Laptop, layers: Layers, leaf: Leaf,\n library: Library, lightbulb: Lightbulb, lock: Lock, mail: Mail, map: Map,\n 'message-circle': MessageCircle, mic: Mic, monitor: Monitor, moon: Moon,\n mountain: Mountain, music: Music, network: Network, newspaper: Newspaper,\n notebook: Notebook, orbit: Orbit, package: Package, palette: Palette, 'pen-tool': PenTool,\n phone: Phone, 'piggy-bank': PiggyBank, plane: Plane, plug: Plug, puzzle: Puzzle,\n radar: Radar, radio: Radio, receipt: Receipt, rocket: Rocket, ruler: Ruler,\n satellite: Satellite, scale: Scale, scissors: Scissors, server: Server, shield: Shield,\n ship: Ship, 'shopping-cart': ShoppingCart, signal: Signal, smartphone: Smartphone,\n sparkles: Sparkles, sprout: Sprout, square: Square, star: Star, sun: Sun, table: Table,\n tag: Tag, target: Target, telescope: Telescope, terminal: Terminal, 'test-tube': TestTube,\n 'tree-pine': TreePine, trophy: Trophy, truck: Truck, umbrella: Umbrella, users: Users,\n wallet: Wallet, wand: Wand, waves: Waves, webhook: Webhook, wifi: Wifi, wrench: Wrench,\n zap: Zap,\n}\n\nexport function ProjectIcon({\n icon,\n src,\n name,\n className,\n}: {\n icon: ProjectIconSpec | undefined\n /** Resolved bytes for the `image` arm — an object URL or a data URL. The\n * caller fetches (see the note above); absent draws nothing. */\n src?: string\n /** The project's name, for the alt text. */\n name?: string\n className?: string\n}) {\n if (!icon) return null\n if (icon.type === 'image') {\n if (!src) return null\n return (\n <img\n src={src}\n alt=''\n aria-hidden\n title={name}\n // `object-contain` because a declared icon is whatever aspect the repo\n // checked in, and a squashed logo reads worse than a letterboxed one.\n className={cn('inline-block size-3 shrink-0 object-contain', className)}\n />\n )\n }\n const Glyph = GLYPHS[icon.name] ?? Folder\n return <Glyph className={cn('inline-block size-3 shrink-0', className)} />\n}\n","import {\n ArrowRight,\n Check,\n ChevronDown,\n ChevronRight,\n CircleAlert,\n Dot,\n PauseCircle,\n} from 'lucide-react'\nimport { isAgentRecord, subagentLabel } from '@workerdeck/protocol'\nimport type { SessionInfo, SubagentInfo } from '@workerdeck/protocol'\nimport { Spinner } from '../ui/Spinner.tsx'\nimport { cn } from '../../lib/utils.ts'\n\n/**\n * The work *under* a session row, and the one row shape every client renders it\n * through.\n *\n * This lived inside the VS Code webview first, which is exactly why the\n * dashboard had none of it. Nothing in here is extension-specific: a session's\n * sub-agents are a protocol fact, and a disclosure over them is a list\n * affordance. It sits in `packages/ui` so the sidebar, the dashboard and (in its\n * own idiom) the phone are annotating rows the same way rather than three ways.\n *\n * Today the only source is `SessionInfo.subagents`. The other — the CLI's own\n * **task checklist**, the to-do list it keeps for the current turn — is what the\n * design was drawn from, and it is not built: nothing on the wire carries it yet\n * (see `_docs/features/sub-agent-handling.md`, second thread, which opens with\n * \"check a capture\" rather than \"design a surface\"). When it arrives it is a\n * *source*, not a second row component: checklist when the session has one, its\n * sub-agents otherwise.\n *\n * That is also why `state` has a `pending` arm no sub-agent can produce. A\n * sub-agent record exists only once dispatched, so it is never queued; a to-do\n * is queued for most of its life, and dropping the state would mean widening the\n * union later — the shape is cheaper to state now than to retrofit.\n */\nexport type Step = {\n key: string\n label: string\n /** What one of these is called, for the disclosure's count. */\n noun: string\n /**\n * An **agent** has an identity and work of its own, so it is pressable and\n * wears the sub-agent colour. A **task** is something the model described with\n * no agent behind it (`isAgentRecord`), so it is inert: there is no frame to\n * open, and a row that offered one would show an empty screen.\n *\n * Two values and not a boolean because the checklist source this shape was\n * drawn for (see above) produces the second kind natively — a to-do is a task\n * in exactly this sense, and it will slot in here rather than widening\n * anything.\n */\n kind: 'agent' | 'task'\n state: 'done' | 'running' | 'pending' | 'failed'\n /** A trailing reading — a sub-agent's tool count. Absent draws nothing. */\n detail?: string\n title: string\n onSelect: () => void\n}\n\nexport function sessionSteps(\n info: SessionInfo,\n onSelectSubagent: (toolUseId: string) => void,\n): Step[] {\n // The label is protocol's `subagentLabel`, not a spelling of its own: the\n // dashboard and the phone render the same rows from the same records, and two\n // spellings would be two different answers to \"which agent is this\".\n return (info.subagents ?? []).map((sub) => ({\n key: sub.toolUseId,\n label: subagentLabel(sub),\n noun: 'agent',\n kind: isAgentRecord(sub) ? ('agent' as const) : ('task' as const),\n state: stepState(sub.status),\n detail: sub.toolCount > 0 ? String(sub.toolCount) : undefined,\n title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,\n onSelect: () => onSelectSubagent(sub.toolUseId),\n }))\n}\n\nfunction stepState(status: SubagentInfo['status']): Step['state'] {\n switch (status) {\n case 'running':\n return 'running'\n case 'failed':\n return 'failed'\n default:\n return 'done'\n }\n}\n\n/** How many of these are still going — the live half of the disclosure's count. */\nexport function runningSteps(steps: readonly Step[]): number {\n return steps.filter((s) => s.state === 'running').length\n}\n\n/**\n * The disclosure, which is also the reading: `3 agents` — or `2 of 3 agents`\n * while some have settled, because \"how many are still going\" is the live\n * question and a bare total answers it wrong the moment one finishes.\n *\n * Sub-agents are an annotation on a working row rather than a state of their own\n * (see `runningSubagents` in protocol's `session-list.ts`), so this never\n * competes with the row's status glyph: that still says what the *session* is\n * doing.\n */\nexport function StepToggle({\n expanded,\n running,\n total,\n noun,\n onToggle,\n}: {\n expanded: boolean\n running: number\n total: number\n noun: string\n onToggle: () => void\n}) {\n // Two spellings of one count. The **words** are the honest reading and go in\n // the tooltip and to a screen reader; the line itself gets the digits, because\n // this sits on the second line of a 280px row next to the folder and the age,\n // and `1 of 6 agents` truncated the folder name away to say something the row\n // could say in three characters.\n const label = running > 0 && running < total ? `${running}/${total}` : String(total)\n const words =\n running > 0 && running < total\n ? `${running} of ${total} ${noun}s running`\n : `${total} ${noun}${total === 1 ? '' : 's'}`\n const Chevron = expanded ? ChevronDown : ChevronRight\n return (\n <button\n type='button'\n aria-expanded={expanded}\n aria-label={`${expanded ? 'Hide' : 'Show'} ${words}`}\n title={`${expanded ? 'Hide' : 'Show'} ${words}`}\n onClick={(e) => {\n // The whole row is a button and this one does not mean \"select\" — the\n // same guard the row's overflow needs, and the reason a drag-select\n // inside the row does not toggle it.\n e.stopPropagation()\n onToggle()\n }}\n className={cn(\n 'flex shrink-0 items-center gap-0.5 rounded px-0.5 outline-none',\n 'hover:bg-surface-hover hover:text-fg-2',\n running > 0 ? 'text-info' : 'text-fg-4',\n )}>\n <Chevron className='size-3' />\n <span className='tabular-nums'>{label}</span>\n </button>\n )\n}\n\n/**\n * One step under its session. Pressing an **agent** hands the panel over to that\n * agent's own work — it is not a session and never becomes one, but it does now\n * have a surface (`SessionPanel.openSubagent`). A host that cannot frame one\n * falls back to revealing its `Task` row, which was this row's only meaning\n * before the takeover existed. A **task** is not pressable at all; see\n * {@link Step.kind}.\n *\n * Divided from the row's header and from each other by a rule rather than by\n * indentation: these are a list *inside* the row, and at 11px an indent is not\n * enough to say so. The rule is black at 25% so it darkens whatever the row is\n * filled with, selected or not, without needing a colour per state.\n */\nexport function StepRow({ step, onSelect }: { step: Step; onSelect: () => void }) {\n const agent = step.kind === 'agent'\n // Body colour by *kind*, state carried by the icon — the rule the transcript's\n // own `TaskRow` already follows, where the body is green and the marker holds\n // the beat. Green means sub-agent across this product (it is the one hue\n // nothing else on a session surface claims), so a list that spent blue on\n // \"running\" here would be saying something different from the transcript\n // about the same agent. Failure still outranks it: an alarm is not a category.\n const body = step.state === 'failed' ? 'text-danger' : agent ? 'text-success' : 'text-fg-4'\n const content = (\n <>\n <StepIcon state={step.state} kind={step.kind} />\n <span className='min-w-0 flex-1 truncate'>{step.label}</span>\n {/* The progress reading while it works, and what it cost when it is done.\n Zero draws nothing: `0 tools` beside a thinking agent reads as a stall,\n which is the same call `taskSummary` makes one surface over. */}\n {step.detail ? <span className='shrink-0 tabular-nums text-fg-4'>{step.detail}</span> : null}\n {agent ? <ArrowRight className='size-3 shrink-0 opacity-60' /> : null}\n </>\n )\n const shape =\n 'flex w-full items-center gap-2 border-t border-black/25 py-1 pl-3 pr-2 text-left text-label outline-none'\n\n // A task is not a button, in the markup and not merely in the styling: there\n // is nothing to press, and a disabled-looking button still announces itself as\n // one. It stops the click all the same — the whole session row is pressable\n // underneath, so falling through would open the session from a row that says\n // it does nothing.\n if (!agent) {\n return (\n <div title={step.title} onClick={(e) => e.stopPropagation()} className={cn(shape, body)}>\n {content}\n </div>\n )\n }\n return (\n <button\n type='button'\n title={step.title}\n onClick={(e) => {\n e.stopPropagation()\n onSelect()\n }}\n className={cn(shape, 'hover:bg-surface-hover', body)}>\n {content}\n </button>\n )\n}\n\nfunction StepIcon({ state, kind }: { state: Step['state']; kind: Step['kind'] }) {\n // A task that is neither running nor failed gets a neutral dot rather than a\n // tick: `done` is a claim about work, and nothing here did any.\n if (kind === 'task' && state !== 'running' && state !== 'failed') {\n return <Dot className='size-3 shrink-0' />\n }\n switch (state) {\n case 'running':\n return <Spinner className='size-3 shrink-0' />\n case 'failed':\n return <CircleAlert className='size-3 shrink-0' />\n case 'pending':\n return <PauseCircle className='size-3 shrink-0' />\n default:\n return <Check className='size-3 shrink-0' />\n }\n}\n","import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport {\n BellRing,\n CircleAlert,\n CircleSlash,\n Eraser,\n Layers,\n Moon,\n PauseCircle,\n Pencil,\n Search,\n SearchX,\n Trash2,\n X,\n} from 'lucide-react'\nimport {\n STATE_LABELS,\n STATE_ORDER,\n adaptersOf,\n clearFilters,\n filterRows,\n groupRows,\n hasFacetFilter,\n projectLabel,\n projectSubpath,\n projectsOf,\n sessionLabel,\n subsetSummary,\n} from '@workerdeck/protocol'\nimport type {\n GroupBy,\n SessionRow,\n SessionState,\n SortBy,\n ViewConfig,\n WorkspaceScope,\n} from '@workerdeck/protocol'\nimport { Button } from '../ui/Button.tsx'\nimport { Empty } from '../ui/Empty.tsx'\nimport { Input } from '../ui/Input.tsx'\nimport { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'\nimport { Spinner } from '../ui/Spinner.tsx'\nimport { ContextRing } from './ContextRing.tsx'\nimport { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'\nimport { ProjectIcon } from './ProjectIcon.tsx'\nimport { StepRow, StepToggle, runningSteps, sessionSteps } from './SessionSteps.tsx'\nimport { cn } from '../../lib/utils.ts'\nimport { formatCost, formatRelativeTime, friendlyModel } from '../../lib/format.ts'\n\n/**\n * A sessions list with the affordances a list of thirty needs: search, facets,\n * grouping, sorting, unread counts, and one honest line about what is hidden.\n *\n * The *rules* are `@workerdeck/protocol`'s (`filterRows`/`groupRows`/\n * `subsetSummary`), not this component's — the VS Code sidebar renders the same\n * model with workbench chrome, its activity-bar badge counts the same rows this\n * would show, and iOS mirrors them in Swift. What lives here is the styled\n * rendering of that model, so a host that wants the dashboard's look gets it\n * without reimplementing the model behind it.\n *\n * `SessionList` remains beside this for the plain case (a fixed set of rows, no\n * controls); this is what you reach for when the list is the screen.\n */\n\nexport interface SessionBrowserProps {\n rows: SessionRow[]\n config: ViewConfig\n onConfigChange: (config: ViewConfig) => void\n /** The host's own folders, if it has such a notion. Absent — a dashboard, a\n * phone — makes the scope filter genuinely inert rather than empty. */\n scope?: WorkspaceScope\n activeId?: string\n onSelect?: (row: SessionRow) => void\n onDelete?: (row: SessionRow) => void\n /**\n * Rename, from the row's pencil. Empty string restores the derived title. A\n * gateway edit (`PATCH /sessions/:id`), never a local override — every client\n * should see the same name. Omit to make titles read-only.\n *\n * A hover affordance rather than the extension's double-click-the-title,\n * because here a single click on the row navigates: the *first* click of a\n * double-click would have already left the page.\n */\n onRename?: (row: SessionRow, title: string) => void\n /**\n * Clear the conversation in place, from the row's eraser. Same session, empty\n * context — **not** a delete: the engine keeps the old conversation and it\n * stays resumable, which is why the row's copy never says \"deleted\".\n *\n * Rendered only where the session's own capability record allows it\n * (`capabilities.clearContext`, absent = false), so a row that cannot be\n * cleared has no eraser rather than one that errors. Omit the prop to have\n * none at all — a host with no way to send a session command has nothing to\n * hang it on (this is a WS command, not a REST route).\n */\n onClearContext?: (row: SessionRow) => void\n /** Open a session *at* one of its sub-agents, when the host can scroll a\n * transcript to a `Task` row. Absent, the sub-agent list still expands and a\n * step just opens its session — see `SessionRowItemProps`. */\n onSelectSubagent?: (row: SessionRow, toolUseId: string) => void\n /** Rendered when nothing at all exists (as opposed to nothing matching). */\n emptyState?: React.ReactNode\n /**\n * Whether the search + facet bar is shown. Defaults to `true` — a list that\n * *is* the screen shows its controls.\n *\n * A host with somewhere better to put the toggle (a view title bar) passes\n * `false` and owns the boolean itself, the way the VS Code extension does: the\n * key lives where the commands do. Two rules come with it, and they are why\n * this hides only the bar and nothing else — **closing the bar never clears\n * the filters**, and the subset line below it renders either way, so a list\n * filtered by a control you can't currently see still says so.\n */\n showControls?: boolean\n /**\n * Resolved project-icon bytes by content hash — `useProjectIcons`' output.\n *\n * Passed in rather than fetched here for the reason `ProjectIcon` states: the\n * wire carries an *address*, and who can fetch it differs per client. Absent,\n * or a hash not in it yet, simply draws no picture; the project's name is\n * already there.\n */\n projectIcons?: Record<string, string>\n className?: string\n}\n\n/**\n * How a list row is drawn, in one place, so `SidebarRow` in `web` matches this\n * exactly rather than approximating it — the dashboard's other three sidebars\n * are that component, and a sessions list that hovered differently from the\n * gateways list beside it would read as a different product.\n *\n * Two rules are load-bearing:\n *\n * - **Fill means hover, and only hover.** It stays on the row whether or not\n * the row is selected, because a selected row still has to answer the\n * pointer. Selection gets the gutter instead.\n * - **`ml-0` on the selected row is not cosmetic.** It hands the accent border\n * the 4px the margin was holding, so the text does not shift sideways as a\n * row becomes the selected one. The squared left corners are what let the bar\n * sit flush against the sidebar edge.\n */\nexport function rowShapeClass(active: boolean): string {\n return cn(\n 'px-2 py-1.5 hover:bg-row-hover',\n active ? 'mr-1 ml-0 rounded-r-md border-l-4 border-l-accent' : 'mx-1 rounded-md',\n )\n}\n\nexport function SessionBrowser({\n rows,\n config,\n onConfigChange,\n scope,\n activeId,\n onSelect,\n onDelete,\n onRename,\n onClearContext,\n onSelectSubagent,\n emptyState,\n showControls = true,\n projectIcons,\n className,\n}: SessionBrowserProps) {\n const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])\n const groups = useMemo(() => groupRows(visible, config), [visible, config])\n const subset = subsetSummary(config, scope, visible.length, rows.length)\n // Derived, not enumerated: a new engine or a new gateway needs no change here,\n // and a facet with one possible value is not a choice worth showing.\n const adapters = useMemo(() => adaptersOf(rows), [rows])\n const projects = useMemo(() => projectsOf(rows), [rows])\n const gateways = useMemo(() => {\n const seen = new Map<string, string>()\n for (const row of rows) seen.set(row.hostId, row.hostName)\n return [...seen].map(([id, name]) => ({ id, name }))\n }, [rows])\n\n const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })\n\n return (\n <div data-slot='session-browser' className={cn('flex flex-col gap-3', className)}>\n {/* One control per row, label left, input right — the shape VS Code uses\n for its own filter surfaces. A wrapping row of pill-shaped selects\n reflows into an unpredictable number of lines as facets appear and\n disappear; a column of labelled rows is the same height every time and\n reads at sidebar width, which is the only width this has. */}\n <div className={cn('flex flex-col gap-1.5 px-2', !showControls && 'hidden')}>\n <div className='relative'>\n <Search className='pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-fg-4' />\n <Input\n value={config.search}\n onChange={(e) => set({ search: e.target.value })}\n placeholder='Search sessions'\n aria-label='Search sessions'\n className='pl-8'\n />\n </div>\n <FilterRow label='State'>\n <FacetSelect\n label='State'\n value={config.states}\n options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}\n onChange={(states) => set({ states: states as SessionState[] })}\n />\n </FilterRow>\n {adapters.length > 1 ? (\n <FilterRow label='Engine'>\n <FacetSelect\n label='Engine'\n value={config.adapters}\n options={adapters.map((a) => ({ value: a, label: a }))}\n onChange={(adapters) => set({ adapters })}\n />\n </FilterRow>\n ) : null}\n {gateways.length > 1 ? (\n <FilterRow label='Gateway'>\n <FacetSelect\n label='Gateway'\n value={config.gateways}\n options={gateways.map((g) => ({ value: g.id, label: g.name }))}\n onChange={(gateways) => set({ gateways })}\n />\n </FilterRow>\n ) : null}\n {projects.length > 1 ? (\n <FilterRow label='Project'>\n <FacetSelect\n label='Project'\n value={config.projects ?? []}\n options={projects.map((p) => ({ value: p.key, label: p.label }))}\n onChange={(next) => set({ projects: next })}\n />\n </FilterRow>\n ) : null}\n <FilterRow label='Group'>\n <OneOfSelect\n label='Group'\n value={config.groupBy}\n options={[\n { value: 'none', label: 'No grouping' },\n { value: 'state', label: 'By state' },\n { value: 'adapter', label: 'By engine' },\n ...(projects.length > 1 ? [{ value: 'project' as const, label: 'By project' }] : []),\n ...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'By gateway' }] : []),\n ]}\n onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}\n />\n </FilterRow>\n <FilterRow label='Sort'>\n <OneOfSelect\n label='Sort'\n value={config.sortBy}\n options={[\n { value: 'recent', label: 'Recent' },\n { value: 'name', label: 'Name' },\n { value: 'state', label: 'State' },\n ...(projects.length > 1 ? [{ value: 'project' as const, label: 'Project' }] : []),\n ...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'Gateway' }] : []),\n ]}\n onChange={(sortBy) => set({ sortBy: sortBy as SortBy })}\n />\n </FilterRow>\n </div>\n\n {subset ? (\n <div className='flex items-center gap-2 px-3 text-label text-fg-4'>\n <span>\n {subset.shown} of {subset.total}\n {subset.causes.length ? ` · ${subset.causes.join(' · ')}` : null}\n </span>\n <button\n type='button'\n className='text-fg-3 underline underline-offset-2 hover:text-fg-1'\n onClick={() => onConfigChange(clearFilters(config))}>\n Show all\n </button>\n </div>\n ) : null}\n\n {rows.length === 0 ? (\n (emptyState ?? <Empty icon={<Layers />} title='No sessions yet' />)\n ) : visible.length === 0 ? (\n // Two different dead ends, two different ways out. \"Nothing matches\" is\n // a filter someone set; anything else is the state of the world — and\n // only the first has a button, because an action that does nothing is\n // worse than none.\n hasFacetFilter(config) ? (\n <Empty\n icon={<SearchX />}\n title='No matches'\n description='No session matches the current search and filters.'\n action='Clear filters'\n onAction={() => onConfigChange(clearFilters(config))}\n />\n ) : (\n <Empty icon={<Layers />} title='Nothing here' description='No session to show.' />\n )\n ) : (\n <div className='flex flex-col gap-4'>\n {groups.map((group) => (\n <div key={group.key} className='flex flex-col gap-1'>\n {config.groupBy !== 'none' && group.label ? (\n <div className='flex items-center gap-2 px-3 text-label font-medium text-fg-4'>\n {/* Only the project facet has a mark of its own, and a group\n IS one project root, so the first row is a fair source. */}\n {config.groupBy === 'project' ? (\n <ProjectIcon\n icon={group.rows[0]?.info.project?.icon}\n src={iconSrcOf(group.rows[0], projectIcons)}\n name={group.label}\n />\n ) : null}\n <span className='uppercase tracking-wide'>{group.label}</span>\n <span className='text-fg-4/70'>{group.rows.length}</span>\n </div>\n ) : null}\n {group.rows.map((row) => (\n <SessionRowItem\n key={`${row.hostId}:${row.info.id}`}\n row={row}\n active={row.info.id === activeId}\n showGateway={gateways.length > 1 && config.groupBy !== 'gateway'}\n showProject={config.groupBy !== 'project'}\n projectIcons={projectIcons}\n onSelect={onSelect}\n onDelete={onDelete}\n onRename={onRename}\n onClearContext={onClearContext}\n onSelectSubagent={onSelectSubagent}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n/** The bytes for a row's project icon, if it has an image one and the caller has\n * fetched it yet. Shared by the row and its group header so the two cannot draw\n * different pictures for one project. */\nfunction iconSrcOf(\n row: SessionRow | undefined,\n icons: Record<string, string> | undefined,\n): string | undefined {\n const icon = row?.info.project?.icon\n return icon?.type === 'image' ? icons?.[icon.hash] : undefined\n}\n\ninterface SessionRowItemProps {\n row: SessionRow\n active?: boolean\n showGateway?: boolean\n /** False when the list is already grouped by project — the header has said the\n * name, so the row must not spend its metadata line repeating it. What takes\n * the slot is the *sub-path inside the project* (`projectSubpath`), which is\n * the one thing the header cannot say and the only thing that tells two\n * sessions in the same repo apart; a session sitting at the project root has\n * nothing to add and the slot disappears entirely. The rule `showGateway`\n * follows one facet over. */\n showProject?: boolean\n projectIcons?: Record<string, string>\n onSelect?: (row: SessionRow) => void\n onDelete?: (row: SessionRow) => void\n onRename?: (row: SessionRow, title: string) => void\n /** See `SessionBrowserProps.onClearContext` — gated on the row's own\n * capability record, not on the engine name. */\n onClearContext?: (row: SessionRow) => void\n /** Open one of a session's sub-agents — which now means handing the session\n * panel over to that agent's own work (`SessionPanel.openSubagent`), and meant\n * \"scroll to its `Task` row\" before that surface existed. Both are honest; a\n * host picks what it can draw. Optional, and its absence is not a missing\n * feature: without it a step just opens the session, which is all a host with\n * neither can offer. Only *agent* steps ever call it — see `Step.kind`. */\n onSelectSubagent?: (row: SessionRow, toolUseId: string) => void\n}\n\nfunction SessionRowItem({\n row,\n active,\n showGateway,\n showProject = true,\n projectIcons,\n onSelect,\n onDelete,\n onRename,\n onClearContext,\n onSelectSubagent,\n}: SessionRowItemProps) {\n const { info } = row\n const [editing, setEditing] = useState(false)\n // Expansion is the row's own state and deliberately not persisted: a list that\n // reopened yesterday's work on every reload would be showing a settled tail\n // nobody asked for.\n const [expanded, setExpanded] = useState(false)\n\n // What it is and what it has spent, in one line — the same set the extension\n // shows, joined the same way, so the two lists read as one product.\n // protocol's own spelling, so this row, the group header above it and the\n // project facet cannot disagree about what a project is called. Under a\n // project group it is the sub-path instead — see `showProject` — and a session\n // at the root contributes nothing at all rather than a name already on screen.\n const project = showProject ? projectLabel(row) : projectSubpath(row)\n const projectIcon = showProject ? info.project?.icon : undefined\n const engine = info.engine ?? 'claude'\n // Everything after the model. The model itself is drawn separately because it\n // is the one segment that wears a colour — see `vendorTextClass` — and a\n // coloured run inside a joined string would have to be spliced back out.\n const details = [\n project,\n showGateway ? row.hostName : undefined,\n info.profile ? `@${info.profile}` : undefined,\n formatCost(info.totalCostUsd),\n ].filter(Boolean)\n const steps = sessionSteps(info, (toolUseId) =>\n onSelectSubagent ? onSelectSubagent(row, toolUseId) : onSelect?.(row),\n )\n // Where the icon goes: immediately before the project's own name, wherever\n // that landed in the joined line. Split rather than interleaved as nodes,\n // because everything here is one truncating mono run and a flex of pieces\n // would each shrink a little and leave several half-words.\n const cut = project === undefined ? -1 : details.indexOf(project)\n\n return (\n <div\n data-slot='session-row'\n data-active={active || undefined}\n // Selection lives on the whole row, not on the two text buttons inside\n // it: the age, the unread badge and the state glyph sit outside them, and\n // a row where a third of the surface silently does nothing is a row that\n // feels broken. The buttons stay — they are what makes the row reachable\n // by keyboard — but their activation now reaches this handler by\n // bubbling, so there is one code path and no double-fire. Anything that\n // is its own action (rename, close, the name editor) stops the event.\n onClick={() => !editing && onSelect?.(row)}\n className={cn(\n 'group flex cursor-pointer flex-col gap-0.5 text-left transition-colors',\n rowShapeClass(active === true),\n )}>\n {/* Line one: how it is doing, then what you scan the list by, then how\n long since it moved.\n\n State leads. It used to sit last, on the argument that the title is\n what you read and the state is what you glance at — but a glance wants\n a *column*, and a trailing glyph has no fixed x: it lands wherever the\n age and the ring leave it. Leading, every row's state stacks into one\n scannable strip, and the mark on line two lands under it in the same\n gutter. This is the VS Code sidebar's shape, and all three clients are\n on it now. */}\n <div className='flex items-center gap-1.5'>\n <Gutter>\n <SessionStatusIcon row={row} />\n </Gutter>\n {/* The editor replaces the link rather than sitting inside it — an\n input nested in a button is invalid, and disabling the button to\n protect the edit disables the field with it. */}\n {editing && onRename ? (\n <NameEditor\n initial={info.title ?? ''}\n onCommit={(title) => {\n setEditing(false)\n onRename(row, title)\n }}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <button\n type='button'\n className={cn(\n 'min-w-0 flex-1 truncate text-left text-body-sm outline-none',\n active ? 'font-medium text-fg-1' : 'text-fg-2',\n )}>\n {sessionLabel(info)}\n </button>\n )}\n {row.unseen > 0 ? (\n // Transcript rows since this session was last on screen — the same\n // unit the VS Code badge counts, because turns undercount badly.\n <span\n title={`${row.unseen} new`}\n className='shrink-0 rounded-full bg-accent px-1.5 text-label text-accent-fg'>\n {row.unseen}\n </span>\n ) : null}\n <span className='shrink-0 text-label text-fg-4'>\n {formatRelativeTime(info.lastActivityAt ?? info.createdAt)}\n </span>\n <ContextRing usage={info.contextUsage} />\n </div>\n\n {/* Line two: what it is — the engine's mark and its model in the vendor's\n own colour, then where it runs — with the actions at the far right,\n away from the title you are actually reading. */}\n <div className='flex items-center gap-1 text-label text-fg-4'>\n <Gutter>\n {/* The colour goes on the icon, not on this cell: the svg ships its\n own `text-fg-3` and only a class on the element itself merges over\n it. */}\n <EngineIcon\n engine={engine}\n model={info.model}\n className={vendorMarkClass(engine, info.model)}\n />\n </Gutter>\n <button\n type='button'\n tabIndex={-1}\n className='min-w-0 flex-1 truncate text-left font-mono outline-none'>\n <span className={vendorTextClass(engine, info.model)}>\n {/* The model id as a person says it — `claude-opus-5[1m]` is a wire\n value, and a list line has no room for a context-window suffix. */}\n {friendlyModel(info.model)}\n </span>\n {details.length > 0 ? ' · ' : ''}\n {cut < 0 ? (\n details.join(' · ')\n ) : (\n <>\n {details.slice(0, cut).map((part) => `${part} · `)}\n <ProjectIcon\n icon={projectIcon}\n src={iconSrcOf(row, projectIcons)}\n name={project}\n /* Nudged onto the text baseline: a 12px glyph box against an\n 11px line sits a hair proud without it. */\n className='mr-1 align-[-0.2em]'\n />\n {details.slice(cut).join(' · ')}\n </>\n )}\n </button>\n {steps.length > 0 ? (\n <StepToggle\n expanded={expanded}\n running={runningSteps(steps)}\n total={steps.length}\n noun={steps[0]!.noun}\n onToggle={() => setExpanded((v) => !v)}\n />\n ) : null}\n {onRename && !editing ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Rename session'\n className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'\n onClick={(e) => {\n e.stopPropagation()\n setEditing(true)\n }}>\n <Pencil className='size-3 text-fg-3' />\n </Button>\n ) : null}\n {onClearContext && info.capabilities?.clearContext ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Clear context'\n title='Clear the conversation — the session keeps running and the old conversation stays resumable'\n className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'\n onClick={(e) => {\n e.stopPropagation()\n onClearContext(row)\n }}>\n <Eraser className='size-3 text-fg-3' />\n </Button>\n ) : null}\n {onDelete ? (\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Close session'\n className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'\n onClick={(e) => {\n e.stopPropagation()\n onDelete(row)\n }}>\n <Trash2 className='size-3 text-fg-3' />\n </Button>\n ) : null}\n </div>\n {expanded ? steps.map((step) => <StepRow key={step.key} step={step} onSelect={step.onSelect} />) : null}\n </div>\n )\n}\n\n/**\n * The one 14px cell both lines hang their glyph in.\n *\n * A shared column rather than a leading character: the state and the engine mark\n * are different widths and a bare glyph would let the two text columns start at\n * different x. Centring inside a fixed cell is what makes them agree —\n * `packages/ui`'s terminal-gutter argument at row scale.\n */\nfunction Gutter({ children }: { children: ReactNode }) {\n return <span className='flex w-3.5 shrink-0 items-center justify-center'>{children}</span>\n}\n\n/**\n * State as one glyph — a ringing bell when it wants a human, a spinner while it\n * works, a moon when it is only sleeping. Replaces the text badge: in a sidebar\n * the word costs more room than it earns, and the states that matter are the two\n * you can recognise without reading.\n *\n * **It reads `row.state`, not `info.status`, and that distinction is the whole\n * point of the row model.** `sessionState` already folds in the arm this glyph\n * cannot see for itself: a *background* sub-agent outlives its turn by design, so\n * the turn ends, `status` comes to rest at `idle`, and the agent keeps working.\n * Reading the raw status drew a **moon on a row filed under the \"Working\"\n * header** — the list contradicting itself on one line, which is exactly what a\n * derived view model exists to prevent. The value was in scope and unread.\n *\n * The terminal statuses still come off `info.status`, because `ended` collapses\n * `failed` and `closed` into one bucket and those are worth telling apart here.\n */\nexport function SessionStatusIcon({ row }: { row: SessionRow }) {\n const { info } = row\n if (row.state === 'attention') {\n return <BellRing className='size-3.5 shrink-0 animate-pulse text-warning' />\n }\n if (row.state === 'working') {\n return <Spinner className='size-3.5 shrink-0 text-info' />\n }\n switch (info.status) {\n case 'failed':\n return <CircleAlert className='size-3.5 shrink-0 text-danger' />\n case 'closed':\n return <CircleSlash className='size-3.5 shrink-0 text-fg-4' />\n case 'parked':\n return <PauseCircle className='size-3.5 shrink-0 text-fg-3' />\n default:\n return <Moon className='size-3.5 shrink-0 text-fg-4' />\n }\n}\n\n/**\n * Inline rename. Enter commits, Escape cancels, blur commits — but only a blur\n * that is still inside this document: switching windows must not silently commit,\n * nor kill the editor in the frame it opened.\n */\nfunction NameEditor({\n initial,\n onCommit,\n onCancel,\n}: {\n initial: string\n onCommit: (title: string) => void\n onCancel: () => void\n}) {\n const [value, setValue] = useState(initial)\n const ref = useRef<HTMLInputElement>(null)\n useEffect(() => {\n ref.current?.select()\n }, [])\n return (\n <input\n ref={ref}\n value={value}\n autoFocus\n aria-label='Session name'\n // The row is a button; a click in here must not select the session.\n onClick={(e) => e.stopPropagation()}\n onChange={(e) => setValue(e.target.value)}\n onBlur={() => (document.hasFocus() ? onCommit(value.trim()) : undefined)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') onCommit(value.trim())\n else if (e.key === 'Escape') onCancel()\n e.stopPropagation()\n }}\n className='-my-0.5 min-w-0 flex-1 rounded-sm border border-ring bg-bg px-1 py-0.5 text-body-sm font-medium text-fg-1 outline-none'\n />\n )\n}\n\n/** A multi-select facet. Empty = no filter, which is why the trigger reads the\n * facet's name rather than \"All\": nothing is being excluded. */\n/**\n * One labelled control row. The label column is fixed so every control starts\n * on the same x — the thing that makes a stack of them read as a form rather\n * than as five unrelated widgets.\n */\nfunction FilterRow({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className='flex items-center gap-2'>\n <span aria-hidden className='w-14 shrink-0 truncate text-label text-fg-3'>\n {label}\n </span>\n <div className='min-w-0 flex-1'>{children}</div>\n </div>\n )\n}\n\nfunction FacetSelect({\n label,\n value,\n options,\n onChange,\n}: {\n label: string\n value: string[]\n options: { value: string; label: string }[]\n onChange: (value: string[]) => void\n}) {\n return (\n <div className='flex items-center gap-1'>\n <Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>\n <SelectTrigger aria-label={label} className='min-w-0 flex-1'>\n <SelectValue>\n {/* \"All\" rather than the label, which the row already carries. */}\n {value.length === 0\n ? 'All'\n : value.length === 1\n ? (options.find((o) => o.value === value[0])?.label ?? 'All')\n : `${value.length} selected`}\n </SelectValue>\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n <SelectItemText>{option.label}</SelectItemText>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {value.length > 0 ? (\n <Button variant='ghost' size='icon-sm' aria-label={`Clear ${label}`} onClick={() => onChange([])}>\n <X className='size-3 text-fg-4' />\n </Button>\n ) : null}\n </div>\n )\n}\n\nfunction OneOfSelect({\n label,\n value,\n options,\n onChange,\n}: {\n label: string\n value: string\n options: readonly { value: string; label: string }[]\n onChange: (value: string) => void\n}) {\n return (\n <Select value={value} onValueChange={(v) => onChange(v as string)}>\n <SelectTrigger aria-label={label} className='w-full min-w-0'>\n <SelectValue>{options.find((o) => o.value === value)?.label ?? label}</SelectValue>\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n <SelectItemText>{option.label}</SelectItemText>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n"],"mappings":";;;;;;;;;AAGA,SAAgB,KAAK,EAAE,WAAW,GAAG,SAAyC;AAC5E,QACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,8CAA8C,UAAU;EACtE,GAAI;EACJ,CAAA;;AAIN,SAAgB,WAAW,EAAE,WAAW,GAAG,SAAyC;AAClF,QACE,oBAAC,OAAD;EACE,WAAW,GAAG,2DAA2D,UAAU;EACnF,GAAI;EACJ,CAAA;;AAIN,SAAgB,UAAU,EAAE,WAAW,GAAG,SAA6C;AACrF,QAAO,oBAAC,MAAD;EAAI,WAAW,GAAG,0CAA0C,UAAU;EAAE,GAAI;EAAS,CAAA;;AAG9F,SAAgB,YAAY,EAAE,WAAW,GAAG,SAAyC;AACnF,QAAO,oBAAC,OAAD;EAAK,WAAW,GAAG,aAAa,UAAU;EAAE,GAAI;EAAS,CAAA;;;;ACxBlE,MAAa,WAAW,YAGrB,EAAE,WAAW,GAAG,SAAS,QAC1B,oBAAC,YAAD;CACO;CACL,aAAU;CACV,WAAW,GACT,+FACA,wDACA,wGACA,oDACA,UACD;CACD,GAAI;CACJ,CAAA,CACF;AACF,SAAS,cAAc;;;AChBvB,MAAa,cAAcA,cAAqB;AAChD,MAAa,qBAAqBA,cAAqB;AACvD,MAAa,mBAAmBA,cAAqB;AAErD,MAAa,sBAA2E,EACtF,WACA,UACA,GAAG,YAEH,qBAACA,cAAqB,QAAtB,EAAA,UAAA,CACE,oBAACA,cAAqB,UAAtB,EACE,WAAW,GACT,sDACA,+CACA,4DACD,EACD,CAAA,EACF,oBAACA,cAAqB,OAAtB;CACE,aAAU;CACV,WAAW,GACT,iGACA,oFACA,2DACA,8DACA,0DACA,UACD;CACD,GAAI;CACH;CAC0B,CAAA,CACD,EAAA,CAAA;AAGhC,MAAa,oBAAyE,EACpF,WACA,GAAG,YAEH,oBAACA,cAAqB,OAAtB;CACE,WAAW,GAAG,0CAA0C,UAAU;CAClE,GAAI;CACJ,CAAA;AAGJ,MAAa,0BAAqF,EAChG,WACA,GAAG,YAEH,oBAACA,cAAqB,aAAtB;CACE,WAAW,GAAG,6CAA6C,UAAU;CACrE,GAAI;CACJ,CAAA;;;;ACjDJ,SAAgB,UAAU;AACxB,QACE,oBAACC,WAAD;EACE,UAAS;EACT,cAAc,EACZ,OAAO;GACL,YAAY;GACZ,OAAO;GACP,QAAQ;GACR,WAAW;GACX,YAAY;GACZ,UAAU;GACX,EACF;EACD,CAAA;;;;;;;;;;;;;;;;;;;;ACAN,SAAgB,MAAM,EACpB,MACA,OACA,aACA,QACA,YAOC;AACD,QACE,qBAAC,OAAD;EAAK,WAAU;YAAf;GACE,oBAAC,OAAD;IAAK,WAAU;cAAuC;IAAW,CAAA;GACjE,oBAAC,KAAD;IAAG,WAAU;cAAsC;IAAU,CAAA;GAC5D,cACC,oBAAC,KAAD;IAAG,WAAU;cACV;IACC,CAAA,GACF;GACH,UAAU,WACT,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAK,WAAU;IAAO,SAAS;cAC3D;IACM,CAAA,GACP;GACA;;;;AAKV,SAAgB,SAAS,EAAE,YAAqC;AAC9D,QACE,oBAAC,QAAD;EAAM,WAAU;EAAyD;EAAgB,CAAA;;;;;ACZ7F,SAAgB,gBAAgB,UAA8B;AAC5D,KAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAO,SAAS,OAAO,QAAQ,IAAI,SAAS,UAAU,IAAI,KAAK,MAAM,KAAK,GAAG;;;AAc/E,SAAgB,kBAAkB,UAAqB,SAAgC;AACrF,QAAO,SAAS,QACb,QAA4B,IAAI,SAAS,UAAU,IAAI,YAAY,QACrE;;;;;;;;;;;;;;;;;;;;;;AC3BH,MAAM,gBAA8C;CAClD,MAAM;CACN,SAAS;CACT,OAAO;CACR;;;AAoBD,SAAS,QAAQ,EACf,MACA,SACA,OACA,YAMC;AACD,KAAI,CAAC,QAAS,QAAO,oBAAC,KAAD;EAAW;EAAO;EAAe,CAAA;AACtD,QACE,oBAAC,UAAD;EAAQ,MAAK;EAAS,WAAU;EAAe,SAAS;EAAS,OAAO;YACtE,oBAAC,KAAD;GAAW;GAAO;GAAe,CAAA;EAC1B,CAAA;;AAIb,SAAgB,mBAAmB,EACjC,OACA,YACA,YACA,cACA,eACA,eAC0B;CAC1B,MAAM,eAAe,mBAAmB;EAAE,QAAQ,MAAM;EAAQ;EAAY,CAAC;CAC7E,MAAM,QAAQ,MAAM;CACpB,MAAM,SAAS,eAAe,cAAc,MAAM,WAAW;CAC7D,MAAM,QAAQ,WAAW;EAAE,OAAO,MAAM;EAAO,QAAQ,MAAM,UAAU,EAAE;EAAE,CAAC;CAE5E,MAAM,QAA2B,CAC/B,oBAAC,SAAD;EAEE,MAAM,cAAc,aAAa;EACjC,SAAS;EACT,OAAM;YACL,aAAa;EACN,EALJ,SAKI,CACX;AAED,KAAI,MACF,OAAM,KACJ,qBAAC,SAAD;EAEE,MAAM,cAAc,gBAAgB,MAAM;EAC1C,SAAS;EACT,OAAM;YAJR;GAKG,KAAK,MAAM,MAAM,WAAW;GAAC;GAAQ,aAAa,MAAM,YAAY;GAAC;GAC9D;IALJ,UAKI,CACX;AAGH,KAAI,OACF,OAAM,KACJ,qBAAC,SAAD;EAEE,MAAM,cAAc,cAAc,OAAO,KAAK,YAAY;EAC1D,SAAS;EACT,OAAM;YAJR;GAKG,KAAK,MAAM,OAAO,KAAK,eAAe,EAAE;GAAC;GAAG,YAAY,OAAO,IAAI;GAC5D;IALJ,QAKI,CACX;AAKH,KAAI,MAAM,eAAe,EACvB,OAAM,KACJ,oBAAC,KAAD;EAAgB,MAAK;YAClB,WAAW,MAAM,aAAa;EAC3B,EAFG,OAEH,CACP;AAGH,KAAI,MACF,OAAM,KACJ,oBAAC,KAAD;EAAiB,MAAK;YACnB;EACG,EAFG,QAEH,CACP;AAEH,KAAI,MAAM,kBAAkB,MAAM,mBAAmB,UACnD,OAAM,KACJ,oBAAC,KAAD;EAAgB,MAAK;YAClB,MAAM;EACH,EAFG,OAEH,CACP;AAGH,QACE,oBAAC,KAAD;EAAK,aAAU;EAAc,MAAK;YAC/B,MAAM,KAAK,MAAM,UAChB,qBAAC,QAAD,EAAA,UAAA,CACG,QAAQ,IAAI,oBAAC,KAAD;GAAK,MAAK;aAAQ;GAAS,CAAA,GAAG,MAC1C,KACI,EAAA,EAHI,MAGJ,CACP;EACE,CAAA;;;;AC7IV,SAAgB,gBAAgB,EAAE,SAAS,QAAQ,UAAU,YAAkC;CAC7F,MAAM,OAAO,YAAY,QAAQ;AACjC,QACE,qBAAC,OAAD;EACE,aAAU;EACV,eAAa,UAAU,KAAA;EACvB,WAAW,GACT,qHACA,SAAS,6BAA6B,yBACvC;YANH,CAOE,qBAAC,UAAD;GACE,MAAK;GACL,eAAe,WAAW,QAAQ,GAAG;GACrC,WAAU;aAHZ,CAIE,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,QAAD;KAAM,WAAU;eACb,QAAQ,SAAS,QAAQ,GAAG,MAAM,GAAG,EAAE;KACnC,CAAA,EACP,oBAAC,OAAD;KAAO,SAAS,KAAK;KAAS,KAAA;KAAI,WAAU;eACzC,KAAK;KACA,CAAA,CACJ;OACN,qBAAC,OAAD;IAAK,WAAU;cAAf;KACE,oBAAC,QAAD;MAAM,WAAU;gBAAY,QAAQ;MAAW,CAAA;KAC9C,QAAQ,UAAU,qBAAC,QAAD;MAAM,WAAU;gBAAhB,CAA2B,KAAE,QAAQ,QAAe;UAAG;KAC1E,oBAAC,QAAD;MAAM,WAAU;gBAAY,WAAW,QAAQ,aAAa;MAAQ,CAAA;KACpE,oBAAC,QAAD;MAAM,WAAU;gBAAY,mBAAmB,QAAQ,kBAAkB,QAAQ,UAAU;MAAQ,CAAA;KAC/F;MACC;MACR,WACC,oBAAC,QAAD;GACE,SAAQ;GACR,MAAK;GACL,cAAW;GACX,WAAU;GACV,eAAe,SAAS,QAAQ,GAAG;aACnC,oBAAC,QAAD,EAAQ,WAAU,sBAAuB,CAAA;GAClC,CAAA,GACP,KACA;;;AAaV,SAAgB,YAAY,EAC1B,UACA,UACA,UACA,UACA,YAAY,oBACZ,aACmB;AACnB,QACE,oBAAC,OAAD;EAAK,aAAU;EAAe,WAAW,GAAG,uBAAuB,UAAU;YAC1E,SAAS,WAAW,IACnB,oBAAC,OAAD;GAAK,WAAU;aAAkD;GAAgB,CAAA,GAEjF,SAAS,KAAK,YACZ,oBAAC,iBAAD;GAEW;GACT,QAAQ,QAAQ,OAAO;GACb;GACA;GACV,EALK,QAAQ,GAKb,CACF;EAEA,CAAA;;;;;;;;;;;;;;;;;;;;;ACnEV,SAAgB,YAAY,EAC1B,OACA,OAAO,IACP,aAKC;AACD,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,QACE,oBAAC,QAAD;EACE,OAAO,WAAW,MAAM,WAAW,QAAQ,EAAE,CAAC,MAAM,aAAa,MAAM,YAAY,CAAC,KAAK,aAAa,MAAM,UAAU;EACtH,WAAW,GAAG,yBAAyB,gBAAgB,MAAM,WAAW,EAAE,UAAU;YACpF,oBAAC,cAAD;GAAc,OAAO,MAAM;GAAkB;GAAM,aAAa;GAAK,CAAA;EAChE,CAAA;;;;;;;;;;;;;;;;;;;;;ACnBX,MAAM,QAAsD;CAC1D,QAAQ;EACN,OAAO;EACP,GAAG;EACJ;CACD,OAAO;EACL,OAAO;EACP,GAAG;EACJ;CACD,QAAQ;EACN,OAAO;EACP,GAAG;EACJ;CACD,UAAU;EACR,OAAO;EACP,GAAG;EACJ;CACD,UAAU;EACR,OAAO;EACP,GAAG;EACJ;CACF;;;;;;;;;AAUD,SAAgB,WAAW,QAAgB,OAAoC;AAC7E,KAAI,WAAW,SAAU,QAAO;AAChC,KAAI,WAAW,QAAS,QAAO;CAC/B,MAAM,KAAK,OAAO,aAAa,IAAI;AACnC,KAAI,CAAC,GAAI,QAAO,KAAA;AAChB,KAAI,GAAG,SAAS,SAAS,CAAE,QAAO;AAClC,KAAI,GAAG,SAAS,WAAW,CAAE,QAAO;AACpC,KAAI,GAAG,SAAS,WAAW,IAAI,GAAG,SAAS,OAAO,CAAE,QAAO;AAC3D,KAAI,GAAG,SAAS,SAAS,CAAE,QAAO;AAClC,KAAI,GAAG,WAAW,MAAM,IAAI,GAAG,WAAW,KAAK,IAAI,GAAG,WAAW,KAAK,IAAI,GAAG,SAAS,SAAS,CAC7F,QAAO;;;;;;;;;;;;;;;;;AAoBX,MAAM,cAAsC;CAC1C,QAAQ;CACR,OAAO;CACR;AACD,MAAM,cAAsC,EAC1C,QAAQ,sBACT;;;;AAKD,SAAgB,gBAAgB,QAAgB,OAAwB;AACtE,QAAO,YAAY,WAAW,QAAQ,MAAM,IAAI,OAAO;;;;;;;;AASzD,SAAgB,gBAAgB,QAAgB,OAAwB;AACtE,QAAO,YAAY,WAAW,QAAQ,MAAM,IAAI,OAAO;;AAGzD,SAAgB,WAAW,EACzB,QACA,OACA,aAKC;CACD,MAAM,OAAO,MAAM,WAAW,QAAQ,MAAM,IAAI;AAGhD,KAAI,CAAC,KACH,QACE,oBAAC,QAAD;EAAM,eAAA;EAAY,OAAO;EAAQ,WAAW,GAAG,sBAAsB,UAAU;YAAE;EAE1E,CAAA;AAGX,QACE,qBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,UAAS;EACT,MAAK;EACL,cAAY,KAAK;EACjB,WAAW,GAAG,6BAA6B,UAAU;YANvD,CAOE,oBAAC,SAAD,EAAA,UAAQ,KAAK,OAAc,CAAA,EAC3B,oBAAC,QAAD,EAAM,GAAG,KAAK,GAAK,CAAA,CACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzEV,MAAM,SAAgE;CACpE,QAAQ;CAAQ,MAAM;CAAM,QAAQ;CAAQ,MAAM;CAAM,QAAQ;CAAQ,QAAQ;CAChF,KAAK;CAAK,KAAK;CAAK,OAAO;CAAO,WAAW;CAAW,OAAO;CAAO,KAAK;CAC3E,cAAc;CAAW,OAAO;CAAO,QAAQ;CAAQ,KAAK;CAAK,KAAK;CACtE,iBAAiB;CAAc,OAAO;CAAO,MAAM;CAAM,QAAQ;CAAQ,KAAK;CAC9E,SAAS;CAAS,WAAW;CAAW,KAAK;CAAK,eAAe;CACjE,UAAU;CAAU,SAAS;CAAS,KAAK;CAAK,SAAS;CAAS,MAAM;CACxE,OAAO;CAAO,iBAAiB;CAAc,QAAQ;CAAQ,aAAa;CAC1E,OAAO;CAAO,KAAK;CAAK,OAAO;CAAO,MAAM;CAAM,cAAc;CAAW,OAAO;CAClF,kBAAkB;CAAe,QAAQ;CAAQ,SAAS;CAAS,OAAO;CAC1E,OAAO;CAAO,KAAK;CAAK,UAAU;CAAU,QAAQ;CAAQ,QAAQ;CAAQ,MAAM;CAClF,SAAS;CAAS,WAAW;CAAW,MAAM;CAAM,MAAM;CAAM,KAAKC;CACrE,kBAAkB;CAAe,KAAK;CAAK,SAAS;CAAS,MAAM;CACnE,UAAU;CAAU,OAAO;CAAO,SAAS;CAAS,WAAW;CAC/D,UAAU;CAAU,OAAO;CAAO,SAAS;CAAS,SAAS;CAAS,YAAY;CAClF,OAAO;CAAO,cAAc;CAAW,OAAO;CAAO,MAAM;CAAM,QAAQ;CACzE,OAAO;CAAO,OAAO;CAAO,SAAS;CAAS,QAAQ;CAAQ,OAAO;CACrE,WAAW;CAAW,OAAO;CAAO,UAAU;CAAU,QAAQ;CAAQ,QAAQ;CAChF,MAAM;CAAM,iBAAiB;CAAc,QAAQ;CAAQ,YAAY;CACvE,UAAU;CAAU,QAAQ;CAAQ,QAAQ;CAAQ,MAAM;CAAM,KAAK;CAAK,OAAO;CACjF,KAAK;CAAK,QAAQ;CAAQ,WAAW;CAAW,UAAU;CAAU,aAAa;CACjF,aAAa;CAAU,QAAQ;CAAQ,OAAO;CAAO,UAAU;CAAU,OAAO;CAChF,QAAQ;CAAQ,MAAM;CAAM,OAAO;CAAO,SAAS;CAAS,MAAM;CAAM,QAAQ;CAChF,KAAK;CACN;AAED,SAAgB,YAAY,EAC1B,MACA,KACA,MACA,aASC;AACD,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,KAAK,SAAS,SAAS;AACzB,MAAI,CAAC,IAAK,QAAO;AACjB,SACE,oBAAC,OAAD;GACO;GACL,KAAI;GACJ,eAAA;GACA,OAAO;GAGP,WAAW,GAAG,+CAA+C,UAAU;GACvE,CAAA;;AAIN,QAAO,oBADO,OAAO,KAAK,SAAS,QAC5B,EAAO,WAAW,GAAG,gCAAgC,UAAU,EAAI,CAAA;;;;ACxD5E,SAAgB,aACd,MACA,kBACQ;AAIR,SAAQ,KAAK,aAAa,EAAE,EAAE,KAAK,SAAS;EAC1C,KAAK,IAAI;EACT,OAAO,cAAc,IAAI;EACzB,MAAM;EACN,MAAM,cAAc,IAAI,GAAI,UAAqB;EACjD,OAAO,UAAU,IAAI,OAAO;EAC5B,QAAQ,IAAI,YAAY,IAAI,OAAO,IAAI,UAAU,GAAG,KAAA;EACpD,OAAO,GAAG,cAAc,IAAI,CAAC,KAAK,IAAI,UAAU,OAAO,IAAI,cAAc,IAAI,KAAK;EAClF,gBAAgB,iBAAiB,IAAI,UAAU;EAChD,EAAE;;AAGL,SAAS,UAAU,QAA+C;AAChE,SAAQ,QAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,QACE,QAAO;;;;AAKb,SAAgB,aAAa,OAAgC;AAC3D,QAAO,MAAM,QAAQ,MAAM,EAAE,UAAU,UAAU,CAAC;;;;;;;;;;;;AAapD,SAAgB,WAAW,EACzB,UACA,SACA,OACA,MACA,YAOC;CAMD,MAAM,QAAQ,UAAU,KAAK,UAAU,QAAQ,GAAG,QAAQ,GAAG,UAAU,OAAO,MAAM;CACpF,MAAM,QACJ,UAAU,KAAK,UAAU,QACrB,GAAG,QAAQ,MAAM,MAAM,GAAG,KAAK,aAC/B,GAAG,MAAM,GAAG,OAAO,UAAU,IAAI,KAAK;CAC5C,MAAM,UAAU,WAAW,cAAc;AACzC,QACE,qBAAC,UAAD;EACE,MAAK;EACL,iBAAe;EACf,cAAY,GAAG,WAAW,SAAS,OAAO,GAAG;EAC7C,OAAO,GAAG,WAAW,SAAS,OAAO,GAAG;EACxC,UAAU,MAAM;AAId,KAAE,iBAAiB;AACnB,aAAU;;EAEZ,WAAW,GACT,kEACA,0CACA,UAAU,IAAI,cAAc,YAC7B;YAhBH,CAiBE,oBAAC,SAAD,EAAS,WAAU,UAAW,CAAA,EAC9B,oBAAC,QAAD;GAAM,WAAU;aAAgB;GAAa,CAAA,CACtC;;;;;;;;;;;;;;;;AAiBb,SAAgB,QAAQ,EAAE,MAAM,YAAkD;CAChF,MAAM,QAAQ,KAAK,SAAS;CAO5B,MAAM,OAAO,KAAK,UAAU,WAAW,gBAAgB,QAAQ,iBAAiB;CAChF,MAAM,UACJ,qBAAA,YAAA,EAAA,UAAA;EACE,oBAAC,UAAD;GAAU,OAAO,KAAK;GAAO,MAAM,KAAK;GAAQ,CAAA;EAChD,oBAAC,QAAD;GAAM,WAAU;aAA2B,KAAK;GAAa,CAAA;EAI5D,KAAK,SAAS,oBAAC,QAAD;GAAM,WAAU;aAAmC,KAAK;GAAc,CAAA,GAAG;EACvF,QAAQ,oBAAC,YAAD,EAAY,WAAU,8BAA+B,CAAA,GAAG;EAChE,EAAA,CAAA;CAEL,MAAM,QACJ;AAOF,KAAI,CAAC,MACH,QACE,oBAAC,OAAD;EAAK,OAAO,KAAK;EAAO,UAAU,MAAM,EAAE,iBAAiB;EAAE,WAAW,GAAG,OAAO,KAAK;YACpF;EACG,CAAA;AAGV,QACE,oBAAC,UAAD;EACE,MAAK;EACL,OAAO,KAAK;EACZ,UAAU,MAAM;AACd,KAAE,iBAAiB;AACnB,aAAU;;EAEZ,WAAW,GAAG,OAAO,0BAA0B,KAAK;YACnD;EACM,CAAA;;AAIb,SAAS,SAAS,EAAE,OAAO,QAAsD;AAG/E,KAAI,SAAS,UAAU,UAAU,aAAa,UAAU,SACtD,QAAO,oBAAC,KAAD,EAAK,WAAU,mBAAoB,CAAA;AAE5C,SAAQ,OAAR;EACE,KAAK,UACH,QAAO,oBAAC,SAAD,EAAS,WAAU,mBAAoB,CAAA;EAChD,KAAK,SACH,QAAO,oBAAC,aAAD,EAAa,WAAU,mBAAoB,CAAA;EACpD,KAAK,UACH,QAAO,oBAAC,aAAD,EAAa,WAAU,mBAAoB,CAAA;EACpD,QACE,QAAO,oBAAC,OAAD,EAAO,WAAU,mBAAoB,CAAA;;;;;;;;;;;;;;;;;;;;;ACvFlD,SAAgB,cAAc,QAAyB;AACrD,QAAO,GACL,kCACA,SAAS,sDAAsD,kBAChE;;AAGH,SAAgB,eAAe,EAC7B,MACA,QACA,gBACA,OACA,UACA,UACA,UACA,UACA,gBACA,kBACA,YACA,eAAe,MACf,cACA,aACsB;CACtB,MAAM,UAAU,cAAc,WAAW,MAAM,QAAQ,MAAM,EAAE;EAAC;EAAM;EAAQ;EAAM,CAAC;CACrF,MAAM,SAAS,cAAc,UAAU,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,CAAC;CAC3E,MAAM,SAAS,cAAc,QAAQ,OAAO,QAAQ,QAAQ,KAAK,OAAO;CAGxE,MAAM,WAAW,cAAc,WAAW,KAAK,EAAE,CAAC,KAAK,CAAC;CACxD,MAAM,WAAW,cAAc,WAAW,KAAK,EAAE,CAAC,KAAK,CAAC;CACxD,MAAM,WAAW,cAAc;EAC7B,MAAM,uBAAO,IAAI,KAAqB;AACtC,OAAK,MAAM,OAAO,KAAM,MAAK,IAAI,IAAI,QAAQ,IAAI,SAAS;AAC1D,SAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,WAAW;GAAE;GAAI;GAAM,EAAE;IACnD,CAAC,KAAK,CAAC;CAEV,MAAM,OAAO,UAA+B,eAAe;EAAE,GAAG;EAAQ,GAAG;EAAO,CAAC;AAEnF,QACE,qBAAC,OAAD;EAAK,aAAU;EAAkB,WAAW,GAAG,uBAAuB,UAAU;YAAhF;GAME,qBAAC,OAAD;IAAK,WAAW,GAAG,8BAA8B,CAAC,gBAAgB,SAAS;cAA3E;KACE,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD,EAAQ,WAAU,qFAAsF,CAAA,EACxG,oBAAC,OAAD;OACE,OAAO,OAAO;OACd,WAAW,MAAM,IAAI,EAAE,QAAQ,EAAE,OAAO,OAAO,CAAC;OAChD,aAAY;OACZ,cAAW;OACX,WAAU;OACV,CAAA,CACE;;KACN,oBAAC,WAAD;MAAW,OAAM;gBACf,oBAAC,aAAD;OACE,OAAM;OACN,OAAO,OAAO;OACd,SAAS,YAAY,KAAK,OAAO;QAAE,OAAO;QAAG,OAAO,aAAa;QAAI,EAAE;OACvE,WAAW,WAAW,IAAI,EAAU,QAA0B,CAAC;OAC/D,CAAA;MACQ,CAAA;KACX,SAAS,SAAS,IACjB,oBAAC,WAAD;MAAW,OAAM;gBACf,oBAAC,aAAD;OACE,OAAM;OACN,OAAO,OAAO;OACd,SAAS,SAAS,KAAK,OAAO;QAAE,OAAO;QAAG,OAAO;QAAG,EAAE;OACtD,WAAW,aAAa,IAAI,EAAE,UAAU,CAAC;OACzC,CAAA;MACQ,CAAA,GACV;KACH,SAAS,SAAS,IACjB,oBAAC,WAAD;MAAW,OAAM;gBACf,oBAAC,aAAD;OACE,OAAM;OACN,OAAO,OAAO;OACd,SAAS,SAAS,KAAK,OAAO;QAAE,OAAO,EAAE;QAAI,OAAO,EAAE;QAAM,EAAE;OAC9D,WAAW,aAAa,IAAI,EAAE,UAAU,CAAC;OACzC,CAAA;MACQ,CAAA,GACV;KACH,SAAS,SAAS,IACjB,oBAAC,WAAD;MAAW,OAAM;gBACf,oBAAC,aAAD;OACE,OAAM;OACN,OAAO,OAAO,YAAY,EAAE;OAC5B,SAAS,SAAS,KAAK,OAAO;QAAE,OAAO,EAAE;QAAK,OAAO,EAAE;QAAO,EAAE;OAChE,WAAW,SAAS,IAAI,EAAE,UAAU,MAAM,CAAC;OAC3C,CAAA;MACQ,CAAA,GACV;KACJ,oBAAC,WAAD;MAAW,OAAM;gBACf,oBAAC,aAAD;OACE,OAAM;OACN,OAAO,OAAO;OACd,SAAS;QACP;SAAE,OAAO;SAAQ,OAAO;SAAe;QACvC;SAAE,OAAO;SAAS,OAAO;SAAY;QACrC;SAAE,OAAO;SAAW,OAAO;SAAa;QACxC,GAAI,SAAS,SAAS,IAAI,CAAC;SAAE,OAAO;SAAoB,OAAO;SAAc,CAAC,GAAG,EAAE;QACnF,GAAI,SAAS,SAAS,IAAI,CAAC;SAAE,OAAO;SAAoB,OAAO;SAAc,CAAC,GAAG,EAAE;QACpF;OACD,WAAW,YAAY,IAAI,EAAW,SAAoB,CAAC;OAC3D,CAAA;MACQ,CAAA;KACZ,oBAAC,WAAD;MAAW,OAAM;gBACf,oBAAC,aAAD;OACE,OAAM;OACN,OAAO,OAAO;OACd,SAAS;QACP;SAAE,OAAO;SAAU,OAAO;SAAU;QACpC;SAAE,OAAO;SAAQ,OAAO;SAAQ;QAChC;SAAE,OAAO;SAAS,OAAO;SAAS;QAClC,GAAI,SAAS,SAAS,IAAI,CAAC;SAAE,OAAO;SAAoB,OAAO;SAAW,CAAC,GAAG,EAAE;QAChF,GAAI,SAAS,SAAS,IAAI,CAAC;SAAE,OAAO;SAAoB,OAAO;SAAW,CAAC,GAAG,EAAE;QACjF;OACD,WAAW,WAAW,IAAI,EAAU,QAAkB,CAAC;OACvD,CAAA;MACQ,CAAA;KACR;;GAEL,SACC,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,qBAAC,QAAD,EAAA,UAAA;KACG,OAAO;KAAM;KAAK,OAAO;KACzB,OAAO,OAAO,SAAS,MAAM,OAAO,OAAO,KAAK,MAAM,KAAK;KACvD,EAAA,CAAA,EACP,oBAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,eAAe,eAAe,aAAa,OAAO,CAAC;eAAE;KAE9C,CAAA,CACL;QACJ;GAEH,KAAK,WAAW,IACd,cAAc,oBAAC,OAAD;IAAO,MAAM,oBAAC,QAAD,EAAU,CAAA;IAAE,OAAM;IAAoB,CAAA,GAChE,QAAQ,WAAW,IAKrB,eAAe,OAAO,GACpB,oBAAC,OAAD;IACE,MAAM,oBAAC,SAAD,EAAW,CAAA;IACjB,OAAM;IACN,aAAY;IACZ,QAAO;IACP,gBAAgB,eAAe,aAAa,OAAO,CAAC;IACpD,CAAA,GAEF,oBAAC,OAAD;IAAO,MAAM,oBAAC,QAAD,EAAU,CAAA;IAAE,OAAM;IAAe,aAAY;IAAwB,CAAA,GAGpF,oBAAC,OAAD;IAAK,WAAU;cACZ,OAAO,KAAK,UACX,qBAAC,OAAD;KAAqB,WAAU;eAA/B,CACG,OAAO,YAAY,UAAU,MAAM,QAClC,qBAAC,OAAD;MAAK,WAAU;gBAAf;OAGG,OAAO,YAAY,YAClB,oBAAC,aAAD;QACE,MAAM,MAAM,KAAK,IAAI,KAAK,SAAS;QACnC,KAAK,UAAU,MAAM,KAAK,IAAI,aAAa;QAC3C,MAAM,MAAM;QACZ,CAAA,GACA;OACJ,oBAAC,QAAD;QAAM,WAAU;kBAA2B,MAAM;QAAa,CAAA;OAC9D,oBAAC,QAAD;QAAM,WAAU;kBAAgB,MAAM,KAAK;QAAc,CAAA;OACrD;UACJ,MACH,MAAM,KAAK,KAAK,QACf,oBAAC,gBAAD;MAEO;MACL,QAAQ,IAAI,KAAK,OAAO;MACxB,aAAa,SAAS,SAAS,KAAK,OAAO,YAAY;MACvD,aAAa,OAAO,YAAY;MAClB;MACJ;MACA;MACA;MACM;MACE;MAClB,EAXK,GAAG,IAAI,OAAO,GAAG,IAAI,KAAK,KAW/B,CACF,CACE;OA/BI,MAAM,IA+BV,CACN;IACE,CAAA;GAEJ;;;;;;AAOV,SAAS,UACP,KACA,OACoB;CACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,QAAO,MAAM,SAAS,UAAU,QAAQ,KAAK,QAAQ,KAAA;;AA+BvD,SAAS,eAAe,EACtB,KACA,QACA,aACA,cAAc,MACd,cACA,UACA,UACA,UACA,gBACA,oBACsB;CACtB,MAAM,EAAE,SAAS;CACjB,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAI7C,MAAM,CAAC,UAAU,eAAe,SAAS,MAAM;CAQ/C,MAAM,UAAU,cAAc,aAAa,IAAI,GAAG,eAAe,IAAI;CACrE,MAAM,cAAc,cAAc,KAAK,SAAS,OAAO,KAAA;CACvD,MAAM,SAAS,KAAK,UAAU;CAI9B,MAAM,UAAU;EACd;EACA,cAAc,IAAI,WAAW,KAAA;EAC7B,KAAK,UAAU,IAAI,KAAK,YAAY,KAAA;EACpC,WAAW,KAAK,aAAa;EAC9B,CAAC,OAAO,QAAQ;CACjB,MAAM,QAAQ,aAAa,OAAO,cAChC,mBAAmB,iBAAiB,KAAK,UAAU,GAAG,WAAW,IAAI,CACtE;CAKD,MAAM,MAAM,YAAY,KAAA,IAAY,KAAK,QAAQ,QAAQ,QAAQ;AAEjE,QACE,qBAAC,OAAD;EACE,aAAU;EACV,eAAa,UAAU,KAAA;EAQvB,eAAe,CAAC,WAAW,WAAW,IAAI;EAC1C,WAAW,GACT,0EACA,cAAc,WAAW,KAAK,CAC/B;YAdH;GAyBE,qBAAC,OAAD;IAAK,WAAU;cAAf;KACE,oBAAC,QAAD,EAAA,UACE,oBAAC,mBAAD,EAAwB,KAAO,CAAA,EACxB,CAAA;KAIR,WAAW,WACV,oBAAC,YAAD;MACE,SAAS,KAAK,SAAS;MACvB,WAAW,UAAU;AACnB,kBAAW,MAAM;AACjB,gBAAS,KAAK,MAAM;;MAEtB,gBAAgB,WAAW,MAAM;MACjC,CAAA,GAEF,oBAAC,UAAD;MACE,MAAK;MACL,WAAW,GACT,+DACA,SAAS,0BAA0B,YACpC;gBACA,aAAa,KAAK;MACZ,CAAA;KAEV,IAAI,SAAS,IAGZ,oBAAC,QAAD;MACE,OAAO,GAAG,IAAI,OAAO;MACrB,WAAU;gBACT,IAAI;MACA,CAAA,GACL;KACJ,oBAAC,QAAD;MAAM,WAAU;gBACb,mBAAmB,KAAK,kBAAkB,KAAK,UAAU;MACrD,CAAA;KACP,oBAAC,aAAD,EAAa,OAAO,KAAK,cAAgB,CAAA;KACrC;;GAKN,qBAAC,OAAD;IAAK,WAAU;cAAf;KACE,oBAAC,QAAD,EAAA,UAIE,oBAAC,YAAD;MACU;MACR,OAAO,KAAK;MACZ,WAAW,gBAAgB,QAAQ,KAAK,MAAM;MAC9C,CAAA,EACK,CAAA;KACT,qBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,WAAU;gBAHZ;OAIE,oBAAC,QAAD;QAAM,WAAW,gBAAgB,QAAQ,KAAK,MAAM;kBAGjD,cAAc,KAAK,MAAM;QACrB,CAAA;OACN,QAAQ,SAAS,IAAI,QAAQ;OAC7B,MAAM,IACL,QAAQ,KAAK,MAAM,GAEnB,qBAAA,YAAA,EAAA,UAAA;QACG,QAAQ,MAAM,GAAG,IAAI,CAAC,KAAK,SAAS,GAAG,KAAK,KAAK;QAClD,oBAAC,aAAD;SACE,MAAM;SACN,KAAK,UAAU,KAAK,aAAa;SACjC,MAAM;SAGN,WAAU;SACV,CAAA;QACD,QAAQ,MAAM,IAAI,CAAC,KAAK,MAAM;QAC9B,EAAA,CAAA;OAEE;;KACR,MAAM,SAAS,IACd,oBAAC,YAAD;MACY;MACV,SAAS,aAAa,MAAM;MAC5B,OAAO,MAAM;MACb,MAAM,MAAM,GAAI;MAChB,gBAAgB,aAAa,MAAM,CAAC,EAAE;MACtC,CAAA,GACA;KACH,YAAY,CAAC,UACZ,oBAAC,QAAD;MACE,SAAQ;MACR,MAAK;MACL,cAAW;MACX,WAAU;MACV,UAAU,MAAM;AACd,SAAE,iBAAiB;AACnB,kBAAW,KAAK;;gBAElB,oBAAC,QAAD,EAAQ,WAAU,oBAAqB,CAAA;MAChC,CAAA,GACP;KACH,kBAAkB,KAAK,cAAc,eACpC,oBAAC,QAAD;MACE,SAAQ;MACR,MAAK;MACL,cAAW;MACX,OAAM;MACN,WAAU;MACV,UAAU,MAAM;AACd,SAAE,iBAAiB;AACnB,sBAAe,IAAI;;gBAErB,oBAAC,QAAD,EAAQ,WAAU,oBAAqB,CAAA;MAChC,CAAA,GACP;KACH,WACC,oBAAC,QAAD;MACE,SAAQ;MACR,MAAK;MACL,cAAW;MACX,WAAU;MACV,UAAU,MAAM;AACd,SAAE,iBAAiB;AACnB,gBAAS,IAAI;;gBAEf,oBAAC,QAAD,EAAQ,WAAU,oBAAqB,CAAA;MAChC,CAAA,GACP;KACA;;GACL,WAAW,MAAM,KAAK,SAAS,oBAAC,SAAD;IAA8B;IAAM,UAAU,KAAK;IAAY,EAAjD,KAAK,IAA4C,CAAC,GAAG;GAC/F;;;;;;;;;;;AAYV,SAAS,OAAO,EAAE,YAAqC;AACrD,QAAO,oBAAC,QAAD;EAAM,WAAU;EAAmD;EAAgB,CAAA;;;;;;;;;;;;;;;;;;;AAoB5F,SAAgB,kBAAkB,EAAE,OAA4B;CAC9D,MAAM,EAAE,SAAS;AACjB,KAAI,IAAI,UAAU,YAChB,QAAO,oBAAC,UAAD,EAAU,WAAU,gDAAiD,CAAA;AAE9E,KAAI,IAAI,UAAU,UAChB,QAAO,oBAAC,SAAD,EAAS,WAAU,+BAAgC,CAAA;AAE5D,SAAQ,KAAK,QAAb;EACE,KAAK,SACH,QAAO,oBAAC,aAAD,EAAa,WAAU,iCAAkC,CAAA;EAClE,KAAK,SACH,QAAO,oBAAC,aAAD,EAAa,WAAU,+BAAgC,CAAA;EAChE,KAAK,SACH,QAAO,oBAAC,aAAD,EAAa,WAAU,+BAAgC,CAAA;EAChE,QACE,QAAO,oBAAC,MAAD,EAAM,WAAU,+BAAgC,CAAA;;;;;;;;AAS7D,SAAS,WAAW,EAClB,SACA,UACA,YAKC;CACD,MAAM,CAAC,OAAO,YAAY,SAAS,QAAQ;CAC3C,MAAM,MAAM,OAAyB,KAAK;AAC1C,iBAAgB;AACd,MAAI,SAAS,QAAQ;IACpB,EAAE,CAAC;AACN,QACE,oBAAC,SAAD;EACO;EACE;EACP,WAAA;EACA,cAAW;EAEX,UAAU,MAAM,EAAE,iBAAiB;EACnC,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM;EACzC,cAAe,SAAS,UAAU,GAAG,SAAS,MAAM,MAAM,CAAC,GAAG,KAAA;EAC9D,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,QAAS,UAAS,MAAM,MAAM,CAAC;YACpC,EAAE,QAAQ,SAAU,WAAU;AACvC,KAAE,iBAAiB;;EAErB,WAAU;EACV,CAAA;;;;;;;;;AAWN,SAAS,UAAU,EAAE,OAAO,YAA0D;AACpF,QACE,qBAAC,OAAD;EAAK,WAAU;YAAf,CACE,oBAAC,QAAD;GAAM,eAAA;GAAY,WAAU;aACzB;GACI,CAAA,EACP,oBAAC,OAAD;GAAK,WAAU;GAAkB;GAAe,CAAA,CAC5C;;;AAIV,SAAS,YAAY,EACnB,OACA,OACA,SACA,YAMC;AACD,QACE,qBAAC,OAAD;EAAK,WAAU;YAAf,CACE,qBAAC,QAAD;GAAQ,UAAA;GAAgB;GAAO,gBAAgB,MAAM,SAAS,EAAc;aAA5E,CACE,oBAAC,eAAD;IAAe,cAAY;IAAO,WAAU;cAC1C,oBAAC,aAAD,EAAA,UAEG,MAAM,WAAW,IACd,QACA,MAAM,WAAW,IACd,QAAQ,MAAM,MAAM,EAAE,UAAU,MAAM,GAAG,EAAE,SAAS,QACrD,GAAG,MAAM,OAAO,YACV,CAAA;IACA,CAAA,EAChB,oBAAC,eAAD,EAAA,UACG,QAAQ,KAAK,WACZ,oBAAC,YAAD;IAA+B,OAAO,OAAO;cAC3C,oBAAC,gBAAD,EAAA,UAAiB,OAAO,OAAuB,CAAA;IACpC,EAFI,OAAO,MAEX,CACb,EACY,CAAA,CACT;MACR,MAAM,SAAS,IACd,oBAAC,QAAD;GAAQ,SAAQ;GAAQ,MAAK;GAAU,cAAY,SAAS;GAAS,eAAe,SAAS,EAAE,CAAC;aAC9F,oBAAC,GAAD,EAAG,WAAU,oBAAqB,CAAA;GAC3B,CAAA,GACP,KACA;;;AAIV,SAAS,YAAY,EACnB,OACA,OACA,SACA,YAMC;AACD,QACE,qBAAC,QAAD;EAAe;EAAO,gBAAgB,MAAM,SAAS,EAAY;YAAjE,CACE,oBAAC,eAAD;GAAe,cAAY;GAAO,WAAU;aAC1C,oBAAC,aAAD,EAAA,UAAc,QAAQ,MAAM,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,OAAoB,CAAA;GACrE,CAAA,EAChB,oBAAC,eAAD,EAAA,UACG,QAAQ,KAAK,WACZ,oBAAC,YAAD;GAA+B,OAAO,OAAO;aAC3C,oBAAC,gBAAD,EAAA,UAAiB,OAAO,OAAuB,CAAA;GACpC,EAFI,OAAO,MAEX,CACb,EACY,CAAA,CACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerdeck/ui",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "Styled agent-control component library for WorkerDeck: session panel, transcript with streaming, tool-call cards, permission prompts, session list, composer. Tailwind v4 + Base UI + cva. Pairs with the headless @workerdeck/react layer.",
6
6
  "license": "MIT",
@@ -38,9 +38,9 @@
38
38
  "streamdown": "^2.5.0",
39
39
  "tailwind-merge": "^3.6.0",
40
40
  "use-stick-to-bottom": "^1.1.6",
41
- "@workerdeck/client": "0.19.0",
42
- "@workerdeck/protocol": "0.19.0",
43
- "@workerdeck/react": "0.19.0"
41
+ "@workerdeck/client": "0.20.0",
42
+ "@workerdeck/protocol": "0.20.0",
43
+ "@workerdeck/react": "0.20.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "monaco-editor": "^0.56.0",
@@ -4,6 +4,7 @@ import {
4
4
  BellRing,
5
5
  CircleAlert,
6
6
  CircleSlash,
7
+ Eraser,
7
8
  Layers,
8
9
  Moon,
9
10
  PauseCircle,
@@ -82,6 +83,18 @@ export interface SessionBrowserProps {
82
83
  * double-click would have already left the page.
83
84
  */
84
85
  onRename?: (row: SessionRow, title: string) => void
86
+ /**
87
+ * Clear the conversation in place, from the row's eraser. Same session, empty
88
+ * context — **not** a delete: the engine keeps the old conversation and it
89
+ * stays resumable, which is why the row's copy never says "deleted".
90
+ *
91
+ * Rendered only where the session's own capability record allows it
92
+ * (`capabilities.clearContext`, absent = false), so a row that cannot be
93
+ * cleared has no eraser rather than one that errors. Omit the prop to have
94
+ * none at all — a host with no way to send a session command has nothing to
95
+ * hang it on (this is a WS command, not a REST route).
96
+ */
97
+ onClearContext?: (row: SessionRow) => void
85
98
  /** Open a session *at* one of its sub-agents, when the host can scroll a
86
99
  * transcript to a `Task` row. Absent, the sub-agent list still expands and a
87
100
  * step just opens its session — see `SessionRowItemProps`. */
@@ -144,6 +157,7 @@ export function SessionBrowser({
144
157
  onSelect,
145
158
  onDelete,
146
159
  onRename,
160
+ onClearContext,
147
161
  onSelectSubagent,
148
162
  emptyState,
149
163
  showControls = true,
@@ -314,6 +328,7 @@ export function SessionBrowser({
314
328
  onSelect={onSelect}
315
329
  onDelete={onDelete}
316
330
  onRename={onRename}
331
+ onClearContext={onClearContext}
317
332
  onSelectSubagent={onSelectSubagent}
318
333
  />
319
334
  ))}
@@ -352,6 +367,9 @@ interface SessionRowItemProps {
352
367
  onSelect?: (row: SessionRow) => void
353
368
  onDelete?: (row: SessionRow) => void
354
369
  onRename?: (row: SessionRow, title: string) => void
370
+ /** See `SessionBrowserProps.onClearContext` — gated on the row's own
371
+ * capability record, not on the engine name. */
372
+ onClearContext?: (row: SessionRow) => void
355
373
  /** Open one of a session's sub-agents — which now means handing the session
356
374
  * panel over to that agent's own work (`SessionPanel.openSubagent`), and meant
357
375
  * "scroll to its `Task` row" before that surface existed. Both are honest; a
@@ -370,6 +388,7 @@ function SessionRowItem({
370
388
  onSelect,
371
389
  onDelete,
372
390
  onRename,
391
+ onClearContext,
373
392
  onSelectSubagent,
374
393
  }: SessionRowItemProps) {
375
394
  const { info } = row
@@ -536,6 +555,20 @@ function SessionRowItem({
536
555
  <Pencil className='size-3 text-fg-3' />
537
556
  </Button>
538
557
  ) : null}
558
+ {onClearContext && info.capabilities?.clearContext ? (
559
+ <Button
560
+ variant='ghost'
561
+ size='icon-sm'
562
+ aria-label='Clear context'
563
+ title='Clear the conversation — the session keeps running and the old conversation stays resumable'
564
+ className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
565
+ onClick={(e) => {
566
+ e.stopPropagation()
567
+ onClearContext(row)
568
+ }}>
569
+ <Eraser className='size-3 text-fg-3' />
570
+ </Button>
571
+ ) : null}
539
572
  {onDelete ? (
540
573
  <Button
541
574
  variant='ghost'