@workerdeck/ui 1.1.0 → 1.2.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.
@@ -0,0 +1,25 @@
1
+ //#region src/lib/format.d.ts
2
+ /**
3
+ * The whole payload of a `compaction` row. The wire event carries no summary because codex's
4
+ * `contextCompaction` item carries none, so the sentence *is* the content.
5
+ *
6
+ * It lives here, in a pure module, because the renderer (`terminal/items.tsx`) and the height book
7
+ * (`terminal/height.ts`) must measure and draw the same string — height.ts cannot import the
8
+ * `.tsx` without pulling React into a module the tests require to stay pure.
9
+ */
10
+ declare const COMPACTION_TEXT = "context compacted · earlier turns summarised to fit the window";
11
+ declare function formatCost(usd: number | undefined): string;
12
+ declare function formatDuration(ms: number): string;
13
+ declare function formatTokens(tokens: number): string;
14
+ declare function formatBytes(bytes: number): string;
15
+ declare function formatCountdown(untilEpochMs: number, now?: number): string;
16
+ declare function formatRelativeTime(epochMs: number | undefined, now?: number): string;
17
+ declare function formatRateLimitWindow(key: string): string;
18
+ declare function formatRateLimitWindowLong(key: string): string;
19
+ declare function rateLimitWindowSeconds(key: string): number | undefined;
20
+ declare function formatAgoPrecise(epochMs: number, now?: number): string;
21
+ declare function toolInputPreview(input: unknown, max?: number): string;
22
+ declare function friendlyModel(id: string | undefined): string | undefined;
23
+ //#endregion
24
+ export { formatCountdown as a, formatRateLimitWindowLong as c, friendlyModel as d, rateLimitWindowSeconds as f, formatCost as i, formatRelativeTime as l, formatAgoPrecise as n, formatDuration as o, toolInputPreview as p, formatBytes as r, formatRateLimitWindow as s, COMPACTION_TEXT as t, formatTokens as u };
25
+ //# sourceMappingURL=format-C4h4jZL_.d.mts.map
@@ -1,4 +1,4 @@
1
- import { a as formatDuration, c as formatRelativeTime, d as rateLimitWindowSeconds, f as toolInputPreview, i as formatCountdown, l as formatTokens, n as formatBytes, o as formatRateLimitWindow, r as formatCost, s as formatRateLimitWindowLong, t as formatAgoPrecise, u as friendlyModel } from "./format-C_lpFq0l.mjs";
1
+ import { a as formatCountdown, c as formatRateLimitWindowLong, d as friendlyModel, f as rateLimitWindowSeconds, i as formatCost, l as formatRelativeTime, n as formatAgoPrecise, o as formatDuration, p as toolInputPreview, r as formatBytes, s as formatRateLimitWindow, t as COMPACTION_TEXT, u as formatTokens } from "./format-C4h4jZL_.mjs";
2
2
  import { ModelOption, RateLimitInfo, SessionStatus } from "@workerdeck/protocol";
3
3
  //#region src/lib/status.d.ts
4
4
  type StatusSeverity = 'none' | 'warning' | 'error';
@@ -40,5 +40,5 @@ declare function meterColorClass(pct: number | undefined): string;
40
40
  */
41
41
  declare function meterTintClass(pct: number): string;
42
42
  //#endregion
43
- export { ModelReadings, StatusPresentation, StatusReadings, StatusSeverity, UsageLane, contextSeverity, currentModel, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, friendlyModel, meterColorClass, meterSeverity, meterTintClass, modelLabel, rateLimitWindowSeconds, statusPresentation, tightestWindow, toolInputPreview, usageWindow, windowLabel };
43
+ export { COMPACTION_TEXT, ModelReadings, StatusPresentation, StatusReadings, StatusSeverity, UsageLane, contextSeverity, currentModel, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, friendlyModel, meterColorClass, meterSeverity, meterTintClass, modelLabel, rateLimitWindowSeconds, statusPresentation, tightestWindow, toolInputPreview, usageWindow, windowLabel };
44
44
  //# sourceMappingURL=format.d.mts.map
package/build/format.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { S as toolInputPreview, _ as formatRateLimitWindowLong, a as meterTintClass, b as friendlyModel, c as tightestWindow, d as formatAgoPrecise, f as formatBytes, g as formatRateLimitWindow, h as formatDuration, i as meterSeverity, l as usageWindow, m as formatCountdown, n as currentModel, o as modelLabel, p as formatCost, r as meterColorClass, s as statusPresentation, t as contextSeverity, u as windowLabel, v as formatRelativeTime, x as rateLimitWindowSeconds, y as formatTokens } from "./status-Dm9INVTt.mjs";
2
- export { contextSeverity, currentModel, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, friendlyModel, meterColorClass, meterSeverity, meterTintClass, modelLabel, rateLimitWindowSeconds, statusPresentation, tightestWindow, toolInputPreview, usageWindow, windowLabel };
1
+ import { C as toolInputPreview, S as rateLimitWindowSeconds, _ as formatRateLimitWindow, a as meterTintClass, b as formatTokens, c as tightestWindow, d as COMPACTION_TEXT, f as formatAgoPrecise, g as formatDuration, h as formatCountdown, i as meterSeverity, l as usageWindow, m as formatCost, n as currentModel, o as modelLabel, p as formatBytes, r as meterColorClass, s as statusPresentation, t as contextSeverity, u as windowLabel, v as formatRateLimitWindowLong, x as friendlyModel, y as formatRelativeTime } from "./status-BW2xj79g.mjs";
2
+ export { COMPACTION_TEXT, contextSeverity, currentModel, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, friendlyModel, meterColorClass, meterSeverity, meterTintClass, modelLabel, rateLimitWindowSeconds, statusPresentation, tightestWindow, toolInputPreview, usageWindow, windowLabel };
package/build/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as formatDuration, c as formatRelativeTime, d as rateLimitWindowSeconds, f as toolInputPreview, i as formatCountdown, l as formatTokens, n as formatBytes, o as formatRateLimitWindow, r as formatCost, s as formatRateLimitWindowLong, t as formatAgoPrecise, u as friendlyModel } from "./format-C_lpFq0l.mjs";
1
+ import { a as formatCountdown, c as formatRateLimitWindowLong, d as friendlyModel, f as rateLimitWindowSeconds, i as formatCost, l as formatRelativeTime, n as formatAgoPrecise, o as formatDuration, p as toolInputPreview, r as formatBytes, s as formatRateLimitWindow, u as formatTokens } from "./format-C4h4jZL_.mjs";
2
2
  import { C as permissionModeChoices, S as PermissionModeSelectProps, _ as useAffordances, a as SessionVitals, b as PermissionModeMeta, c as TranscriptDensityProvider, d as TranscriptVariantProvider, f as useTranscriptDensity, g as WithActions, h as TerminalAffordances, i as SessionSurfacePanel, l as TranscriptFont, m as CopyAction, n as SessionPanel, o as TerminalMetrics, p as useTranscriptVariant, r as SessionPanelProps, s as TranscriptDensity, t as SessionControls, u as TranscriptVariant, v as PERMISSION_MODES, w as permissionModeMeta, x as PermissionModeSelect, y as PermissionModeChoice } from "./SessionPanel-yNO60Tzw.mjs";
3
3
  import { ButtonHTMLAttributes, FunctionComponent, HTMLAttributes, InputHTMLAttributes, ReactElement, ReactNode, Ref, RefObject, TextareaHTMLAttributes } from "react";
4
4
  import { VariantProps } from "class-variance-authority";
@@ -15,7 +15,7 @@ import { ConnectionState, TranscriptItem, TranscriptState, UseAttachmentsResult
15
15
  import { WorkerDeckClient } from "@workerdeck/client";
16
16
  //#region src/components/ui/Button.d.ts
17
17
  declare const buttonVariants: (props?: ({
18
- variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | null | undefined;
18
+ variant?: "default" | "link" | "outline" | "secondary" | "ghost" | "destructive" | null | undefined;
19
19
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-sm" | null | undefined;
20
20
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
21
21
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
package/build/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as isMutatingTool, A as PERMISSION_MODES, At as MenuItem, B as Composer, Bt as Input, C as TerminalPermissionPrompt, Ct as DialogClose, D as SubagentStrip, Dt as DialogTrigger, E as parseUserQuestions, Et as DialogRow, F as SkillsDialog, Ft as SelectItem, G as previewPatch, Gt as cn, H as TerminalSurface, Ht as badgeVariants, I as McpDialog, It as SelectItemText, J as Ink, K as Band, L as HostFilesDialog, Lt as SelectTrigger, M as permissionModeChoices, Mt as MenuTrigger, N as permissionModeMeta, Nt as Select, O as PermissionPrompt, Ot as Menu, P as ModelSelect, Pt as SelectContent, Q as useAffordances, Rt as SelectValue, S as TerminalQuestionPrompt, St as DialogBody, T as QuestionPrompt, Tt as DialogHeader, U as TerminalMarkdown, Ut as Button, V as skillPrompt, Vt as Badge, W as TerminalDiff, Wt as buttonVariants, X as CopyAction, Y as Row, Z as WithActions, _ as ConversationContent, _t as copyText, a as Transcript, at as commandTrigger, b as STATUS_META, bt as TooltipProvider, c as PromptTokenText, ct as usePromptAreaState, d as FileCard, dt as segmentsToPlainText, et as toolIcon, f as TerminalItemView, ft as ProgressRing, g as Conversation, gt as CopyButton, h as Loader, ht as Spinner, i as useMinuteClock, it as useTranscriptVariant, j as PermissionModeSelect, jt as MenuSeparator, k as Response, kt as MenuContent, l as Message, lt as PromptArea, m as SessionEmptyState, mt as CodeBlock, n as UsageDialog, nt as TranscriptVariantProvider, o as ToolCallCard, ot as hashtagTrigger, p as TerminalTranscript, pt as Splitter, q as Blank, r as UsageMeters, rt as useTranscriptDensity, s as Reasoning, st as mentionTrigger, t as SessionPanel, tt as TranscriptDensityProvider, u as MessageContent, ut as plainTextToSegments, v as ConversationScrollButton, vt as Tip, w as QUESTION_BEHAVIORS, wt as DialogContent, x as SessionInfoDialog, xt as Dialog, y as StatusBar, yt as TooltipContent, z as ContextDialog, zt as PortalScope } from "./SessionPanel-I5QSLLb0.mjs";
2
- import { S as toolInputPreview, _ as formatRateLimitWindowLong, b as friendlyModel, c as tightestWindow, d as formatAgoPrecise, f as formatBytes, g as formatRateLimitWindow, h as formatDuration, i as meterSeverity, m as formatCountdown, o as modelLabel, p as formatCost, r as meterColorClass, s as statusPresentation, t as contextSeverity, u as windowLabel, v as formatRelativeTime, x as rateLimitWindowSeconds, y as formatTokens } from "./status-Dm9INVTt.mjs";
1
+ import { $ as isMutatingTool, A as PERMISSION_MODES, At as MenuItem, B as Composer, Bt as Input, C as TerminalPermissionPrompt, Ct as DialogClose, D as SubagentStrip, Dt as DialogTrigger, E as parseUserQuestions, Et as DialogRow, F as SkillsDialog, Ft as SelectItem, G as previewPatch, Gt as cn, H as TerminalSurface, Ht as badgeVariants, I as McpDialog, It as SelectItemText, J as Ink, K as Band, L as HostFilesDialog, Lt as SelectTrigger, M as permissionModeChoices, Mt as MenuTrigger, N as permissionModeMeta, Nt as Select, O as PermissionPrompt, Ot as Menu, P as ModelSelect, Pt as SelectContent, Q as useAffordances, Rt as SelectValue, S as TerminalQuestionPrompt, St as DialogBody, T as QuestionPrompt, Tt as DialogHeader, U as TerminalMarkdown, Ut as Button, V as skillPrompt, Vt as Badge, W as TerminalDiff, Wt as buttonVariants, X as CopyAction, Y as Row, Z as WithActions, _ as ConversationContent, _t as copyText, a as Transcript, at as commandTrigger, b as STATUS_META, bt as TooltipProvider, c as PromptTokenText, ct as usePromptAreaState, d as FileCard, dt as segmentsToPlainText, et as toolIcon, f as TerminalItemView, ft as ProgressRing, g as Conversation, gt as CopyButton, h as Loader, ht as Spinner, i as useMinuteClock, it as useTranscriptVariant, j as PermissionModeSelect, jt as MenuSeparator, k as Response, kt as MenuContent, l as Message, lt as PromptArea, m as SessionEmptyState, mt as CodeBlock, n as UsageDialog, nt as TranscriptVariantProvider, o as ToolCallCard, ot as hashtagTrigger, p as TerminalTranscript, pt as Splitter, q as Blank, r as UsageMeters, rt as useTranscriptDensity, s as Reasoning, st as mentionTrigger, t as SessionPanel, tt as TranscriptDensityProvider, u as MessageContent, ut as plainTextToSegments, v as ConversationScrollButton, vt as Tip, w as QUESTION_BEHAVIORS, wt as DialogContent, x as SessionInfoDialog, xt as Dialog, y as StatusBar, yt as TooltipContent, z as ContextDialog, zt as PortalScope } from "./SessionPanel-DVhnUwXw.mjs";
2
+ import { C as toolInputPreview, S as rateLimitWindowSeconds, _ as formatRateLimitWindow, b as formatTokens, c as tightestWindow, f as formatAgoPrecise, g as formatDuration, h as formatCountdown, i as meterSeverity, m as formatCost, o as modelLabel, p as formatBytes, r as meterColorClass, s as statusPresentation, t as contextSeverity, u as windowLabel, v as formatRateLimitWindowLong, x as friendlyModel, y as formatRelativeTime } from "./status-BW2xj79g.mjs";
3
3
  import { Fragment, forwardRef, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
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";
package/build/scoped.css CHANGED
@@ -1684,6 +1684,10 @@
1684
1684
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1685
1685
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1686
1686
  }
1687
+ .wd-root .ring {
1688
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
1689
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1690
+ }
1687
1691
  .wd-root .ring-2 {
1688
1692
  --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
1689
1693
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -1,4 +1,13 @@
1
1
  //#region src/lib/format.ts
2
+ /**
3
+ * The whole payload of a `compaction` row. The wire event carries no summary because codex's
4
+ * `contextCompaction` item carries none, so the sentence *is* the content.
5
+ *
6
+ * It lives here, in a pure module, because the renderer (`terminal/items.tsx`) and the height book
7
+ * (`terminal/height.ts`) must measure and draw the same string — height.ts cannot import the
8
+ * `.tsx` without pulling React into a module the tests require to stay pure.
9
+ */
10
+ const COMPACTION_TEXT = "context compacted · earlier turns summarised to fit the window";
2
11
  function formatCost(usd) {
3
12
  if (usd === void 0 || Number.isNaN(usd)) return "—";
4
13
  if (usd === 0) return "$0.00";
@@ -238,6 +247,6 @@ function meterTintClass(pct) {
238
247
  return pct >= 90 ? "bg-danger" : pct >= 70 ? "bg-warning" : "bg-accent";
239
248
  }
240
249
  //#endregion
241
- export { toolInputPreview as S, formatRateLimitWindowLong as _, meterTintClass as a, friendlyModel as b, tightestWindow as c, formatAgoPrecise as d, formatBytes as f, formatRateLimitWindow as g, formatDuration as h, meterSeverity as i, usageWindow as l, formatCountdown as m, currentModel as n, modelLabel as o, formatCost as p, meterColorClass as r, statusPresentation as s, contextSeverity as t, windowLabel as u, formatRelativeTime as v, rateLimitWindowSeconds as x, formatTokens as y };
250
+ export { toolInputPreview as C, rateLimitWindowSeconds as S, formatRateLimitWindow as _, meterTintClass as a, formatTokens as b, tightestWindow as c, COMPACTION_TEXT as d, formatAgoPrecise as f, formatDuration as g, formatCountdown as h, meterSeverity as i, usageWindow as l, formatCost as m, currentModel as n, modelLabel as o, formatBytes as p, meterColorClass as r, statusPresentation as s, contextSeverity as t, windowLabel as u, formatRateLimitWindowLong as v, friendlyModel as x, formatRelativeTime as y };
242
251
 
243
- //# sourceMappingURL=status-Dm9INVTt.mjs.map
252
+ //# sourceMappingURL=status-BW2xj79g.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-BW2xj79g.mjs","names":[],"sources":["../src/lib/format.ts","../src/lib/status.ts"],"sourcesContent":["/**\n * The whole payload of a `compaction` row. The wire event carries no summary because codex's\n * `contextCompaction` item carries none, so the sentence *is* the content.\n *\n * It lives here, in a pure module, because the renderer (`terminal/items.tsx`) and the height book\n * (`terminal/height.ts`) must measure and draw the same string — height.ts cannot import the\n * `.tsx` without pulling React into a module the tests require to stay pure.\n */\nexport const COMPACTION_TEXT = 'context compacted · earlier turns summarised to fit the window'\n\nexport function formatCost(usd: number | undefined): string {\n if (usd === undefined || Number.isNaN(usd)) {\n return '—'\n }\n if (usd === 0) {\n return '$0.00'\n }\n if (usd < 0.01) {\n return '<$0.01'\n }\n return `$${usd.toFixed(2)}`\n}\n\nexport function formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${Math.round(ms)}ms`\n }\n const s = ms / 1000\n if (s < 60) {\n return `${s.toFixed(1)}s`\n }\n const m = Math.floor(s / 60)\n return `${m}m ${Math.round(s % 60)}s`\n}\n\nexport function formatTokens(tokens: number): string {\n if (tokens >= 1_000_000) {\n return `${(tokens / 1_000_000).toFixed(1)}M`\n }\n if (tokens >= 1000) {\n return `${(tokens / 1000).toFixed(1)}k`\n }\n return String(Math.round(tokens))\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes >= 1024 * 1024) {\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n }\n if (bytes >= 1024) {\n return `${(bytes / 1024).toFixed(1)} KB`\n }\n return `${bytes} B`\n}\n\nexport function formatCountdown(untilEpochMs: number, now = Date.now()): string {\n const remaining = untilEpochMs - now\n if (remaining <= 0) {\n return 'now'\n }\n const minutes = Math.ceil(remaining / 60_000)\n if (minutes < 1) {\n return '<1m'\n }\n if (minutes < 60) {\n return `${minutes}m`\n }\n const days = Math.floor(minutes / (60 * 24))\n if (days >= 1) {\n return `${days}d ${Math.floor((minutes % (60 * 24)) / 60)}h`\n }\n return `${Math.floor(minutes / 60)}h ${minutes % 60}m`\n}\n\nexport function formatRelativeTime(epochMs: number | undefined, now = Date.now()): string {\n if (!epochMs) {\n return '—'\n }\n const diff = Math.max(0, now - epochMs)\n const s = Math.floor(diff / 1000)\n if (s < 60) {\n return 'just now'\n }\n const m = Math.floor(s / 60)\n if (m < 60) {\n return `${m}m ago`\n }\n const h = Math.floor(m / 60)\n if (h < 24) {\n return `${h}h ago`\n }\n const d = Math.floor(h / 24)\n return `${d}d ago`\n}\n\nexport function formatRateLimitWindow(key: string): string {\n if (key === 'five_hour') {\n return '5h'\n }\n if (key === 'seven_day') {\n return '7d'\n }\n const spaced = key.replaceAll('_', ' ')\n return key.startsWith('seven_day_') ? `7d ${spaced.slice('seven day '.length)}` : spaced\n}\n\nexport function formatRateLimitWindowLong(key: string): string {\n if (key === 'five_hour') {\n return '5-hour session'\n }\n if (key === 'seven_day') {\n return 'Weekly'\n }\n if (key === 'seven_day_oauth_apps') {\n return 'Weekly · apps'\n }\n const capitalize = (s: string) => s.replace(/\\b\\w/g, (c) => c.toUpperCase())\n if (!key.startsWith('seven_day_')) {\n return capitalize(key.replaceAll('_', ' '))\n }\n return `Weekly · ${capitalize(key.slice('seven_day_'.length).replaceAll('_', ' '))}`\n}\n\nexport function rateLimitWindowSeconds(key: string): number | undefined {\n if (key === 'five_hour') {\n return 5 * 3600\n }\n if (key.startsWith('seven_day')) {\n return 7 * 86_400\n }\n return undefined\n}\n\nexport function formatAgoPrecise(epochMs: number, now = Date.now()): string {\n const seconds = Math.max(0, Math.floor((now - epochMs) / 1000))\n if (seconds < 60) {\n return `${seconds} sec${seconds === 1 ? '' : 's'} ago`\n }\n if (seconds < 3600) {\n const minutes = Math.floor(seconds / 60)\n return `${minutes} min${minutes === 1 ? '' : 's'} ago`\n }\n const hours = Math.floor(seconds / 3600)\n return `${hours} hour${hours === 1 ? '' : 's'} ago`\n}\n\nexport function toolInputPreview(input: unknown, max = 80): string {\n if (input === null || input === undefined) {\n return ''\n }\n if (typeof input === 'object') {\n const o = input as Record<string, unknown>\n const primary = o.command ?? o.file_path ?? o.path ?? o.url ?? o.pattern ?? o.query ?? o.description\n if (typeof primary === 'string') {\n return primary.length > max ? primary.slice(0, max - 1) + '…' : primary\n }\n }\n const text = JSON.stringify(input) ?? ''\n return text.length > max ? text.slice(0, max - 1) + '…' : text\n}\n\nconst MODEL_FAMILIES: Record<string, { name: string; joiner?: string }> = {\n gpt: { name: 'GPT', joiner: '-' },\n deepseek: { name: 'DeepSeek' },\n glm: { name: 'GLM' },\n qwen: { name: 'Qwen' },\n kimi: { name: 'Kimi' },\n llama: { name: 'Llama' },\n mistral: { name: 'Mistral' },\n grok: { name: 'Grok' },\n}\n\nexport function friendlyModel(id: string | undefined): string | undefined {\n if (!id) {\n return undefined\n }\n const withoutVariant = id.split('[')[0] ?? id\n const parts = withoutVariant.toLowerCase().split('-').filter(Boolean)\n if (parts[0] === 'claude') {\n parts.shift()\n }\n const familyToken = parts.shift()\n if (!familyToken) {\n return id\n }\n const family = MODEL_FAMILIES[familyToken]\n const name =\n family?.name ??\n // OpenAI's reasoning series is lower-case by its own convention ('o3-mini'); \"O3\" reads as a different product.\n (/^o\\d+$/.test(familyToken) ? familyToken : `${familyToken.charAt(0).toUpperCase()}${familyToken.slice(1)}`)\n\n const version: string[] = []\n const words: string[] = []\n for (const part of parts) {\n if (/^\\d{8}$/.test(part)) {\n continue\n }\n if (/^\\d+(\\.\\d+)?$/.test(part)) {\n version.push(part)\n } else {\n words.push(`${part.charAt(0).toUpperCase()}${part.slice(1)}`)\n }\n }\n const versioned = version.length > 0 ? `${name}${family?.joiner ?? ' '}${version.join('.')}` : name\n return [versioned, ...words].join(' ')\n}\n","import type { ModelOption, RateLimitInfo, SessionStatus } from '@workerdeck/protocol'\n\nexport type StatusSeverity = 'none' | 'warning' | 'error'\n\nexport type StatusPresentation = {\n icon: string\n label: string\n severity: StatusSeverity\n}\n\nexport type StatusReadings = {\n status: SessionStatus\n connection?: 'live' | 'reconnecting' | 'offline'\n}\n\nconst STATUS_META: Record<SessionStatus, StatusPresentation> = {\n starting: { icon: 'loading~spin', label: 'Starting', severity: 'none' },\n running: { icon: 'loading~spin', label: 'Running', severity: 'none' },\n awaiting_approval: { icon: 'warning', label: 'Needs approval', severity: 'warning' },\n idle: { icon: 'check', label: 'Idle', severity: 'none' },\n parked: { icon: 'debug-pause', label: 'Parked', severity: 'none' },\n failed: { icon: 'error', label: 'Failed', severity: 'error' },\n closed: { icon: 'circle-slash', label: 'Closed', severity: 'none' },\n}\n\nexport function statusPresentation(vitals: StatusReadings | undefined): StatusPresentation {\n if (!vitals) {\n return { icon: 'hubot', label: 'Connecting…', severity: 'none' }\n }\n if (vitals.connection === 'offline') {\n return { icon: 'debug-disconnect', label: 'Offline', severity: 'error' }\n }\n if (vitals.connection === 'reconnecting') {\n return { icon: 'sync~spin', label: 'Reconnecting…', severity: 'warning' }\n }\n return STATUS_META[vitals.status] ?? { icon: 'hubot', label: vitals.status, severity: 'none' }\n}\n\nexport function meterSeverity(pct: number | undefined): StatusSeverity {\n if (pct === undefined) {\n return 'none'\n }\n if (pct >= 95) {\n return 'error'\n }\n if (pct >= 80) {\n return 'warning'\n }\n return 'none'\n}\n\nexport type UsageLane = 'session' | 'weekly' | 'model'\n\nexport function usageWindow(\n rateLimits: Record<string, RateLimitInfo> | undefined,\n lane: UsageLane,\n): { key: string; info: RateLimitInfo } | undefined {\n if (!rateLimits) {\n return undefined\n }\n if (lane === 'session') {\n const info = rateLimits.five_hour\n return info ? { key: 'five_hour', info } : undefined\n }\n if (lane === 'weekly') {\n const info = rateLimits.seven_day\n return info ? { key: 'seven_day', info } : undefined\n }\n const scoped = Object.fromEntries(\n Object.entries(rateLimits).filter(([key]) => key.startsWith('seven_day_') && key !== 'seven_day_oauth_apps'),\n )\n return tightestWindow(scoped)\n}\n\nexport function tightestWindow(rateLimits: Record<string, RateLimitInfo> | undefined): { key: string; info: RateLimitInfo } | undefined {\n const entries = Object.entries(rateLimits ?? {})\n if (entries.length === 0) {\n return undefined\n }\n let best: { key: string; info: RateLimitInfo } | undefined\n for (const [key, info] of entries) {\n const rank = info.status === 'rejected' ? Number.POSITIVE_INFINITY : (info.utilization ?? -1)\n const bestRank =\n best === undefined\n ? Number.NEGATIVE_INFINITY\n : best.info.status === 'rejected'\n ? Number.POSITIVE_INFINITY\n : (best.info.utilization ?? -1)\n if (rank > bestRank) {\n best = { key, info }\n }\n }\n return best\n}\n\nexport function windowLabel(key: string): string {\n if (key === 'five_hour') {\n return 'Session'\n }\n if (key === 'seven_day') {\n return 'Weekly'\n }\n const scoped = key.startsWith('seven_day_') ? key.slice('seven_day_'.length) : key\n const words = scoped.replaceAll('_', ' ')\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\nexport type ModelReadings = { model?: string; models: readonly ModelOption[] }\n\nexport function currentModel(vitals: ModelReadings | undefined): ModelOption | undefined {\n const id = vitals?.model\n if (!id) {\n return undefined\n }\n const bare = (value: string) => value.replace(/\\[.*\\]$/, '')\n const wanted = bare(id)\n return vitals.models.find((m) => bare(m.value) === wanted || (m.resolvedModel && bare(m.resolvedModel) === wanted))\n}\n\nexport function modelLabel(vitals: ModelReadings | undefined): string {\n if (!vitals?.model) {\n return 'Default'\n }\n return currentModel(vitals)?.displayName ?? vitals.model\n}\n\nexport function contextSeverity(usage: { percentage: number } | undefined): StatusSeverity {\n return meterSeverity(usage?.percentage)\n}\n\nexport function meterColorClass(pct: number | undefined): string {\n const severity = meterSeverity(pct)\n return severity === 'error' ? 'text-danger' : severity === 'warning' ? 'text-warning' : 'text-fg-3'\n}\n\n/**\n * The fill colour for a meter bar. Deliberately a different ramp from\n * {@link meterColorClass}: a bar reads as alarming later than a number does, so the tint\n * turns at 70/90 where the text severity turns at 80/95.\n */\nexport function meterTintClass(pct: number): string {\n return pct >= 90 ? 'bg-danger' : pct >= 70 ? 'bg-warning' : 'bg-accent'\n}\n"],"mappings":";;;;;;;;;AAQA,MAAa,kBAAkB;AAE/B,SAAgB,WAAW,KAAiC;CAC1D,IAAI,QAAQ,KAAA,KAAa,OAAO,MAAM,GAAG,GACvC,OAAO;CAET,IAAI,QAAQ,GACV,OAAO;CAET,IAAI,MAAM,KACR,OAAO;CAET,OAAO,IAAI,IAAI,QAAQ,CAAC;AAC1B;AAEA,SAAgB,eAAe,IAAoB;CACjD,IAAI,KAAK,KACP,OAAO,GAAG,KAAK,MAAM,EAAE,EAAE;CAE3B,MAAM,IAAI,KAAK;CACf,IAAI,IAAI,IACN,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE;CAGzB,OAAO,GADG,KAAK,MAAM,IAAI,EACf,EAAE,IAAI,KAAK,MAAM,IAAI,EAAE,EAAE;AACrC;AAEA,SAAgB,aAAa,QAAwB;CACnD,IAAI,UAAU,KACZ,OAAO,IAAI,SAAS,IAAA,CAAW,QAAQ,CAAC,EAAE;CAE5C,IAAI,UAAU,KACZ,OAAO,IAAI,SAAS,IAAA,CAAM,QAAQ,CAAC,EAAE;CAEvC,OAAO,OAAO,KAAK,MAAM,MAAM,CAAC;AAClC;AAEA,SAAgB,YAAY,OAAuB;CACjD,IAAI,SAAS,SACX,OAAO,IAAI,QAAS,QAAA,CAAc,QAAQ,CAAC,EAAE;CAE/C,IAAI,SAAS,MACX,OAAO,IAAI,QAAQ,KAAA,CAAM,QAAQ,CAAC,EAAE;CAEtC,OAAO,GAAG,MAAM;AAClB;AAEA,SAAgB,gBAAgB,cAAsB,MAAM,KAAK,IAAI,GAAW;CAC9E,MAAM,YAAY,eAAe;CACjC,IAAI,aAAa,GACf,OAAO;CAET,MAAM,UAAU,KAAK,KAAK,YAAY,GAAM;CAC5C,IAAI,UAAU,GACZ,OAAO;CAET,IAAI,UAAU,IACZ,OAAO,GAAG,QAAQ;CAEpB,MAAM,OAAO,KAAK,MAAM,UAAW,IAAQ;CAC3C,IAAI,QAAQ,GACV,OAAO,GAAG,KAAK,IAAI,KAAK,MAAO,UAAW,OAAY,EAAE,EAAE;CAE5D,OAAO,GAAG,KAAK,MAAM,UAAU,EAAE,EAAE,IAAI,UAAU,GAAG;AACtD;AAEA,SAAgB,mBAAmB,SAA6B,MAAM,KAAK,IAAI,GAAW;CACxF,IAAI,CAAC,SACH,OAAO;CAET,MAAM,OAAO,KAAK,IAAI,GAAG,MAAM,OAAO;CACtC,MAAM,IAAI,KAAK,MAAM,OAAO,GAAI;CAChC,IAAI,IAAI,IACN,OAAO;CAET,MAAM,IAAI,KAAK,MAAM,IAAI,EAAE;CAC3B,IAAI,IAAI,IACN,OAAO,GAAG,EAAE;CAEd,MAAM,IAAI,KAAK,MAAM,IAAI,EAAE;CAC3B,IAAI,IAAI,IACN,OAAO,GAAG,EAAE;CAGd,OAAO,GADG,KAAK,MAAM,IAAI,EACf,EAAE;AACd;AAEA,SAAgB,sBAAsB,KAAqB;CACzD,IAAI,QAAQ,aACV,OAAO;CAET,IAAI,QAAQ,aACV,OAAO;CAET,MAAM,SAAS,IAAI,WAAW,KAAK,GAAG;CACtC,OAAO,IAAI,WAAW,YAAY,IAAI,MAAM,OAAO,MAAM,EAAmB,MAAM;AACpF;AAEA,SAAgB,0BAA0B,KAAqB;CAC7D,IAAI,QAAQ,aACV,OAAO;CAET,IAAI,QAAQ,aACV,OAAO;CAET,IAAI,QAAQ,wBACV,OAAO;CAET,MAAM,cAAc,MAAc,EAAE,QAAQ,UAAU,MAAM,EAAE,YAAY,CAAC;CAC3E,IAAI,CAAC,IAAI,WAAW,YAAY,GAC9B,OAAO,WAAW,IAAI,WAAW,KAAK,GAAG,CAAC;CAE5C,OAAO,YAAY,WAAW,IAAI,MAAM,EAAmB,CAAC,CAAC,WAAW,KAAK,GAAG,CAAC;AACnF;AAEA,SAAgB,uBAAuB,KAAiC;CACtE,IAAI,QAAQ,aACV,OAAO;CAET,IAAI,IAAI,WAAW,WAAW,GAC5B,OAAO;AAGX;AAEA,SAAgB,iBAAiB,SAAiB,MAAM,KAAK,IAAI,GAAW;CAC1E,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,OAAO,MAAM,WAAW,GAAI,CAAC;CAC9D,IAAI,UAAU,IACZ,OAAO,GAAG,QAAQ,MAAM,YAAY,IAAI,KAAK,IAAI;CAEnD,IAAI,UAAU,MAAM;EAClB,MAAM,UAAU,KAAK,MAAM,UAAU,EAAE;EACvC,OAAO,GAAG,QAAQ,MAAM,YAAY,IAAI,KAAK,IAAI;CACnD;CACA,MAAM,QAAQ,KAAK,MAAM,UAAU,IAAI;CACvC,OAAO,GAAG,MAAM,OAAO,UAAU,IAAI,KAAK,IAAI;AAChD;AAEA,SAAgB,iBAAiB,OAAgB,MAAM,IAAY;CACjE,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;CAET,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,IAAI;EACV,MAAM,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE;EACzF,IAAI,OAAO,YAAY,UACrB,OAAO,QAAQ,SAAS,MAAM,QAAQ,MAAM,GAAG,MAAM,CAAC,IAAI,MAAM;CAEpE;CACA,MAAM,OAAO,KAAK,UAAU,KAAK,KAAK;CACtC,OAAO,KAAK,SAAS,MAAM,KAAK,MAAM,GAAG,MAAM,CAAC,IAAI,MAAM;AAC5D;AAEA,MAAM,iBAAoE;CACxE,KAAK;EAAE,MAAM;EAAO,QAAQ;CAAI;CAChC,UAAU,EAAE,MAAM,WAAW;CAC7B,KAAK,EAAE,MAAM,MAAM;CACnB,MAAM,EAAE,MAAM,OAAO;CACrB,MAAM,EAAE,MAAM,OAAO;CACrB,OAAO,EAAE,MAAM,QAAQ;CACvB,SAAS,EAAE,MAAM,UAAU;CAC3B,MAAM,EAAE,MAAM,OAAO;AACvB;AAEA,SAAgB,cAAc,IAA4C;CACxE,IAAI,CAAC,IACH;CAGF,MAAM,SADiB,GAAG,MAAM,GAAG,CAAC,CAAC,MAAM,GAAA,CACd,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;CACpE,IAAI,MAAM,OAAO,UACf,MAAM,MAAM;CAEd,MAAM,cAAc,MAAM,MAAM;CAChC,IAAI,CAAC,aACH,OAAO;CAET,MAAM,SAAS,eAAe;CAC9B,MAAM,OACJ,QAAQ,SAEP,SAAS,KAAK,WAAW,IAAI,cAAc,GAAG,YAAY,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,YAAY,MAAM,CAAC;CAE1G,MAAM,UAAoB,CAAC;CAC3B,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,UAAU,KAAK,IAAI,GACrB;EAEF,IAAI,gBAAgB,KAAK,IAAI,GAC3B,QAAQ,KAAK,IAAI;OAEjB,MAAM,KAAK,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,GAAG;CAEhE;CAEA,OAAO,CADW,QAAQ,SAAS,IAAI,GAAG,OAAO,QAAQ,UAAU,MAAM,QAAQ,KAAK,GAAG,MAAM,MAC5E,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG;AACvC;;;AC9LA,MAAM,cAAyD;CAC7D,UAAU;EAAE,MAAM;EAAgB,OAAO;EAAY,UAAU;CAAO;CACtE,SAAS;EAAE,MAAM;EAAgB,OAAO;EAAW,UAAU;CAAO;CACpE,mBAAmB;EAAE,MAAM;EAAW,OAAO;EAAkB,UAAU;CAAU;CACnF,MAAM;EAAE,MAAM;EAAS,OAAO;EAAQ,UAAU;CAAO;CACvD,QAAQ;EAAE,MAAM;EAAe,OAAO;EAAU,UAAU;CAAO;CACjE,QAAQ;EAAE,MAAM;EAAS,OAAO;EAAU,UAAU;CAAQ;CAC5D,QAAQ;EAAE,MAAM;EAAgB,OAAO;EAAU,UAAU;CAAO;AACpE;AAEA,SAAgB,mBAAmB,QAAwD;CACzF,IAAI,CAAC,QACH,OAAO;EAAE,MAAM;EAAS,OAAO;EAAe,UAAU;CAAO;CAEjE,IAAI,OAAO,eAAe,WACxB,OAAO;EAAE,MAAM;EAAoB,OAAO;EAAW,UAAU;CAAQ;CAEzE,IAAI,OAAO,eAAe,gBACxB,OAAO;EAAE,MAAM;EAAa,OAAO;EAAiB,UAAU;CAAU;CAE1E,OAAO,YAAY,OAAO,WAAW;EAAE,MAAM;EAAS,OAAO,OAAO;EAAQ,UAAU;CAAO;AAC/F;AAEA,SAAgB,cAAc,KAAyC;CACrE,IAAI,QAAQ,KAAA,GACV,OAAO;CAET,IAAI,OAAO,IACT,OAAO;CAET,IAAI,OAAO,IACT,OAAO;CAET,OAAO;AACT;AAIA,SAAgB,YACd,YACA,MACkD;CAClD,IAAI,CAAC,YACH;CAEF,IAAI,SAAS,WAAW;EACtB,MAAM,OAAO,WAAW;EACxB,OAAO,OAAO;GAAE,KAAK;GAAa;EAAK,IAAI,KAAA;CAC7C;CACA,IAAI,SAAS,UAAU;EACrB,MAAM,OAAO,WAAW;EACxB,OAAO,OAAO;GAAE,KAAK;GAAa;EAAK,IAAI,KAAA;CAC7C;CAIA,OAAO,eAHQ,OAAO,YACpB,OAAO,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,WAAW,YAAY,KAAK,QAAQ,sBAAsB,CAElF,CAAC;AAC9B;AAEA,SAAgB,eAAe,YAAyG;CACtI,MAAM,UAAU,OAAO,QAAQ,cAAc,CAAC,CAAC;CAC/C,IAAI,QAAQ,WAAW,GACrB;CAEF,IAAI;CACJ,KAAK,MAAM,CAAC,KAAK,SAAS,SAQxB,KAPa,KAAK,WAAW,aAAa,OAAO,oBAAqB,KAAK,eAAe,OAExF,SAAS,KAAA,IACL,OAAO,oBACP,KAAK,KAAK,WAAW,aACnB,OAAO,oBACN,KAAK,KAAK,eAAe,KAEhC,OAAO;EAAE;EAAK;CAAK;CAGvB,OAAO;AACT;AAEA,SAAgB,YAAY,KAAqB;CAC/C,IAAI,QAAQ,aACV,OAAO;CAET,IAAI,QAAQ,aACV,OAAO;CAGT,MAAM,SADS,IAAI,WAAW,YAAY,IAAI,IAAI,MAAM,EAAmB,IAAI,IAAA,CAC1D,WAAW,KAAK,GAAG;CACxC,OAAO,MAAM,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,MAAM,MAAM,CAAC;AACtD;AAIA,SAAgB,aAAa,QAA4D;CACvF,MAAM,KAAK,QAAQ;CACnB,IAAI,CAAC,IACH;CAEF,MAAM,QAAQ,UAAkB,MAAM,QAAQ,WAAW,EAAE;CAC3D,MAAM,SAAS,KAAK,EAAE;CACtB,OAAO,OAAO,OAAO,MAAM,MAAM,KAAK,EAAE,KAAK,MAAM,UAAW,EAAE,iBAAiB,KAAK,EAAE,aAAa,MAAM,MAAO;AACpH;AAEA,SAAgB,WAAW,QAA2C;CACpE,IAAI,CAAC,QAAQ,OACX,OAAO;CAET,OAAO,aAAa,MAAM,CAAC,EAAE,eAAe,OAAO;AACrD;AAEA,SAAgB,gBAAgB,OAA2D;CACzF,OAAO,cAAc,OAAO,UAAU;AACxC;AAEA,SAAgB,gBAAgB,KAAiC;CAC/D,MAAM,WAAW,cAAc,GAAG;CAClC,OAAO,aAAa,UAAU,gBAAgB,aAAa,YAAY,iBAAiB;AAC1F;;;;;;AAOA,SAAgB,eAAe,KAAqB;CAClD,OAAO,OAAO,KAAK,cAAc,OAAO,KAAK,eAAe;AAC9D"}
@@ -1,5 +1,5 @@
1
- import { Bt as Input, Gt as cn, R as EntryIcon, Ut as Button, bt as TooltipProvider, ht as Spinner, pt as Splitter, t as SessionPanel, vt as Tip } from "./SessionPanel-I5QSLLb0.mjs";
2
- import { f as formatBytes } from "./status-Dm9INVTt.mjs";
1
+ import { Bt as Input, Gt as cn, R as EntryIcon, Ut as Button, bt as TooltipProvider, ht as Spinner, pt as Splitter, t as SessionPanel, vt as Tip } from "./SessionPanel-DVhnUwXw.mjs";
2
+ import { p as formatBytes } from "./status-BW2xj79g.mjs";
3
3
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { ChevronRight, File, FileWarning, Folder, FolderOpen, PanelLeftClose, PanelLeftOpen, RefreshCw, Search, TriangleAlert, X } from "lucide-react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerdeck/ui",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -39,9 +39,9 @@
39
39
  "streamdown": "^2.6.0",
40
40
  "tailwind-merge": "^3.6.0",
41
41
  "use-stick-to-bottom": "^1.1.6",
42
- "@workerdeck/client": "1.1.0",
43
- "@workerdeck/react": "1.1.0",
44
- "@workerdeck/protocol": "1.1.0"
42
+ "@workerdeck/react": "1.2.0",
43
+ "@workerdeck/client": "1.2.0",
44
+ "@workerdeck/protocol": "1.2.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "monaco-editor": "^0.56.0",
@@ -1,7 +1,7 @@
1
1
  import type { MessageAttachment } from '@workerdeck/protocol'
2
2
  import type { TranscriptItem } from '@workerdeck/react'
3
3
  import { cn } from '../../lib/utils.ts'
4
- import { formatCost, formatDuration, formatRelativeTime } from '../../lib/format.ts'
4
+ import { COMPACTION_TEXT, formatCost, formatDuration, formatRelativeTime } from '../../lib/format.ts'
5
5
  import { FileCard } from './FileCard.tsx'
6
6
  import { Message, MessageContent } from './Message.tsx'
7
7
  import { PromptTokenText } from './PromptTokenText.tsx'
@@ -99,6 +99,9 @@ export function TranscriptItemView({
99
99
  case 'notice': {
100
100
  return <NoticeRow item={item} />
101
101
  }
102
+ case 'compaction': {
103
+ return <CompactionRow />
104
+ }
102
105
  case 'file_delivered': {
103
106
  return <FileCard item={item} href={fileUrl?.(item.path)} />
104
107
  }
@@ -108,6 +111,21 @@ export function TranscriptItemView({
108
111
  }
109
112
  }
110
113
 
114
+ /**
115
+ * The cards spelling of a compaction boundary — a rule with the label through it, the same shape
116
+ * `RecapRow` uses below, because both are boundaries rather than messages. The terminal spelling is
117
+ * `terminal/items.tsx`'s `CompactionRow`, one grid row, and the two share `COMPACTION_TEXT`.
118
+ */
119
+ function CompactionRow() {
120
+ return (
121
+ <div data-slot="compaction" className="flex items-center gap-2 py-1">
122
+ <div className="h-px flex-1 bg-border" />
123
+ <span className="font-mono text-label text-fg-3">≡ {COMPACTION_TEXT}</span>
124
+ <div className="h-px flex-1 bg-border" />
125
+ </div>
126
+ )
127
+ }
128
+
111
129
  export function RecapRow({ line, since, terminal }: { line: string; since?: number; terminal?: boolean }) {
112
130
  const away = since === undefined ? undefined : formatRelativeTime(since)
113
131
  const text = away ? `${line} · last here ${away}` : line
@@ -3,7 +3,7 @@ import type { TranscriptItem, TranscriptState } from '@workerdeck/react'
3
3
  import { cn } from '../../lib/utils.ts'
4
4
  import type { TerminalAffordances } from './affordances.tsx'
5
5
  import { OpenSubagentAction, WithActions } from './affordances.tsx'
6
- import { AssistantRow, FileRow, NoticeRow, ToolRunRow, ThinkingRow, ToolRow, TurnResultRow, UserRow, WorkingRow } from './items.tsx'
6
+ import { AssistantRow, CompactionRow, FileRow, NoticeRow, ToolRunRow, ThinkingRow, ToolRow, TurnResultRow, UserRow, WorkingRow } from './items.tsx'
7
7
  import { blockNeedsBlank, taskChildItems, terminalBlocks, type TaskBlock } from './blocks.ts'
8
8
  import { usePulse } from '../agent/pulse.tsx'
9
9
  import { Pressable, useRevealOnOpen } from './press.tsx'
@@ -41,6 +41,9 @@ export function TerminalItemView({ item, fileUrl }: { item: TranscriptItem; file
41
41
  case 'notice': {
42
42
  return <NoticeRow item={item} />
43
43
  }
44
+ case 'compaction': {
45
+ return <CompactionRow />
46
+ }
44
47
  case 'file_delivered': {
45
48
  return <FileRow item={item} href={fileUrl?.(item.path)} />
46
49
  }
@@ -1,6 +1,6 @@
1
1
  import type { FilePatch, PatchHunk } from '@workerdeck/protocol'
2
2
  import type { TranscriptItem } from '@workerdeck/react'
3
- import { formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
3
+ import { COMPACTION_TEXT, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
4
4
  import { taskChildItems, type TerminalBlock, type ToolCallItem } from './blocks.ts'
5
5
  import { IMAGE_BOX_LINES } from './image-box.ts'
6
6
  import { collapsedResult } from './result-preview.ts'
@@ -622,6 +622,11 @@ export function itemHeight(item: TranscriptItem, m: CellMetrics): ComputedHeight
622
622
  case 'notice': {
623
623
  return rowH(item.text, m, { extraPx })
624
624
  }
625
+ // Measured against the exported constant, not a copy of the sentence: the two must not drift,
626
+ // and a mismatch here is a scrubber that points a few pixels off for the rest of the session.
627
+ case 'compaction': {
628
+ return rowH(COMPACTION_TEXT, m, { extraPx })
629
+ }
625
630
  case 'file_delivered': {
626
631
  const text = `${item.path} · ${formatBytes(item.bytes)}` + (item.description ? ` · ${item.description}` : '')
627
632
  return rowH(text, m, { extraPx })
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import type { TranscriptItem } from '@workerdeck/react'
3
- import { formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
3
+ import { COMPACTION_TEXT, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
4
4
  import { isMutatingTool } from '../../lib/tool-icon.ts'
5
5
  import { usePulse } from '../agent/pulse.tsx'
6
6
  import { BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
@@ -278,6 +278,19 @@ export function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind:
278
278
  )
279
279
  }
280
280
 
281
+ /**
282
+ * One row, deliberately. A rule with a centred label reads better and costs the height book a
283
+ * second measurement for a row that carries no content — and the point here is only that the
284
+ * boundary is *visible*, so the ring dropping 90% → 15% stops reading as a bug.
285
+ */
286
+ export function CompactionRow() {
287
+ return (
288
+ <Row glyph="≡" glyphTone="yellow" tone="faint">
289
+ {COMPACTION_TEXT}
290
+ </Row>
291
+ )
292
+ }
293
+
281
294
  export function NoticeRow({ item }: { item: Extract<TranscriptItem, { kind: 'notice' }> }) {
282
295
  const error = item.level === 'error'
283
296
  return (
package/src/lib/format.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The whole payload of a `compaction` row. The wire event carries no summary because codex's
3
+ * `contextCompaction` item carries none, so the sentence *is* the content.
4
+ *
5
+ * It lives here, in a pure module, because the renderer (`terminal/items.tsx`) and the height book
6
+ * (`terminal/height.ts`) must measure and draw the same string — height.ts cannot import the
7
+ * `.tsx` without pulling React into a module the tests require to stay pure.
8
+ */
9
+ export const COMPACTION_TEXT = 'context compacted · earlier turns summarised to fit the window'
10
+
1
11
  export function formatCost(usd: number | undefined): string {
2
12
  if (usd === undefined || Number.isNaN(usd)) {
3
13
  return '—'