@workerdeck/ui 0.11.0 → 0.13.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.
Files changed (35) hide show
  1. package/build/{SessionPanel-_U8tjX29.mjs → SessionPanel-CKQa4i0Y.mjs} +337 -269
  2. package/build/SessionPanel-CKQa4i0Y.mjs.map +1 -0
  3. package/build/{SessionPanel-CyhygZx_.d.mts → SessionPanel-CZMA44NM.d.mts} +73 -2
  4. package/build/format.d.mts +65 -1
  5. package/build/format.mjs +118 -1
  6. package/build/format.mjs.map +1 -0
  7. package/build/index.d.mts +129 -10
  8. package/build/index.mjs +461 -5
  9. package/build/index.mjs.map +1 -1
  10. package/build/workspace.d.mts +36 -1
  11. package/build/workspace.mjs +16 -2
  12. package/build/workspace.mjs.map +1 -1
  13. package/package.json +4 -4
  14. package/src/components/agent/Composer.tsx +18 -10
  15. package/src/components/agent/EngineIcon.tsx +97 -0
  16. package/src/components/agent/Loader.tsx +11 -46
  17. package/src/components/agent/Message.tsx +10 -6
  18. package/src/components/agent/QuestionPrompt.tsx +1 -1
  19. package/src/components/agent/SessionBrowser.tsx +546 -0
  20. package/src/components/agent/SessionPanel.tsx +89 -14
  21. package/src/components/agent/SessionWorkspace.tsx +45 -0
  22. package/src/components/agent/StatusBar.tsx +9 -2
  23. package/src/components/agent/ToolCallCard.tsx +11 -2
  24. package/src/components/agent/Transcript.tsx +28 -11
  25. package/src/components/agent/line-prompt.tsx +2 -2
  26. package/src/components/agent/pulse.tsx +60 -0
  27. package/src/components/agent/transcript-variant.tsx +62 -0
  28. package/src/components/ui/Menu.tsx +1 -1
  29. package/src/components/ui/Select.tsx +1 -1
  30. package/src/components/ui/Tooltip.tsx +1 -1
  31. package/src/format.ts +1 -0
  32. package/src/index.ts +12 -0
  33. package/src/lib/status.ts +124 -0
  34. package/src/styles/theme.css +45 -15
  35. package/build/SessionPanel-_U8tjX29.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["AlertDialogPrimitive","SonnerToaster"],"sources":["../src/components/ui/Card.tsx","../src/components/ui/Textarea.tsx","../src/components/ui/AlertDialog.tsx","../src/components/ui/Sonner.tsx","../src/components/prompt-area/segment-helpers.ts","../src/components/agent/SessionList.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","/**\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 { 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"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["AlertDialogPrimitive","SonnerToaster"],"sources":["../src/components/ui/Card.tsx","../src/components/ui/Textarea.tsx","../src/components/ui/AlertDialog.tsx","../src/components/ui/Sonner.tsx","../src/components/prompt-area/segment-helpers.ts","../src/components/agent/SessionList.tsx","../src/components/agent/SessionBrowser.tsx","../src/components/agent/EngineIcon.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","/**\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 { 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 { useEffect, useMemo, useRef, useState } from 'react'\nimport {\n BellRing,\n CircleAlert,\n CircleSlash,\n Moon,\n PauseCircle,\n Pencil,\n Search,\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 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 { Input } from '../ui/Input.tsx'\nimport { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'\nimport { Spinner } from '../ui/Spinner.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 /** 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 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 emptyState,\n showControls = true,\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 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 <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 ...(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 ...(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>No sessions yet.</Empty>)\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.\n <Empty>\n {hasFacetFilter(config) ? (\n <>\n No sessions match.{' '}\n <button\n type='button'\n className='underline underline-offset-2 hover:text-fg-1'\n onClick={() => onConfigChange(clearFilters(config))}>\n Clear filters\n </button>\n </>\n ) : (\n 'No sessions here.'\n )}\n </Empty>\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-baseline gap-2 px-3 text-label font-medium text-fg-4'>\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}\n onSelect={onSelect}\n onDelete={onDelete}\n onRename={onRename}\n />\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\nfunction Empty({ children }: { children: React.ReactNode }) {\n return <div className='px-3 py-6 text-center text-body-sm text-fg-4'>{children}</div>\n}\n\ninterface SessionRowItemProps {\n row: SessionRow\n active?: boolean\n showGateway?: boolean\n onSelect?: (row: SessionRow) => void\n onDelete?: (row: SessionRow) => void\n onRename?: (row: SessionRow, title: string) => void\n}\n\nfunction SessionRowItem({\n row,\n active,\n showGateway,\n onSelect,\n onDelete,\n onRename,\n}: SessionRowItemProps) {\n const { info } = row\n const [editing, setEditing] = 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 const folder = info.cwd.split('/').filter(Boolean).pop() ?? info.cwd\n const details = [\n showGateway ? row.hostName : undefined,\n friendlyModel(info.model),\n folder,\n info.profile ? `@${info.profile}` : undefined,\n formatCost(info.totalCostUsd),\n ].filter(Boolean)\n\n return (\n <div\n data-slot='session-row'\n data-active={active || undefined}\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: what you scan the list by on the left, how it is doing on the\n right, state last. */}\n <div className='flex items-center gap-1.5'>\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 onClick={() => onSelect?.(row)}\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 <SessionStatusIcon row={row} />\n </div>\n\n {/* Line two: what it is, with the actions at the far right — away from the\n state icon, and away from the title you are actually reading. */}\n <div className='flex items-center gap-1 text-label text-fg-4'>\n <button\n type='button'\n onClick={() => !editing && onSelect?.(row)}\n className='min-w-0 flex-1 truncate text-left font-mono outline-none'>\n {details.join(' · ')}\n </button>\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={() => setEditing(true)}>\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={() => onDelete(row)}>\n <Trash2 className='size-3 text-fg-3' />\n </Button>\n ) : null}\n </div>\n </div>\n )\n}\n\n/**\n * State as one glyph on the right edge — a ringing bell when it wants a human, a\n * spinner while it works, a moon when it is only sleeping. Replaces the text\n * badge: in a sidebar the word costs more room than it earns, and the states\n * that matter are the two you can recognise without reading.\n */\nexport function SessionStatusIcon({ row }: { row: SessionRow }) {\n const { info } = row\n if (info.pendingPermissionCount > 0 || info.status === 'awaiting_approval') {\n return <BellRing className='size-3.5 shrink-0 animate-pulse text-warning' />\n }\n if (info.status === 'running' || info.status === 'starting') {\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","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\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"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { r as SessionPanelProps } from "./SessionPanel-CyhygZx_.mjs";
1
+ import { r as SessionPanelProps } from "./SessionPanel-CZMA44NM.mjs";
2
2
  import * as _$react from "react";
3
3
  import { CSSProperties } from "react";
4
4
  import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
@@ -11,10 +11,38 @@ interface SessionWorkspaceProps {
11
11
  /** Passed straight through to {@link SessionPanel} — including the render-prop
12
12
  * form that claims the session-actions menu. */
13
13
  header?: SessionPanelProps['header'];
14
+ /**
15
+ * Panel seams the workspace does not interpret, forwarded verbatim.
16
+ *
17
+ * They are listed rather than spread so the workspace stays explicit about
18
+ * what it passes on: the panel owns the session's one attach, and a seam that
19
+ * silently arrived here would be a second place to look for why a session
20
+ * renders the way it does.
21
+ */
22
+ transcriptVariant?: SessionPanelProps['transcriptVariant'];
23
+ transcriptDensity?: SessionPanelProps['transcriptDensity'];
24
+ /** Which end of the panel the status bar sits at — see `SessionPanel`. */
25
+ statusPlacement?: SessionPanelProps['statusPlacement'];
26
+ unseen?: SessionPanelProps['unseen'];
27
+ /** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
28
+ * {@link SessionPanel}; the file tree and editor are unaffected, since reading
29
+ * a run's files is the point of a read-only view. */
30
+ readOnly?: SessionPanelProps['readOnly'];
31
+ onVitals?: SessionPanelProps['onVitals'];
14
32
  /** Rail width in pixels on first render. */
15
33
  defaultRailWidth?: number;
16
34
  /** Start with the file rail collapsed even on a wide viewport. */
17
35
  defaultRailCollapsed?: boolean;
36
+ /**
37
+ * The rail moved. Paired with the two defaults so an embedder can persist the
38
+ * layout — the workspace deliberately does not, because *where* to keep it (a
39
+ * Memento, localStorage, a workspace file) is the embedder's call, and a
40
+ * component that picked one would be wrong in the other hosts.
41
+ */
42
+ onRailChange?: (rail: {
43
+ width: number;
44
+ collapsed: boolean;
45
+ }) => void;
18
46
  className?: string;
19
47
  }
20
48
  /**
@@ -42,8 +70,15 @@ declare function SessionWorkspace({
42
70
  client,
43
71
  sessionId,
44
72
  header,
73
+ transcriptVariant,
74
+ transcriptDensity,
75
+ statusPlacement,
76
+ unseen,
77
+ readOnly,
78
+ onVitals,
45
79
  defaultRailWidth,
46
80
  defaultRailCollapsed,
81
+ onRailChange,
47
82
  className
48
83
  }: SessionWorkspaceProps): _$react.JSX.Element;
49
84
  //#endregion
@@ -1,4 +1,4 @@
1
- import { $ as TooltipProvider, Ct as cn, J as Spinner, K as Splitter, Z as Tip, t as SessionPanel, vt as Input, xt as Button } from "./SessionPanel-_U8tjX29.mjs";
1
+ import { $ as Tip, Ct as Button, J as Splitter, Tt as cn, X as Spinner, bt as Input, t as SessionPanel, tt as TooltipProvider } from "./SessionPanel-CKQa4i0Y.mjs";
2
2
  import { n as formatBytes } from "./format-DqR56Y8l.mjs";
3
3
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -694,7 +694,7 @@ const EDITOR_MIN = 120;
694
694
  * The conditional children before it are `? :` expressions that leave a null in
695
695
  * their slot, which is exactly what keeps its index stable.
696
696
  */
697
- function SessionWorkspace({ client, sessionId, header, defaultRailWidth = 260, defaultRailCollapsed, className }) {
697
+ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, statusPlacement, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
698
698
  const { info } = useSessionInfo(client, sessionId);
699
699
  const cwd = info?.cwd;
700
700
  const tree = useHostFileTree(client, cwd);
@@ -710,6 +710,14 @@ function SessionWorkspace({ client, sessionId, header, defaultRailWidth = 260, d
710
710
  const wide = useIsWide();
711
711
  const [railCollapsed, setRailCollapsed] = useState(defaultRailCollapsed ?? false);
712
712
  const [railWidth, setRailWidth] = useState(defaultRailWidth);
713
+ const onRailChangeRef = useRef(onRailChange);
714
+ onRailChangeRef.current = onRailChange;
715
+ useEffect(() => {
716
+ onRailChangeRef.current?.({
717
+ width: railWidth,
718
+ collapsed: railCollapsed
719
+ });
720
+ }, [railWidth, railCollapsed]);
713
721
  const [editorHeight, setEditorHeight] = useState(360);
714
722
  const hasFiles = files.files.length > 0;
715
723
  const overlayRail = !wide;
@@ -800,6 +808,12 @@ function SessionWorkspace({ client, sessionId, header, defaultRailWidth = 260, d
800
808
  client,
801
809
  sessionId,
802
810
  header: hoisted,
811
+ transcriptVariant,
812
+ transcriptDensity,
813
+ statusPlacement,
814
+ unseen,
815
+ readOnly,
816
+ onVitals,
803
817
  className: "min-h-0 flex-1"
804
818
  })
805
819
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.mjs","names":[],"sources":["../src/components/agent/EditorTabs.tsx","../src/components/agent/FileTree.tsx","../src/components/agent/CodeEditor.tsx","../src/components/agent/FileViewer.tsx","../src/components/agent/SessionWorkspace.tsx"],"sourcesContent":["import { useEffect, useRef, type MouseEvent as ReactMouseEvent } from 'react'\nimport { isDirty, type OpenFile } from '@workerdeck/react'\nimport { X } from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Tip, TooltipProvider } from '../ui/Tooltip.tsx'\nimport { formatBytes } from '../../lib/format.ts'\n\nexport interface EditorTabsProps {\n files: OpenFile[]\n activePath?: string\n onActivate: (path: string) => void\n onClose: (path: string) => void\n className?: string\n}\n\n/**\n * The open-file tab strip.\n *\n * Hand-rolled rather than built on `@base-ui/react`'s `Tabs`: a VS Code tab\n * carries a close button, and a `<button>` inside a `<button>` is invalid HTML —\n * getting the primitive to render something else costs more than the roving\n * tabindex it would have provided. So that part is here, explicitly, along with\n * the two affordances that actually make a tab strip feel right: middle-click to\n * close, and the active tab scrolling itself into view.\n *\n * Deliberately state-free. Which files are open, which is focused and what\n * closing does are all decided by `useOpenFiles`.\n */\nexport function EditorTabs({ files, activePath, onActivate, onClose, className }: EditorTabsProps) {\n return (\n // Grouped, so moving along the strip shows each tab's path immediately\n // instead of re-serving the open delay on every tab.\n <TooltipProvider delay={500} closeDelay={0}>\n <div\n data-slot='editor-tabs'\n role='tablist'\n aria-label='Open files'\n className={cn(\n 'flex shrink-0 items-stretch overflow-x-auto border-b border-border bg-surface',\n className,\n )}>\n {files.map((file) => (\n <Tab\n key={file.path}\n file={file}\n active={file.path === activePath}\n onActivate={() => onActivate(file.path)}\n onClose={() => onClose(file.path)}\n onArrow={(direction) => {\n const index = files.findIndex((f) => f.path === file.path)\n const next = files[index + direction]\n if (next) onActivate(next.path)\n }}\n />\n ))}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction Tab({\n file,\n active,\n onActivate,\n onClose,\n onArrow,\n}: {\n file: OpenFile\n active: boolean\n onActivate: () => void\n onClose: () => void\n onArrow: (direction: 1 | -1) => void\n}) {\n const dirty = isDirty(file)\n const ref = useRef<HTMLDivElement>(null)\n // Opening a file from the tree can push the new tab off the end of the strip;\n // the point of opening it was to look at it. `nearest` scrolls the minimum, so\n // a tab already on screen stays exactly where it is.\n useEffect(() => {\n if (active) ref.current?.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n }, [active])\n\n const onAuxClick = (event: ReactMouseEvent) => {\n // Middle click. `auxclick` rather than `mousedown` so it matches how every\n // other middle-click target on the platform behaves.\n if (event.button !== 1) return\n event.preventDefault()\n onClose()\n }\n\n return (\n // The tab *is* the trigger (`render`), and it carries the full path — which\n // is why the viewer no longer spends a whole row on a line of monospace\n // nobody reads. No `title` alongside it: the browser's native tooltip would\n // show up underneath this one.\n <Tip\n side='bottom'\n render={\n <div\n ref={ref}\n role='tab'\n tabIndex={active ? 0 : -1}\n aria-selected={active}\n onClick={onActivate}\n onAuxClick={onAuxClick}\n onKeyDown={(event) => {\n if (event.key === 'ArrowRight') onArrow(1)\n else if (event.key === 'ArrowLeft') onArrow(-1)\n else if (event.key === 'Enter' || event.key === ' ') onActivate()\n else return\n event.preventDefault()\n }}\n className={cn(\n 'group flex min-w-0 shrink-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 py-1.5 transition-colors',\n 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent focus-visible:ring-inset',\n active ? 'bg-bg text-fg-1' : 'text-fg-3 hover:bg-surface-hover hover:text-fg-2',\n )}\n />\n }\n content={\n <span className='flex flex-col gap-0.5'>\n <span className='font-mono break-all'>{file.path}</span>\n {file.bytes !== undefined ? (\n <span className='text-fg-4'>{formatBytes(file.bytes)}</span>\n ) : null}\n </span>\n }>\n <span className={cn('max-w-40 truncate font-mono text-label', dirty && 'italic')}>\n {file.name}\n </span>\n {/* Errors are the one state the strip shows, because a failed tab\n otherwise looks identical to a loaded one until you focus it. */}\n {file.status === 'error' ? <span className='shrink-0 text-danger'>!</span> : null}\n <button\n type='button'\n aria-label={dirty ? `Close ${file.name} (unsaved changes)` : `Close ${file.name}`}\n onClick={(event) => {\n // Without this the click also activates the tab being closed, which\n // fights the reducer's focus-the-neighbour rule.\n event.stopPropagation()\n onClose()\n }}\n className={cn(\n 'shrink-0 rounded p-0.5 text-fg-4 transition-opacity hover:bg-surface-hover hover:text-fg-1',\n // Always reachable by keyboard and on touch; only *shown* on hover for\n // the tab you are pointing at, as VS Code does.\n active || dirty\n ? 'opacity-70'\n : 'opacity-0 group-hover:opacity-70 focus-visible:opacity-100',\n )}>\n {/* VS Code's move: a dirty tab shows a dot where the ✕ goes, and the ✕\n comes back when you point at it — so unsaved work is visible at rest\n without taking away the way to close it. */}\n <span className={cn('block', dirty && 'group-hover:hidden')}>\n {dirty ? <span className='block size-3 rounded-full bg-fg-2' /> : <X className='size-3' />}\n </span>\n {dirty ? (\n <span className='hidden group-hover:block'>\n <X className='size-3' />\n </span>\n ) : null}\n </button>\n </Tip>\n )\n}\n","import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'\nimport type { UseHostFileSearchResult, UseHostFileTreeResult } from '@workerdeck/react'\nimport type { HostDirEntry, HostFileMatch } from '@workerdeck/protocol'\nimport {\n ChevronRight,\n File,\n Folder,\n FolderOpen,\n Link2,\n PanelLeftClose,\n RefreshCw,\n Search,\n X,\n} from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Button } from '../ui/Button.tsx'\nimport { Input } from '../ui/Input.tsx'\nimport { Spinner } from '../ui/Spinner.tsx'\n\nexport interface FileTreeProps {\n /** Tree state from `useHostFileTree` — this component holds none of its own. */\n tree: UseHostFileTreeResult\n /** Optional search from `useHostFileSearch`; omit and the box is not offered. */\n search?: UseHostFileSearchResult\n /** Path of the focused file, highlighted in the tree. */\n activePath?: string\n onOpenFile: (path: string) => void\n /** Offered as a button in the header when given. */\n onCollapse?: () => void\n style?: CSSProperties\n className?: string\n}\n\n/** How far one level of nesting indents, in pixels. Inline rather than a Tailwind\n * class because depth is a number at runtime and `pl-${n}` is not a class. */\nconst INDENT = 12\n\n/**\n * The workspace's left rail: an expandable tree of the session's project,\n * with a search box over the same fuzzy route `@file` completion uses.\n *\n * Presentational by construction — every piece of state it renders comes from\n * the hooks in `@workerdeck/react`, and the only thing it owns is the search\n * query, which is the text in its own input.\n *\n * Searching replaces the tree with matches rather than filtering it: the route\n * answers with paths from all over the project, and threading those back into\n * tree positions would mean expanding a dozen directories to show six results.\n */\nexport function FileTree({\n tree,\n search,\n activePath,\n onOpenFile,\n onCollapse,\n style,\n className,\n}: FileTreeProps) {\n const [query, setQuery] = useState('')\n const [matches, setMatches] = useState<HostFileMatch[] | undefined>()\n const searching = query.trim().length > 0\n\n // Debounced, and only while there is something to search for — an empty box\n // means \"show me the tree again\", not \"match everything\".\n useEffect(() => {\n if (!search?.available) return\n const q = query.trim()\n if (!q) {\n setMatches(undefined)\n return\n }\n const controller = new AbortController()\n const timer = setTimeout(() => {\n void search.search(q, { limit: 60, signal: controller.signal }).then((found) => {\n if (!controller.signal.aborted) setMatches(found)\n })\n }, 150)\n return () => {\n controller.abort()\n clearTimeout(timer)\n }\n }, [search, query])\n\n return (\n <div\n data-slot='file-tree'\n style={style}\n className={cn('flex min-h-0 min-w-0 flex-col bg-surface', className)}>\n <div className='flex items-center gap-1 px-2 pt-2'>\n {search?.available ? (\n <div className='relative min-w-0 flex-1'>\n <Search className='absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-fg-4' />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder='Search files…'\n className='h-7 pr-7 pl-7 text-label'\n spellCheck={false}\n />\n {searching ? (\n <button\n type='button'\n onClick={() => setQuery('')}\n aria-label='Clear search'\n className='absolute top-1/2 right-1.5 -translate-y-1/2 text-fg-4 transition-colors hover:text-fg-2'>\n <X className='size-3.5' />\n </button>\n ) : null}\n </div>\n ) : (\n <span className='min-w-0 flex-1 truncate px-1 font-mono text-label text-fg-4'>\n {tree.root}\n </span>\n )}\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Refresh the file tree'\n onClick={() => tree.refresh()}>\n <RefreshCw className='size-3.5 text-fg-3' />\n </Button>\n {onCollapse ? (\n <Button variant='ghost' size='icon-sm' aria-label='Hide project files' onClick={onCollapse}>\n <PanelLeftClose className='size-3.5 text-fg-3' />\n </Button>\n ) : null}\n </div>\n\n <div className='min-h-0 flex-1 overflow-auto px-1 py-1'>\n {tree.error ? (\n <p className='px-2 py-3 text-body-sm text-danger'>{tree.error}</p>\n ) : searching ? (\n matches === undefined ? (\n <div className='py-6 text-center'>\n <Spinner className='size-4 text-fg-4' />\n </div>\n ) : matches.length === 0 ? (\n <p className='px-2 py-6 text-center text-body-sm text-fg-4'>No matching files.</p>\n ) : (\n <ul>\n {matches.map((match) => (\n <li key={match.path}>\n <Row\n label={fileName(match.relative)}\n // The directory, dimmed and truncated separately, so a deep\n // path cannot push the filename out of the row — a rail full\n // of `apps/ios/DerivedData/B…` says nothing about which file\n // each hit is.\n detail={directoryOf(match.relative)}\n title={match.relative}\n icon={<File className='size-3.5 shrink-0 text-fg-4' />}\n active={match.path === activePath}\n onClick={() => onOpenFile(match.path)}\n />\n </li>\n ))}\n </ul>\n )\n ) : tree.loading ? (\n <div className='py-6 text-center'>\n <Spinner className='size-4 text-fg-4' />\n </div>\n ) : tree.rows.length === 0 ? (\n <p className='px-2 py-6 text-center text-body-sm text-fg-4'>This project is empty.</p>\n ) : (\n <ul role='tree' aria-label='Project files'>\n {tree.rows.map((row) => (\n <li key={row.entry.path} role='treeitem' aria-expanded={row.expanded} aria-level={row.depth + 1}>\n <Row\n label={row.entry.name}\n indent={row.depth}\n active={row.entry.path === activePath}\n icon={\n row.entry.type === 'dir' ? (\n row.loading ? (\n <Spinner className='size-3.5 shrink-0 text-fg-4' />\n ) : row.expanded ? (\n <FolderOpen className='size-3.5 shrink-0 text-accent' />\n ) : (\n <Folder className='size-3.5 shrink-0 text-accent' />\n )\n ) : (\n <EntryIcon type={row.entry.type} />\n )\n }\n chevron={\n row.entry.type === 'dir' ? (\n <ChevronRight\n className={cn(\n 'size-3 shrink-0 text-fg-4 transition-transform',\n row.expanded && 'rotate-90',\n )}\n />\n ) : undefined\n }\n onClick={() =>\n row.entry.type === 'dir'\n ? tree.toggle(row.entry.path)\n : onOpenFile(row.entry.path)\n }\n />\n {row.truncated ? (\n <p\n className='truncate py-0.5 text-label text-fg-4'\n style={{ paddingLeft: (row.depth + 1) * INDENT + 22 }}>\n More entries than the server will return — use search.\n </p>\n ) : null}\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n )\n}\n\n/** One clickable line. A directory and a file differ only in what the click does\n * and whether there is a chevron — visually they are the same row. */\nfunction Row({\n label,\n detail,\n title,\n icon,\n chevron,\n indent = 0,\n active,\n onClick,\n}: {\n label: string\n /** Secondary text after the label, dimmed and the first thing to be truncated. */\n detail?: string\n title?: string\n icon: ReactNode\n chevron?: ReactNode\n indent?: number\n active?: boolean\n onClick: () => void\n}) {\n // Scroll a row that became active elsewhere (a search hit, a `reveal`) into\n // view, but never yank the list while someone is reading it — `nearest` moves\n // the minimum and does nothing when the row is already visible.\n const ref = useRef<HTMLButtonElement>(null)\n useEffect(() => {\n if (active) ref.current?.scrollIntoView({ block: 'nearest' })\n }, [active])\n\n return (\n <button\n ref={ref}\n type='button'\n onClick={onClick}\n title={title ?? label}\n style={{ paddingLeft: indent * INDENT + 4 }}\n className={cn(\n 'flex w-full items-center gap-1 rounded py-1 pr-2 text-left transition-colors',\n active ? 'bg-surface-hover text-fg-1' : 'text-fg-2 hover:bg-surface-hover',\n )}>\n <span className='flex size-3 shrink-0 items-center justify-center'>{chevron}</span>\n {icon}\n {/* `shrink-0` on the name and `min-w-0` on the detail: when the row runs\n out of room the directory gives way and the filename stays whole. */}\n <span className='shrink-0 truncate font-mono text-label'>{label}</span>\n {detail ? (\n <span className='min-w-0 flex-1 truncate font-mono text-label text-fg-4'>{detail}</span>\n ) : null}\n </button>\n )\n}\n\n/** Last segment of a relative match. */\nfunction fileName(relative: string): string {\n return relative.slice(relative.lastIndexOf('/') + 1)\n}\n\n/** Everything before it, or `undefined` for a file at the search root. */\nfunction directoryOf(relative: string): string | undefined {\n const cut = relative.lastIndexOf('/')\n return cut === -1 ? undefined : relative.slice(0, cut)\n}\n\n/** A symlink is reported as itself and never silently resolved — following it is\n * the next request's problem, and that request is refused if it escapes the roots. */\nfunction EntryIcon({ type }: { type: HostDirEntry['type'] }) {\n if (type === 'symlink') return <Link2 className='size-3.5 shrink-0 text-fg-4' />\n return <File className='size-3.5 shrink-0 text-fg-4' />\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type * as Monaco from 'monaco-editor'\nimport { cn } from '../../lib/utils.ts'\nimport { Spinner } from '../ui/Spinner.tsx'\n\nexport interface CodeEditorProps {\n /** Absolute path — decides the language and identifies the model. */\n path: string\n /** Text to show. Applied to the model when it differs from what is on screen,\n * so an external reload lands without fighting the user's cursor. */\n value: string\n onChange?: (value: string) => void\n /** Ctrl/Cmd+S. Wired inside Monaco because the editor swallows keydown. */\n onSave?: () => void\n readOnly?: boolean\n className?: string\n}\n\n/**\n * Monaco — VS Code's own editor — behind a small React surface.\n *\n * Two deliberate choices, both about `@workerdeck/ui` being a **published\n * library** rather than an app:\n *\n * 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a\n * separate chunk that arrives when someone first opens a file. A dashboard\n * that never opens one never pays for it, and Monaco's ~90 language grammars\n * are themselves lazy (each `registerLanguage` carries an `import()` loader),\n * so opening a `.ts` file fetches the TypeScript grammar and nothing else.\n * 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers\n * in a library become every embedder's bootstrapping problem, and\n * `packages/web` ships prebuilt static files at a domain root, which is\n * exactly where hardcoded worker URLs break. The editor is configured so it\n * never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,\n * no diff editor. A host that wants the worker-backed language services\n * (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`\n * itself before the first file is opened — Monaco is a singleton and nothing\n * here fights that.\n *\n * One model per path, kept across tab switches, so undo history and view state\n * survive clicking away and back — which is most of what makes tabs feel like\n * tabs rather than like re-opening a file.\n */\nexport function CodeEditor({\n path,\n value,\n onChange,\n onSave,\n readOnly,\n className,\n}: CodeEditorProps) {\n const host = useRef<HTMLDivElement>(null)\n const editor = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null)\n const monaco = useRef<typeof Monaco | null>(null)\n const [ready, setReady] = useState(false)\n const theme = useDocumentTheme()\n\n // Callbacks through refs: they change identity every render, and re-creating\n // the editor (or re-registering its listeners) on each one would drop the\n // cursor mid-keystroke.\n const handlers = useRef({ onChange, onSave })\n handlers.current = { onChange, onSave }\n\n useEffect(() => {\n let disposed = false\n void loadMonaco().then((api) => {\n if (disposed || !host.current) return\n monaco.current = api\n const instance = api.editor.create(host.current, {\n value,\n language: languageOf(path),\n readOnly,\n automaticLayout: true,\n theme: monacoTheme(document.documentElement.getAttribute('data-theme')),\n minimap: { enabled: false },\n scrollBeyondLastLine: false,\n fontSize: 12,\n lineHeight: 20,\n fontFamily:\n 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace',\n renderLineHighlight: 'line',\n smoothScrolling: true,\n padding: { top: 8, bottom: 8 },\n scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },\n // Without a worker there is no word-based suggestion provider to ask, so\n // asking would surface an empty popup on every identifier.\n wordBasedSuggestions: 'off',\n quickSuggestions: false,\n })\n editor.current = instance\n instance.onDidChangeModelContent(() => {\n handlers.current.onChange?.(instance.getValue())\n })\n instance.addCommand(api.KeyMod.CtrlCmd | api.KeyCode.KeyS, () => {\n handlers.current.onSave?.()\n })\n setReady(true)\n })\n return () => {\n disposed = true\n // Dispose the editor but NOT its model — the model is keyed by path and\n // outlives this mount so that reopening a tab restores its undo stack.\n editor.current?.dispose()\n editor.current = null\n setReady(false)\n }\n // Deliberately created once, with no dependencies. Path, value and readOnly\n // are applied by the effects below instead, because re-creating the editor\n // to change any of them would lose the cursor, the scroll position and the\n // undo history.\n }, [])\n\n // Swap the model when the focused file changes. One model per path, created\n // lazily and kept, so each tab keeps its own undo history and view state.\n useEffect(() => {\n const api = monaco.current\n const instance = editor.current\n if (!api || !instance || !ready) return\n const uri = api.Uri.parse(`workerdeck://host${path}`)\n const model = api.editor.getModel(uri) ?? api.editor.createModel(value, languageOf(path), uri)\n if (instance.getModel() !== model) instance.setModel(model)\n }, [path, ready, value])\n\n // Apply an external change — a reload from disk, a revert — without\n // disturbing anything when the text already matches, which is the common case\n // because most changes to `value` are echoes of the user's own typing.\n useEffect(() => {\n const instance = editor.current\n if (!instance || !ready) return\n const model = instance.getModel()\n if (!model || model.getValue() === value) return\n // `pushEditOperations` rather than `setValue` so the replacement joins the\n // undo stack instead of clearing it.\n model.pushEditOperations(\n [],\n [{ range: model.getFullModelRange(), text: value }],\n () => null,\n )\n }, [value, ready])\n\n useEffect(() => {\n if (ready) editor.current?.updateOptions({ readOnly })\n }, [readOnly, ready])\n\n // The theme is global to Monaco, not per-editor — `setTheme` is on the\n // namespace for that reason.\n useEffect(() => {\n if (ready) monaco.current?.editor.setTheme(monacoTheme(theme))\n }, [theme, ready])\n\n return (\n <div className={cn('relative min-h-0 min-w-0 flex-1', className)}>\n <div ref={host} className='absolute inset-0' />\n {!ready ? (\n <div className='absolute inset-0 flex items-center justify-center bg-bg'>\n <Spinner className='size-4 text-fg-4' />\n </div>\n ) : null}\n </div>\n )\n}\n\n/**\n * Follow the `data-theme` the design tokens already swap on, so the editor is\n * never the one light rectangle in a dark app (or the reverse). An attribute\n * observer rather than a media query: the app has a manual toggle, and the\n * attribute is what that toggle writes.\n */\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = useState<string | null>(() =>\n typeof document === 'undefined' ? null : document.documentElement.getAttribute('data-theme'),\n )\n useEffect(() => {\n const root = document.documentElement\n const sync = () => setTheme(root.getAttribute('data-theme'))\n sync()\n const observer = new MutationObserver(sync)\n observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => observer.disconnect()\n }, [])\n return theme\n}\n\n/** An unset attribute means the host never opted into the token themes, in which\n * case dark matches this package's default surface. */\nfunction monacoTheme(theme: string | null): string {\n return theme === 'light' ? 'wd-light' : 'wd-dark'\n}\n\n/**\n * Load Monaco once, register the themes, and hand back the API.\n *\n * Cached as a promise rather than a value so that several editors mounting in\n * the same frame share one load instead of racing three of them.\n */\nlet monacoPromise: Promise<typeof Monaco> | undefined\nfunction loadMonaco(): Promise<typeof Monaco> {\n monacoPromise ??= (async () => {\n const api = await import('monaco-editor')\n // Themes that inherit the surrounding surface instead of Monaco's own\n // near-black, so the editor does not sit in the layout as a differently\n // coloured rectangle. Both are defined; the CSS variable decides nothing\n // here, so the panel picks by `prefers-color-scheme` on the document.\n api.editor.defineTheme('wd-dark', {\n base: 'vs-dark',\n inherit: true,\n rules: [],\n colors: { 'editor.background': '#00000000' },\n })\n api.editor.defineTheme('wd-light', {\n base: 'vs',\n inherit: true,\n rules: [],\n colors: { 'editor.background': '#00000000' },\n })\n return api\n })()\n return monacoPromise\n}\n\n/**\n * Monaco's language id for a path.\n *\n * By extension, with a short table for the files that have none — Monaco's own\n * registry is keyed on extensions it knows, and an unmatched file falls through\n * to `plaintext`, which is the honest answer rather than a guess.\n */\nfunction languageOf(path: string): string {\n const name = path.slice(path.lastIndexOf('/') + 1)\n const byName = FILENAME_LANGUAGES[name.toLowerCase()]\n if (byName) return byName\n const extension = name.slice(name.lastIndexOf('.') + 1).toLowerCase()\n return EXTENSION_LANGUAGES[extension] ?? 'plaintext'\n}\n\n/** Files whose type is their whole name. */\nconst FILENAME_LANGUAGES: Record<string, string> = {\n dockerfile: 'dockerfile',\n makefile: 'plaintext',\n '.gitignore': 'plaintext',\n '.env': 'shell',\n}\n\n/** Extension → Monaco language id. Only where the id differs from the extension\n * or the extension is ambiguous; everything else Monaco resolves itself. */\nconst EXTENSION_LANGUAGES: Record<string, string> = {\n ts: 'typescript',\n tsx: 'typescript',\n mts: 'typescript',\n cts: 'typescript',\n js: 'javascript',\n jsx: 'javascript',\n mjs: 'javascript',\n cjs: 'javascript',\n json: 'json',\n jsonc: 'json',\n md: 'markdown',\n mdx: 'markdown',\n css: 'css',\n scss: 'scss',\n less: 'less',\n html: 'html',\n htm: 'html',\n xml: 'xml',\n svg: 'xml',\n yml: 'yaml',\n yaml: 'yaml',\n toml: 'plaintext',\n sh: 'shell',\n bash: 'shell',\n zsh: 'shell',\n fish: 'shell',\n py: 'python',\n rb: 'ruby',\n rs: 'rust',\n go: 'go',\n swift: 'swift',\n java: 'java',\n kt: 'kotlin',\n c: 'c',\n h: 'c',\n cpp: 'cpp',\n cc: 'cpp',\n hpp: 'cpp',\n cs: 'csharp',\n php: 'php',\n sql: 'sql',\n graphql: 'graphql',\n gql: 'graphql',\n lua: 'lua',\n r: 'r',\n pl: 'perl',\n dart: 'dart',\n scala: 'scala',\n ini: 'ini',\n cfg: 'ini',\n conf: 'ini',\n txt: 'plaintext',\n log: 'plaintext',\n}\n","import type { ReactNode } from 'react'\nimport type { OpenFile } from '@workerdeck/react'\nimport { currentText, isDirty } from '@workerdeck/react'\nimport { FileWarning, TriangleAlert } from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Button } from '../ui/Button.tsx'\nimport { Spinner } from '../ui/Spinner.tsx'\nimport { CodeEditor } from './CodeEditor.tsx'\n\nexport interface FileViewerProps {\n file: OpenFile | undefined\n /** From `/fs/roots`. False renders the editor read-only rather than letting\n * someone type into a file this gateway will refuse to write. */\n canWrite?: boolean\n onChange?: (path: string, content: string) => void\n onSave?: (path: string) => void\n /** Discard this tab's edits — local only, no re-read. */\n onRevert?: (path: string) => void\n /** Take the version on disk, discarding this tab's edits. */\n onReload?: (path: string) => void\n /** Take this tab's version, over whatever is on disk now. */\n onOverwrite?: (path: string) => void\n onDismissConflict?: (path: string) => void\n className?: string\n}\n\n/**\n * The focused file: Monaco, plus the states a file can be in that are not\n * \"here is some text\".\n *\n * No path row — the tab's tooltip carries the path and the size, and a line of\n * monospace above every file is chrome that never earns its height.\n */\nexport function FileViewer({\n file,\n canWrite,\n onChange,\n onSave,\n onRevert,\n onReload,\n onOverwrite,\n onDismissConflict,\n className,\n}: FileViewerProps) {\n if (!file) return null\n\n return (\n <div\n data-slot='file-viewer'\n className={cn('flex min-h-0 min-w-0 flex-1 flex-col bg-bg', className)}>\n {/* The one failure with a choice attached, so it gets a bar rather than a\n toast: the agent rewrote this file while it was open, and which\n version wins is not something to decide on the user's behalf. */}\n {file.conflict ? (\n <div className='flex shrink-0 flex-wrap items-center gap-2 border-b border-warning/40 bg-warning-bg px-3 py-2'>\n <TriangleAlert className='size-3.5 shrink-0 text-warning' />\n <span className='min-w-0 flex-1 text-body-sm text-warning'>\n This file changed on disk since you opened it.\n </span>\n <Button variant='outline' size='xs' onClick={() => onReload?.(file.path)}>\n Use the version on disk\n </Button>\n <Button variant='outline' size='xs' onClick={() => onOverwrite?.(file.path)}>\n Keep mine\n </Button>\n <Button variant='ghost' size='xs' onClick={() => onDismissConflict?.(file.path)}>\n Dismiss\n </Button>\n </div>\n ) : file.saveError ? (\n <div className='flex shrink-0 items-center gap-2 border-b border-danger/40 bg-danger-bg px-3 py-2'>\n <TriangleAlert className='size-3.5 shrink-0 text-danger' />\n <span className='min-w-0 flex-1 text-body-sm text-danger'>{file.saveError}</span>\n <Button variant='ghost' size='xs' onClick={() => onDismissConflict?.(file.path)}>\n Dismiss\n </Button>\n </div>\n ) : null}\n\n {file.status === 'loading' ? (\n <Centred>\n <Spinner className='size-4 text-fg-4' />\n </Centred>\n ) : file.status === 'error' ? (\n <Centred>\n <TriangleAlert className='size-4 text-danger' />\n <p className='text-body-sm text-danger'>{file.error}</p>\n </Centred>\n ) : file.status === 'binary' ? (\n <Centred>\n <FileWarning className='size-4 text-fg-4' />\n <p className='text-body-sm text-fg-4'>This file isn’t text.</p>\n {/* Said out loud, because \"can't show it\" and \"editing it here would\n destroy it\" are different reassurances. */}\n <p className='text-label text-fg-4'>It can’t be edited here without corrupting it.</p>\n </Centred>\n ) : (\n <CodeEditor\n path={file.path}\n value={currentText(file)}\n readOnly={!canWrite}\n onChange={onChange ? (content) => onChange(file.path, content) : undefined}\n onSave={onSave ? () => onSave(file.path) : undefined}\n />\n )}\n\n {/* A status strip only while there is something to say. Saving is fast\n enough that a permanent row would mostly be blank. */}\n {file.status === 'ready' && (file.saving || isDirty(file) || !canWrite) ? (\n <div className='flex shrink-0 items-center gap-2 border-t border-border px-3 py-1'>\n {file.saving ? (\n <>\n <Spinner className='size-3 text-fg-4' />\n <span className='text-label text-fg-4'>Saving…</span>\n </>\n ) : !canWrite ? (\n <span className='text-label text-fg-4'>\n Read-only — this gateway doesn’t allow writes.\n </span>\n ) : (\n <>\n <span className='text-label text-fg-3'>Unsaved changes</span>\n <span className='flex-1' />\n {/* Revert, not reload: discard *my* edits and go back to what\n this tab read. Re-reading is the conflict bar's job, and it is\n a different question. */}\n <Button variant='ghost' size='xs' onClick={() => onRevert?.(file.path)}>\n Revert\n </Button>\n <Button variant='outline' size='xs' onClick={() => onSave?.(file.path)}>\n Save\n <span className='ml-1 text-fg-4'>⌘S</span>\n </Button>\n </>\n )}\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction Centred({ children }: { children: ReactNode }) {\n return (\n <div className='flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-6'>\n {children}\n </div>\n )\n}\n","import { useCallback, useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { WorkerDeckClient } from '@workerdeck/client'\nimport {\n useHostFileRoots,\n useHostFileSearch,\n useHostFileTree,\n useOpenFiles,\n useSessionInfo,\n isDirty,\n} from '@workerdeck/react'\nimport { PanelLeftOpen } from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Button } from '../ui/Button.tsx'\nimport { Splitter } from '../ui/Splitter.tsx'\nimport { EditorTabs } from './EditorTabs.tsx'\nimport { FileTree } from './FileTree.tsx'\nimport { FileViewer } from './FileViewer.tsx'\nimport { SessionPanel, type SessionPanelProps } from './SessionPanel.tsx'\n\nexport interface SessionWorkspaceProps {\n client: WorkerDeckClient\n sessionId: string | undefined\n /** Passed straight through to {@link SessionPanel} — including the render-prop\n * form that claims the session-actions menu. */\n header?: SessionPanelProps['header']\n /** Rail width in pixels on first render. */\n defaultRailWidth?: number\n /** Start with the file rail collapsed even on a wide viewport. */\n defaultRailCollapsed?: boolean\n className?: string\n}\n\nconst RAIL_MIN = 180\nconst RAIL_MAX = 520\n/** How little of the agent column the editor may leave. Below this the composer\n * and a line of transcript stop fitting together, which is the point at which the\n * agent has stopped being usable rather than merely small. */\nconst AGENT_MIN = 220\nconst EDITOR_MIN = 120\n\n/**\n * A VS Code-shaped workspace around a live session: file tree on the left, open\n * files above, the agent below.\n *\n * **Strictly additive.** {@link SessionPanel} is untouched and still the whole\n * session surface on its own — an embedder picks one or the other, and one that\n * has its own file tree keeps using the panel.\n *\n * Two things here are load-bearing and easy to break:\n *\n * 1. **The editor region is absent from the layout when nothing is open**, not\n * collapsed to zero height. A zero-height pane leaves a draggable splitter and\n * parks the composer at an odd offset; absence is what makes the agent's\n * \"claims the full column\" state actually look like the panel alone.\n * 2. **`SessionPanel` keeps its position in the tree across that transition.** It\n * holds the WebSocket attach and the entire transcript, so moving it between\n * parents — or wrapping it conditionally — would remount it and drop the\n * session's rendered history on the floor the first time someone opens a file.\n * The conditional children before it are `? :` expressions that leave a null in\n * their slot, which is exactly what keeps its index stable.\n */\nexport function SessionWorkspace({\n client,\n sessionId,\n header,\n defaultRailWidth = 260,\n defaultRailCollapsed,\n className,\n}: SessionWorkspaceProps) {\n // The cwd is the tree's root, and it comes from the registry rather than from\n // the panel: reading it off the panel's own session hook would mean attaching\n // a second WebSocket client, and the tool bridge asks the *first* attached\n // client — a second one changes who answers.\n const { info } = useSessionInfo(client, sessionId)\n const cwd = info?.cwd\n\n const tree = useHostFileTree(client, cwd)\n const search = useHostFileSearch(client, cwd)\n const files = useOpenFiles(client)\n // Writing is a separate server opt-in from reading and defaults off, so the\n // editor asks before it offers to save anything.\n const { canWrite } = useHostFileRoots(client)\n\n // Nothing here can save on the user's behalf when the tab is going away, so\n // the browser's own guard is the last line. Registered only while there is\n // something to lose — an unconditional handler makes every navigation prompt.\n useEffect(() => {\n if (!files.hasUnsaved) return\n const warn = (event: BeforeUnloadEvent) => event.preventDefault()\n window.addEventListener('beforeunload', warn)\n return () => window.removeEventListener('beforeunload', warn)\n }, [files.hasUnsaved])\n\n const wide = useIsWide()\n const [railCollapsed, setRailCollapsed] = useState(defaultRailCollapsed ?? false)\n const [railWidth, setRailWidth] = useState(defaultRailWidth)\n const [editorHeight, setEditorHeight] = useState(360)\n\n // Closing every tab returns the agent to the full column; opening one again\n // should restore the height the user had chosen, so this is not reset here.\n const hasFiles = files.files.length > 0\n\n // A narrow viewport cannot spare 260px of rail beside a transcript, so there\n // the rail overlays the workspace instead of sitting next to it — and starts\n // out of the way.\n const overlayRail = !wide\n const railOpen = tree.available && !railCollapsed\n useEffect(() => {\n if (overlayRail) setRailCollapsed(true)\n }, [overlayRail])\n\n // Closing a dirty tab is the one destructive thing the strip can do, and the\n // edits are not recoverable once the tab is gone. `confirm` rather than a\n // styled dialog on purpose: it is modal, it cannot be missed, and a\n // custom one here would be a second modal system inside a component an\n // embedder already renders inside their own.\n const closeTab = useCallback(\n (path: string) => {\n const file = files.files.find((f) => f.path === path)\n if (file && isDirty(file)) {\n const name = file.name\n if (!window.confirm(`${name} has unsaved changes. Close it and lose them?`)) return\n }\n files.close(path)\n },\n [files],\n )\n\n const column = useRef<HTMLDivElement>(null)\n const columnHeight = useElementHeight(column)\n const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN)\n\n // The embedder's header is app chrome and belongs above everything — but only\n // `SessionPanel` can *build* the `⋯` menu it is handed, and only if it is the\n // one calling the render-prop. So the panel still calls it, in its own tree,\n // and the result is portalled up here. That keeps `SessionPanel` untouched and\n // keeps the menu's own context (it is a Base UI popup) intact.\n const [topBar, setTopBar] = useState<HTMLDivElement | null>(null)\n const hoisted: SessionPanelProps['header'] =\n header === undefined || topBar === null\n ? undefined\n : typeof header === 'function'\n ? (slots) => createPortal(header(slots), topBar)\n : createPortal(header, topBar)\n\n return (\n <div\n data-slot='session-workspace'\n className={cn('flex h-full min-h-0 w-full flex-col overflow-hidden bg-bg', className)}>\n {header !== undefined ? <div ref={setTopBar} className='shrink-0' /> : null}\n <div className='relative flex min-h-0 flex-1'>\n {railOpen ? (\n <FileTree\n tree={tree}\n search={search}\n activePath={files.activePath}\n onOpenFile={files.open}\n onCollapse={() => setRailCollapsed(true)}\n style={{ width: overlayRail ? Math.min(railWidth, 320) : railWidth }}\n className={cn(\n 'shrink-0 border-r border-border',\n overlayRail && 'absolute inset-y-0 left-0 z-20 shadow-lg',\n )}\n />\n ) : tree.available ? (\n // Collapsed: a slim strip that keeps the rail one click away. In flow\n // rather than floating over the panel, so it can never land on top of an\n // embedder's own header controls.\n <div className='flex w-8 shrink-0 flex-col items-center border-r border-border bg-surface pt-1.5'>\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Show project files'\n onClick={() => setRailCollapsed(false)}>\n <PanelLeftOpen className='size-4 text-fg-3' />\n </Button>\n </div>\n ) : null}\n {/* No splitter over an overlay rail — dragging a drawer's edge on a phone\n fights the scroll it is sitting on top of. */}\n {railOpen && !overlayRail ? (\n <Splitter\n orientation='vertical'\n value={railWidth}\n onValueChange={setRailWidth}\n min={RAIL_MIN}\n max={RAIL_MAX}\n aria-label='Resize the file tree'\n />\n ) : null}\n\n <div ref={column} className='flex min-h-0 min-w-0 flex-1 flex-col'>\n {/* Slot 1 of 3. The `? :` leaves a null here when nothing is open, which\n is what holds the agent's slot below and keeps it from remounting. */}\n {hasFiles ? (\n <div\n className='flex min-h-0 shrink-0 flex-col overflow-hidden'\n style={{ height: Math.min(editorHeight, editorMax || editorHeight) }}>\n <EditorTabs\n files={files.files}\n activePath={files.activePath}\n onActivate={files.activate}\n onClose={closeTab}\n />\n <FileViewer\n file={files.active}\n canWrite={canWrite}\n onChange={files.edit}\n onSave={(path) => void files.save(path)}\n onRevert={files.revert}\n onReload={files.reload}\n onOverwrite={(path) => void files.overwrite(path)}\n onDismissConflict={files.dismissConflict}\n />\n </div>\n ) : null}\n {/* Slot 2 of 3. */}\n {hasFiles ? (\n <Splitter\n orientation='horizontal'\n value={Math.min(editorHeight, editorMax || editorHeight)}\n onValueChange={setEditorHeight}\n min={EDITOR_MIN}\n max={editorMax}\n aria-label='Resize the open file'\n />\n ) : null}\n {/* Slot 3 of 3 — always here, always at this index. */}\n <SessionPanel\n client={client}\n sessionId={sessionId}\n header={hoisted}\n className='min-h-0 flex-1'\n />\n </div>\n\n {/* Tapping away closes the drawer, which is the only way back to the\n transcript on a narrow screen. */}\n {overlayRail && railOpen ? (\n <button\n type='button'\n aria-label='Close the file tree'\n onClick={() => setRailCollapsed(true)}\n className='absolute inset-0 z-10 bg-black/30'\n />\n ) : null}\n </div>\n </div>\n )\n}\n\n/** Live height of an element, for a splitter that needs to know how much room it\n * is dividing. Zero until the first observation, which callers treat as\n * \"unmeasured\" rather than as a real bound. */\nfunction useElementHeight(ref: RefObject<HTMLElement | null>): number {\n const [height, setHeight] = useState(0)\n useLayoutEffect(() => {\n const element = ref.current\n if (!element) return\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setHeight(entry.contentRect.height)\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [ref])\n return height\n}\n\n/** Whether there is room for a rail beside the content. Presentation only — the\n * workspace's actual state lives in the hooks from `@workerdeck/react`. */\nfunction useIsWide(): boolean {\n const [wide, setWide] = useState(true)\n useEffect(() => {\n const query = window.matchMedia('(min-width: 768px)')\n const sync = () => setWide(query.matches)\n sync()\n query.addEventListener('change', sync)\n return () => query.removeEventListener('change', sync)\n }, [])\n return wide\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace.mjs","names":[],"sources":["../src/components/agent/EditorTabs.tsx","../src/components/agent/FileTree.tsx","../src/components/agent/CodeEditor.tsx","../src/components/agent/FileViewer.tsx","../src/components/agent/SessionWorkspace.tsx"],"sourcesContent":["import { useEffect, useRef, type MouseEvent as ReactMouseEvent } from 'react'\nimport { isDirty, type OpenFile } from '@workerdeck/react'\nimport { X } from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Tip, TooltipProvider } from '../ui/Tooltip.tsx'\nimport { formatBytes } from '../../lib/format.ts'\n\nexport interface EditorTabsProps {\n files: OpenFile[]\n activePath?: string\n onActivate: (path: string) => void\n onClose: (path: string) => void\n className?: string\n}\n\n/**\n * The open-file tab strip.\n *\n * Hand-rolled rather than built on `@base-ui/react`'s `Tabs`: a VS Code tab\n * carries a close button, and a `<button>` inside a `<button>` is invalid HTML —\n * getting the primitive to render something else costs more than the roving\n * tabindex it would have provided. So that part is here, explicitly, along with\n * the two affordances that actually make a tab strip feel right: middle-click to\n * close, and the active tab scrolling itself into view.\n *\n * Deliberately state-free. Which files are open, which is focused and what\n * closing does are all decided by `useOpenFiles`.\n */\nexport function EditorTabs({ files, activePath, onActivate, onClose, className }: EditorTabsProps) {\n return (\n // Grouped, so moving along the strip shows each tab's path immediately\n // instead of re-serving the open delay on every tab.\n <TooltipProvider delay={500} closeDelay={0}>\n <div\n data-slot='editor-tabs'\n role='tablist'\n aria-label='Open files'\n className={cn(\n 'flex shrink-0 items-stretch overflow-x-auto border-b border-border bg-surface',\n className,\n )}>\n {files.map((file) => (\n <Tab\n key={file.path}\n file={file}\n active={file.path === activePath}\n onActivate={() => onActivate(file.path)}\n onClose={() => onClose(file.path)}\n onArrow={(direction) => {\n const index = files.findIndex((f) => f.path === file.path)\n const next = files[index + direction]\n if (next) onActivate(next.path)\n }}\n />\n ))}\n </div>\n </TooltipProvider>\n )\n}\n\nfunction Tab({\n file,\n active,\n onActivate,\n onClose,\n onArrow,\n}: {\n file: OpenFile\n active: boolean\n onActivate: () => void\n onClose: () => void\n onArrow: (direction: 1 | -1) => void\n}) {\n const dirty = isDirty(file)\n const ref = useRef<HTMLDivElement>(null)\n // Opening a file from the tree can push the new tab off the end of the strip;\n // the point of opening it was to look at it. `nearest` scrolls the minimum, so\n // a tab already on screen stays exactly where it is.\n useEffect(() => {\n if (active) ref.current?.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n }, [active])\n\n const onAuxClick = (event: ReactMouseEvent) => {\n // Middle click. `auxclick` rather than `mousedown` so it matches how every\n // other middle-click target on the platform behaves.\n if (event.button !== 1) return\n event.preventDefault()\n onClose()\n }\n\n return (\n // The tab *is* the trigger (`render`), and it carries the full path — which\n // is why the viewer no longer spends a whole row on a line of monospace\n // nobody reads. No `title` alongside it: the browser's native tooltip would\n // show up underneath this one.\n <Tip\n side='bottom'\n render={\n <div\n ref={ref}\n role='tab'\n tabIndex={active ? 0 : -1}\n aria-selected={active}\n onClick={onActivate}\n onAuxClick={onAuxClick}\n onKeyDown={(event) => {\n if (event.key === 'ArrowRight') onArrow(1)\n else if (event.key === 'ArrowLeft') onArrow(-1)\n else if (event.key === 'Enter' || event.key === ' ') onActivate()\n else return\n event.preventDefault()\n }}\n className={cn(\n 'group flex min-w-0 shrink-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 py-1.5 transition-colors',\n 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent focus-visible:ring-inset',\n active ? 'bg-bg text-fg-1' : 'text-fg-3 hover:bg-surface-hover hover:text-fg-2',\n )}\n />\n }\n content={\n <span className='flex flex-col gap-0.5'>\n <span className='font-mono break-all'>{file.path}</span>\n {file.bytes !== undefined ? (\n <span className='text-fg-4'>{formatBytes(file.bytes)}</span>\n ) : null}\n </span>\n }>\n <span className={cn('max-w-40 truncate font-mono text-label', dirty && 'italic')}>\n {file.name}\n </span>\n {/* Errors are the one state the strip shows, because a failed tab\n otherwise looks identical to a loaded one until you focus it. */}\n {file.status === 'error' ? <span className='shrink-0 text-danger'>!</span> : null}\n <button\n type='button'\n aria-label={dirty ? `Close ${file.name} (unsaved changes)` : `Close ${file.name}`}\n onClick={(event) => {\n // Without this the click also activates the tab being closed, which\n // fights the reducer's focus-the-neighbour rule.\n event.stopPropagation()\n onClose()\n }}\n className={cn(\n 'shrink-0 rounded p-0.5 text-fg-4 transition-opacity hover:bg-surface-hover hover:text-fg-1',\n // Always reachable by keyboard and on touch; only *shown* on hover for\n // the tab you are pointing at, as VS Code does.\n active || dirty\n ? 'opacity-70'\n : 'opacity-0 group-hover:opacity-70 focus-visible:opacity-100',\n )}>\n {/* VS Code's move: a dirty tab shows a dot where the ✕ goes, and the ✕\n comes back when you point at it — so unsaved work is visible at rest\n without taking away the way to close it. */}\n <span className={cn('block', dirty && 'group-hover:hidden')}>\n {dirty ? <span className='block size-3 rounded-full bg-fg-2' /> : <X className='size-3' />}\n </span>\n {dirty ? (\n <span className='hidden group-hover:block'>\n <X className='size-3' />\n </span>\n ) : null}\n </button>\n </Tip>\n )\n}\n","import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'\nimport type { UseHostFileSearchResult, UseHostFileTreeResult } from '@workerdeck/react'\nimport type { HostDirEntry, HostFileMatch } from '@workerdeck/protocol'\nimport {\n ChevronRight,\n File,\n Folder,\n FolderOpen,\n Link2,\n PanelLeftClose,\n RefreshCw,\n Search,\n X,\n} from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Button } from '../ui/Button.tsx'\nimport { Input } from '../ui/Input.tsx'\nimport { Spinner } from '../ui/Spinner.tsx'\n\nexport interface FileTreeProps {\n /** Tree state from `useHostFileTree` — this component holds none of its own. */\n tree: UseHostFileTreeResult\n /** Optional search from `useHostFileSearch`; omit and the box is not offered. */\n search?: UseHostFileSearchResult\n /** Path of the focused file, highlighted in the tree. */\n activePath?: string\n onOpenFile: (path: string) => void\n /** Offered as a button in the header when given. */\n onCollapse?: () => void\n style?: CSSProperties\n className?: string\n}\n\n/** How far one level of nesting indents, in pixels. Inline rather than a Tailwind\n * class because depth is a number at runtime and `pl-${n}` is not a class. */\nconst INDENT = 12\n\n/**\n * The workspace's left rail: an expandable tree of the session's project,\n * with a search box over the same fuzzy route `@file` completion uses.\n *\n * Presentational by construction — every piece of state it renders comes from\n * the hooks in `@workerdeck/react`, and the only thing it owns is the search\n * query, which is the text in its own input.\n *\n * Searching replaces the tree with matches rather than filtering it: the route\n * answers with paths from all over the project, and threading those back into\n * tree positions would mean expanding a dozen directories to show six results.\n */\nexport function FileTree({\n tree,\n search,\n activePath,\n onOpenFile,\n onCollapse,\n style,\n className,\n}: FileTreeProps) {\n const [query, setQuery] = useState('')\n const [matches, setMatches] = useState<HostFileMatch[] | undefined>()\n const searching = query.trim().length > 0\n\n // Debounced, and only while there is something to search for — an empty box\n // means \"show me the tree again\", not \"match everything\".\n useEffect(() => {\n if (!search?.available) return\n const q = query.trim()\n if (!q) {\n setMatches(undefined)\n return\n }\n const controller = new AbortController()\n const timer = setTimeout(() => {\n void search.search(q, { limit: 60, signal: controller.signal }).then((found) => {\n if (!controller.signal.aborted) setMatches(found)\n })\n }, 150)\n return () => {\n controller.abort()\n clearTimeout(timer)\n }\n }, [search, query])\n\n return (\n <div\n data-slot='file-tree'\n style={style}\n className={cn('flex min-h-0 min-w-0 flex-col bg-surface', className)}>\n <div className='flex items-center gap-1 px-2 pt-2'>\n {search?.available ? (\n <div className='relative min-w-0 flex-1'>\n <Search className='absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-fg-4' />\n <Input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder='Search files…'\n className='h-7 pr-7 pl-7 text-label'\n spellCheck={false}\n />\n {searching ? (\n <button\n type='button'\n onClick={() => setQuery('')}\n aria-label='Clear search'\n className='absolute top-1/2 right-1.5 -translate-y-1/2 text-fg-4 transition-colors hover:text-fg-2'>\n <X className='size-3.5' />\n </button>\n ) : null}\n </div>\n ) : (\n <span className='min-w-0 flex-1 truncate px-1 font-mono text-label text-fg-4'>\n {tree.root}\n </span>\n )}\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Refresh the file tree'\n onClick={() => tree.refresh()}>\n <RefreshCw className='size-3.5 text-fg-3' />\n </Button>\n {onCollapse ? (\n <Button variant='ghost' size='icon-sm' aria-label='Hide project files' onClick={onCollapse}>\n <PanelLeftClose className='size-3.5 text-fg-3' />\n </Button>\n ) : null}\n </div>\n\n <div className='min-h-0 flex-1 overflow-auto px-1 py-1'>\n {tree.error ? (\n <p className='px-2 py-3 text-body-sm text-danger'>{tree.error}</p>\n ) : searching ? (\n matches === undefined ? (\n <div className='py-6 text-center'>\n <Spinner className='size-4 text-fg-4' />\n </div>\n ) : matches.length === 0 ? (\n <p className='px-2 py-6 text-center text-body-sm text-fg-4'>No matching files.</p>\n ) : (\n <ul>\n {matches.map((match) => (\n <li key={match.path}>\n <Row\n label={fileName(match.relative)}\n // The directory, dimmed and truncated separately, so a deep\n // path cannot push the filename out of the row — a rail full\n // of `apps/ios/DerivedData/B…` says nothing about which file\n // each hit is.\n detail={directoryOf(match.relative)}\n title={match.relative}\n icon={<File className='size-3.5 shrink-0 text-fg-4' />}\n active={match.path === activePath}\n onClick={() => onOpenFile(match.path)}\n />\n </li>\n ))}\n </ul>\n )\n ) : tree.loading ? (\n <div className='py-6 text-center'>\n <Spinner className='size-4 text-fg-4' />\n </div>\n ) : tree.rows.length === 0 ? (\n <p className='px-2 py-6 text-center text-body-sm text-fg-4'>This project is empty.</p>\n ) : (\n <ul role='tree' aria-label='Project files'>\n {tree.rows.map((row) => (\n <li key={row.entry.path} role='treeitem' aria-expanded={row.expanded} aria-level={row.depth + 1}>\n <Row\n label={row.entry.name}\n indent={row.depth}\n active={row.entry.path === activePath}\n icon={\n row.entry.type === 'dir' ? (\n row.loading ? (\n <Spinner className='size-3.5 shrink-0 text-fg-4' />\n ) : row.expanded ? (\n <FolderOpen className='size-3.5 shrink-0 text-accent' />\n ) : (\n <Folder className='size-3.5 shrink-0 text-accent' />\n )\n ) : (\n <EntryIcon type={row.entry.type} />\n )\n }\n chevron={\n row.entry.type === 'dir' ? (\n <ChevronRight\n className={cn(\n 'size-3 shrink-0 text-fg-4 transition-transform',\n row.expanded && 'rotate-90',\n )}\n />\n ) : undefined\n }\n onClick={() =>\n row.entry.type === 'dir'\n ? tree.toggle(row.entry.path)\n : onOpenFile(row.entry.path)\n }\n />\n {row.truncated ? (\n <p\n className='truncate py-0.5 text-label text-fg-4'\n style={{ paddingLeft: (row.depth + 1) * INDENT + 22 }}>\n More entries than the server will return — use search.\n </p>\n ) : null}\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n )\n}\n\n/** One clickable line. A directory and a file differ only in what the click does\n * and whether there is a chevron — visually they are the same row. */\nfunction Row({\n label,\n detail,\n title,\n icon,\n chevron,\n indent = 0,\n active,\n onClick,\n}: {\n label: string\n /** Secondary text after the label, dimmed and the first thing to be truncated. */\n detail?: string\n title?: string\n icon: ReactNode\n chevron?: ReactNode\n indent?: number\n active?: boolean\n onClick: () => void\n}) {\n // Scroll a row that became active elsewhere (a search hit, a `reveal`) into\n // view, but never yank the list while someone is reading it — `nearest` moves\n // the minimum and does nothing when the row is already visible.\n const ref = useRef<HTMLButtonElement>(null)\n useEffect(() => {\n if (active) ref.current?.scrollIntoView({ block: 'nearest' })\n }, [active])\n\n return (\n <button\n ref={ref}\n type='button'\n onClick={onClick}\n title={title ?? label}\n style={{ paddingLeft: indent * INDENT + 4 }}\n className={cn(\n 'flex w-full items-center gap-1 rounded py-1 pr-2 text-left transition-colors',\n active ? 'bg-surface-hover text-fg-1' : 'text-fg-2 hover:bg-surface-hover',\n )}>\n <span className='flex size-3 shrink-0 items-center justify-center'>{chevron}</span>\n {icon}\n {/* `shrink-0` on the name and `min-w-0` on the detail: when the row runs\n out of room the directory gives way and the filename stays whole. */}\n <span className='shrink-0 truncate font-mono text-label'>{label}</span>\n {detail ? (\n <span className='min-w-0 flex-1 truncate font-mono text-label text-fg-4'>{detail}</span>\n ) : null}\n </button>\n )\n}\n\n/** Last segment of a relative match. */\nfunction fileName(relative: string): string {\n return relative.slice(relative.lastIndexOf('/') + 1)\n}\n\n/** Everything before it, or `undefined` for a file at the search root. */\nfunction directoryOf(relative: string): string | undefined {\n const cut = relative.lastIndexOf('/')\n return cut === -1 ? undefined : relative.slice(0, cut)\n}\n\n/** A symlink is reported as itself and never silently resolved — following it is\n * the next request's problem, and that request is refused if it escapes the roots. */\nfunction EntryIcon({ type }: { type: HostDirEntry['type'] }) {\n if (type === 'symlink') return <Link2 className='size-3.5 shrink-0 text-fg-4' />\n return <File className='size-3.5 shrink-0 text-fg-4' />\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type * as Monaco from 'monaco-editor'\nimport { cn } from '../../lib/utils.ts'\nimport { Spinner } from '../ui/Spinner.tsx'\n\nexport interface CodeEditorProps {\n /** Absolute path — decides the language and identifies the model. */\n path: string\n /** Text to show. Applied to the model when it differs from what is on screen,\n * so an external reload lands without fighting the user's cursor. */\n value: string\n onChange?: (value: string) => void\n /** Ctrl/Cmd+S. Wired inside Monaco because the editor swallows keydown. */\n onSave?: () => void\n readOnly?: boolean\n className?: string\n}\n\n/**\n * Monaco — VS Code's own editor — behind a small React surface.\n *\n * Two deliberate choices, both about `@workerdeck/ui` being a **published\n * library** rather than an app:\n *\n * 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a\n * separate chunk that arrives when someone first opens a file. A dashboard\n * that never opens one never pays for it, and Monaco's ~90 language grammars\n * are themselves lazy (each `registerLanguage` carries an `import()` loader),\n * so opening a `.ts` file fetches the TypeScript grammar and nothing else.\n * 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers\n * in a library become every embedder's bootstrapping problem, and\n * `packages/web` ships prebuilt static files at a domain root, which is\n * exactly where hardcoded worker URLs break. The editor is configured so it\n * never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,\n * no diff editor. A host that wants the worker-backed language services\n * (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`\n * itself before the first file is opened — Monaco is a singleton and nothing\n * here fights that.\n *\n * One model per path, kept across tab switches, so undo history and view state\n * survive clicking away and back — which is most of what makes tabs feel like\n * tabs rather than like re-opening a file.\n */\nexport function CodeEditor({\n path,\n value,\n onChange,\n onSave,\n readOnly,\n className,\n}: CodeEditorProps) {\n const host = useRef<HTMLDivElement>(null)\n const editor = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null)\n const monaco = useRef<typeof Monaco | null>(null)\n const [ready, setReady] = useState(false)\n const theme = useDocumentTheme()\n\n // Callbacks through refs: they change identity every render, and re-creating\n // the editor (or re-registering its listeners) on each one would drop the\n // cursor mid-keystroke.\n const handlers = useRef({ onChange, onSave })\n handlers.current = { onChange, onSave }\n\n useEffect(() => {\n let disposed = false\n void loadMonaco().then((api) => {\n if (disposed || !host.current) return\n monaco.current = api\n const instance = api.editor.create(host.current, {\n value,\n language: languageOf(path),\n readOnly,\n automaticLayout: true,\n theme: monacoTheme(document.documentElement.getAttribute('data-theme')),\n minimap: { enabled: false },\n scrollBeyondLastLine: false,\n fontSize: 12,\n lineHeight: 20,\n fontFamily:\n 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace',\n renderLineHighlight: 'line',\n smoothScrolling: true,\n padding: { top: 8, bottom: 8 },\n scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },\n // Without a worker there is no word-based suggestion provider to ask, so\n // asking would surface an empty popup on every identifier.\n wordBasedSuggestions: 'off',\n quickSuggestions: false,\n })\n editor.current = instance\n instance.onDidChangeModelContent(() => {\n handlers.current.onChange?.(instance.getValue())\n })\n instance.addCommand(api.KeyMod.CtrlCmd | api.KeyCode.KeyS, () => {\n handlers.current.onSave?.()\n })\n setReady(true)\n })\n return () => {\n disposed = true\n // Dispose the editor but NOT its model — the model is keyed by path and\n // outlives this mount so that reopening a tab restores its undo stack.\n editor.current?.dispose()\n editor.current = null\n setReady(false)\n }\n // Deliberately created once, with no dependencies. Path, value and readOnly\n // are applied by the effects below instead, because re-creating the editor\n // to change any of them would lose the cursor, the scroll position and the\n // undo history.\n }, [])\n\n // Swap the model when the focused file changes. One model per path, created\n // lazily and kept, so each tab keeps its own undo history and view state.\n useEffect(() => {\n const api = monaco.current\n const instance = editor.current\n if (!api || !instance || !ready) return\n const uri = api.Uri.parse(`workerdeck://host${path}`)\n const model = api.editor.getModel(uri) ?? api.editor.createModel(value, languageOf(path), uri)\n if (instance.getModel() !== model) instance.setModel(model)\n }, [path, ready, value])\n\n // Apply an external change — a reload from disk, a revert — without\n // disturbing anything when the text already matches, which is the common case\n // because most changes to `value` are echoes of the user's own typing.\n useEffect(() => {\n const instance = editor.current\n if (!instance || !ready) return\n const model = instance.getModel()\n if (!model || model.getValue() === value) return\n // `pushEditOperations` rather than `setValue` so the replacement joins the\n // undo stack instead of clearing it.\n model.pushEditOperations(\n [],\n [{ range: model.getFullModelRange(), text: value }],\n () => null,\n )\n }, [value, ready])\n\n useEffect(() => {\n if (ready) editor.current?.updateOptions({ readOnly })\n }, [readOnly, ready])\n\n // The theme is global to Monaco, not per-editor — `setTheme` is on the\n // namespace for that reason.\n useEffect(() => {\n if (ready) monaco.current?.editor.setTheme(monacoTheme(theme))\n }, [theme, ready])\n\n return (\n <div className={cn('relative min-h-0 min-w-0 flex-1', className)}>\n <div ref={host} className='absolute inset-0' />\n {!ready ? (\n <div className='absolute inset-0 flex items-center justify-center bg-bg'>\n <Spinner className='size-4 text-fg-4' />\n </div>\n ) : null}\n </div>\n )\n}\n\n/**\n * Follow the `data-theme` the design tokens already swap on, so the editor is\n * never the one light rectangle in a dark app (or the reverse). An attribute\n * observer rather than a media query: the app has a manual toggle, and the\n * attribute is what that toggle writes.\n */\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = useState<string | null>(() =>\n typeof document === 'undefined' ? null : document.documentElement.getAttribute('data-theme'),\n )\n useEffect(() => {\n const root = document.documentElement\n const sync = () => setTheme(root.getAttribute('data-theme'))\n sync()\n const observer = new MutationObserver(sync)\n observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => observer.disconnect()\n }, [])\n return theme\n}\n\n/** An unset attribute means the host never opted into the token themes, in which\n * case dark matches this package's default surface. */\nfunction monacoTheme(theme: string | null): string {\n return theme === 'light' ? 'wd-light' : 'wd-dark'\n}\n\n/**\n * Load Monaco once, register the themes, and hand back the API.\n *\n * Cached as a promise rather than a value so that several editors mounting in\n * the same frame share one load instead of racing three of them.\n */\nlet monacoPromise: Promise<typeof Monaco> | undefined\nfunction loadMonaco(): Promise<typeof Monaco> {\n monacoPromise ??= (async () => {\n const api = await import('monaco-editor')\n // Themes that inherit the surrounding surface instead of Monaco's own\n // near-black, so the editor does not sit in the layout as a differently\n // coloured rectangle. Both are defined; the CSS variable decides nothing\n // here, so the panel picks by `prefers-color-scheme` on the document.\n api.editor.defineTheme('wd-dark', {\n base: 'vs-dark',\n inherit: true,\n rules: [],\n colors: { 'editor.background': '#00000000' },\n })\n api.editor.defineTheme('wd-light', {\n base: 'vs',\n inherit: true,\n rules: [],\n colors: { 'editor.background': '#00000000' },\n })\n return api\n })()\n return monacoPromise\n}\n\n/**\n * Monaco's language id for a path.\n *\n * By extension, with a short table for the files that have none — Monaco's own\n * registry is keyed on extensions it knows, and an unmatched file falls through\n * to `plaintext`, which is the honest answer rather than a guess.\n */\nfunction languageOf(path: string): string {\n const name = path.slice(path.lastIndexOf('/') + 1)\n const byName = FILENAME_LANGUAGES[name.toLowerCase()]\n if (byName) return byName\n const extension = name.slice(name.lastIndexOf('.') + 1).toLowerCase()\n return EXTENSION_LANGUAGES[extension] ?? 'plaintext'\n}\n\n/** Files whose type is their whole name. */\nconst FILENAME_LANGUAGES: Record<string, string> = {\n dockerfile: 'dockerfile',\n makefile: 'plaintext',\n '.gitignore': 'plaintext',\n '.env': 'shell',\n}\n\n/** Extension → Monaco language id. Only where the id differs from the extension\n * or the extension is ambiguous; everything else Monaco resolves itself. */\nconst EXTENSION_LANGUAGES: Record<string, string> = {\n ts: 'typescript',\n tsx: 'typescript',\n mts: 'typescript',\n cts: 'typescript',\n js: 'javascript',\n jsx: 'javascript',\n mjs: 'javascript',\n cjs: 'javascript',\n json: 'json',\n jsonc: 'json',\n md: 'markdown',\n mdx: 'markdown',\n css: 'css',\n scss: 'scss',\n less: 'less',\n html: 'html',\n htm: 'html',\n xml: 'xml',\n svg: 'xml',\n yml: 'yaml',\n yaml: 'yaml',\n toml: 'plaintext',\n sh: 'shell',\n bash: 'shell',\n zsh: 'shell',\n fish: 'shell',\n py: 'python',\n rb: 'ruby',\n rs: 'rust',\n go: 'go',\n swift: 'swift',\n java: 'java',\n kt: 'kotlin',\n c: 'c',\n h: 'c',\n cpp: 'cpp',\n cc: 'cpp',\n hpp: 'cpp',\n cs: 'csharp',\n php: 'php',\n sql: 'sql',\n graphql: 'graphql',\n gql: 'graphql',\n lua: 'lua',\n r: 'r',\n pl: 'perl',\n dart: 'dart',\n scala: 'scala',\n ini: 'ini',\n cfg: 'ini',\n conf: 'ini',\n txt: 'plaintext',\n log: 'plaintext',\n}\n","import type { ReactNode } from 'react'\nimport type { OpenFile } from '@workerdeck/react'\nimport { currentText, isDirty } from '@workerdeck/react'\nimport { FileWarning, TriangleAlert } from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Button } from '../ui/Button.tsx'\nimport { Spinner } from '../ui/Spinner.tsx'\nimport { CodeEditor } from './CodeEditor.tsx'\n\nexport interface FileViewerProps {\n file: OpenFile | undefined\n /** From `/fs/roots`. False renders the editor read-only rather than letting\n * someone type into a file this gateway will refuse to write. */\n canWrite?: boolean\n onChange?: (path: string, content: string) => void\n onSave?: (path: string) => void\n /** Discard this tab's edits — local only, no re-read. */\n onRevert?: (path: string) => void\n /** Take the version on disk, discarding this tab's edits. */\n onReload?: (path: string) => void\n /** Take this tab's version, over whatever is on disk now. */\n onOverwrite?: (path: string) => void\n onDismissConflict?: (path: string) => void\n className?: string\n}\n\n/**\n * The focused file: Monaco, plus the states a file can be in that are not\n * \"here is some text\".\n *\n * No path row — the tab's tooltip carries the path and the size, and a line of\n * monospace above every file is chrome that never earns its height.\n */\nexport function FileViewer({\n file,\n canWrite,\n onChange,\n onSave,\n onRevert,\n onReload,\n onOverwrite,\n onDismissConflict,\n className,\n}: FileViewerProps) {\n if (!file) return null\n\n return (\n <div\n data-slot='file-viewer'\n className={cn('flex min-h-0 min-w-0 flex-1 flex-col bg-bg', className)}>\n {/* The one failure with a choice attached, so it gets a bar rather than a\n toast: the agent rewrote this file while it was open, and which\n version wins is not something to decide on the user's behalf. */}\n {file.conflict ? (\n <div className='flex shrink-0 flex-wrap items-center gap-2 border-b border-warning/40 bg-warning-bg px-3 py-2'>\n <TriangleAlert className='size-3.5 shrink-0 text-warning' />\n <span className='min-w-0 flex-1 text-body-sm text-warning'>\n This file changed on disk since you opened it.\n </span>\n <Button variant='outline' size='xs' onClick={() => onReload?.(file.path)}>\n Use the version on disk\n </Button>\n <Button variant='outline' size='xs' onClick={() => onOverwrite?.(file.path)}>\n Keep mine\n </Button>\n <Button variant='ghost' size='xs' onClick={() => onDismissConflict?.(file.path)}>\n Dismiss\n </Button>\n </div>\n ) : file.saveError ? (\n <div className='flex shrink-0 items-center gap-2 border-b border-danger/40 bg-danger-bg px-3 py-2'>\n <TriangleAlert className='size-3.5 shrink-0 text-danger' />\n <span className='min-w-0 flex-1 text-body-sm text-danger'>{file.saveError}</span>\n <Button variant='ghost' size='xs' onClick={() => onDismissConflict?.(file.path)}>\n Dismiss\n </Button>\n </div>\n ) : null}\n\n {file.status === 'loading' ? (\n <Centred>\n <Spinner className='size-4 text-fg-4' />\n </Centred>\n ) : file.status === 'error' ? (\n <Centred>\n <TriangleAlert className='size-4 text-danger' />\n <p className='text-body-sm text-danger'>{file.error}</p>\n </Centred>\n ) : file.status === 'binary' ? (\n <Centred>\n <FileWarning className='size-4 text-fg-4' />\n <p className='text-body-sm text-fg-4'>This file isn’t text.</p>\n {/* Said out loud, because \"can't show it\" and \"editing it here would\n destroy it\" are different reassurances. */}\n <p className='text-label text-fg-4'>It can’t be edited here without corrupting it.</p>\n </Centred>\n ) : (\n <CodeEditor\n path={file.path}\n value={currentText(file)}\n readOnly={!canWrite}\n onChange={onChange ? (content) => onChange(file.path, content) : undefined}\n onSave={onSave ? () => onSave(file.path) : undefined}\n />\n )}\n\n {/* A status strip only while there is something to say. Saving is fast\n enough that a permanent row would mostly be blank. */}\n {file.status === 'ready' && (file.saving || isDirty(file) || !canWrite) ? (\n <div className='flex shrink-0 items-center gap-2 border-t border-border px-3 py-1'>\n {file.saving ? (\n <>\n <Spinner className='size-3 text-fg-4' />\n <span className='text-label text-fg-4'>Saving…</span>\n </>\n ) : !canWrite ? (\n <span className='text-label text-fg-4'>\n Read-only — this gateway doesn’t allow writes.\n </span>\n ) : (\n <>\n <span className='text-label text-fg-3'>Unsaved changes</span>\n <span className='flex-1' />\n {/* Revert, not reload: discard *my* edits and go back to what\n this tab read. Re-reading is the conflict bar's job, and it is\n a different question. */}\n <Button variant='ghost' size='xs' onClick={() => onRevert?.(file.path)}>\n Revert\n </Button>\n <Button variant='outline' size='xs' onClick={() => onSave?.(file.path)}>\n Save\n <span className='ml-1 text-fg-4'>⌘S</span>\n </Button>\n </>\n )}\n </div>\n ) : null}\n </div>\n )\n}\n\nfunction Centred({ children }: { children: ReactNode }) {\n return (\n <div className='flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-6'>\n {children}\n </div>\n )\n}\n","import { useCallback, useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { WorkerDeckClient } from '@workerdeck/client'\nimport {\n useHostFileRoots,\n useHostFileSearch,\n useHostFileTree,\n useOpenFiles,\n useSessionInfo,\n isDirty,\n} from '@workerdeck/react'\nimport { PanelLeftOpen } from 'lucide-react'\nimport { cn } from '../../lib/utils.ts'\nimport { Button } from '../ui/Button.tsx'\nimport { Splitter } from '../ui/Splitter.tsx'\nimport { EditorTabs } from './EditorTabs.tsx'\nimport { FileTree } from './FileTree.tsx'\nimport { FileViewer } from './FileViewer.tsx'\nimport { SessionPanel, type SessionPanelProps } from './SessionPanel.tsx'\n\nexport interface SessionWorkspaceProps {\n client: WorkerDeckClient\n sessionId: string | undefined\n /** Passed straight through to {@link SessionPanel} — including the render-prop\n * form that claims the session-actions menu. */\n header?: SessionPanelProps['header']\n /**\n * Panel seams the workspace does not interpret, forwarded verbatim.\n *\n * They are listed rather than spread so the workspace stays explicit about\n * what it passes on: the panel owns the session's one attach, and a seam that\n * silently arrived here would be a second place to look for why a session\n * renders the way it does.\n */\n transcriptVariant?: SessionPanelProps['transcriptVariant']\n transcriptDensity?: SessionPanelProps['transcriptDensity']\n /** Which end of the panel the status bar sits at — see `SessionPanel`. */\n statusPlacement?: SessionPanelProps['statusPlacement']\n unseen?: SessionPanelProps['unseen']\n /** Viewer mode — no composer, no approval prompts. Forwarded verbatim to\n * {@link SessionPanel}; the file tree and editor are unaffected, since reading\n * a run's files is the point of a read-only view. */\n readOnly?: SessionPanelProps['readOnly']\n onVitals?: SessionPanelProps['onVitals']\n /** Rail width in pixels on first render. */\n defaultRailWidth?: number\n /** Start with the file rail collapsed even on a wide viewport. */\n defaultRailCollapsed?: boolean\n /**\n * The rail moved. Paired with the two defaults so an embedder can persist the\n * layout — the workspace deliberately does not, because *where* to keep it (a\n * Memento, localStorage, a workspace file) is the embedder's call, and a\n * component that picked one would be wrong in the other hosts.\n */\n onRailChange?: (rail: { width: number; collapsed: boolean }) => void\n className?: string\n}\n\nconst RAIL_MIN = 180\nconst RAIL_MAX = 520\n/** How little of the agent column the editor may leave. Below this the composer\n * and a line of transcript stop fitting together, which is the point at which the\n * agent has stopped being usable rather than merely small. */\nconst AGENT_MIN = 220\nconst EDITOR_MIN = 120\n\n/**\n * A VS Code-shaped workspace around a live session: file tree on the left, open\n * files above, the agent below.\n *\n * **Strictly additive.** {@link SessionPanel} is untouched and still the whole\n * session surface on its own — an embedder picks one or the other, and one that\n * has its own file tree keeps using the panel.\n *\n * Two things here are load-bearing and easy to break:\n *\n * 1. **The editor region is absent from the layout when nothing is open**, not\n * collapsed to zero height. A zero-height pane leaves a draggable splitter and\n * parks the composer at an odd offset; absence is what makes the agent's\n * \"claims the full column\" state actually look like the panel alone.\n * 2. **`SessionPanel` keeps its position in the tree across that transition.** It\n * holds the WebSocket attach and the entire transcript, so moving it between\n * parents — or wrapping it conditionally — would remount it and drop the\n * session's rendered history on the floor the first time someone opens a file.\n * The conditional children before it are `? :` expressions that leave a null in\n * their slot, which is exactly what keeps its index stable.\n */\nexport function SessionWorkspace({\n client,\n sessionId,\n header,\n transcriptVariant,\n transcriptDensity,\n statusPlacement,\n unseen,\n readOnly,\n onVitals,\n defaultRailWidth = 260,\n defaultRailCollapsed,\n onRailChange,\n className,\n}: SessionWorkspaceProps) {\n // The cwd is the tree's root, and it comes from the registry rather than from\n // the panel: reading it off the panel's own session hook would mean attaching\n // a second WebSocket client, and the tool bridge asks the *first* attached\n // client — a second one changes who answers.\n const { info } = useSessionInfo(client, sessionId)\n const cwd = info?.cwd\n\n const tree = useHostFileTree(client, cwd)\n const search = useHostFileSearch(client, cwd)\n const files = useOpenFiles(client)\n // Writing is a separate server opt-in from reading and defaults off, so the\n // editor asks before it offers to save anything.\n const { canWrite } = useHostFileRoots(client)\n\n // Nothing here can save on the user's behalf when the tab is going away, so\n // the browser's own guard is the last line. Registered only while there is\n // something to lose — an unconditional handler makes every navigation prompt.\n useEffect(() => {\n if (!files.hasUnsaved) return\n const warn = (event: BeforeUnloadEvent) => event.preventDefault()\n window.addEventListener('beforeunload', warn)\n return () => window.removeEventListener('beforeunload', warn)\n }, [files.hasUnsaved])\n\n const wide = useIsWide()\n const [railCollapsed, setRailCollapsed] = useState(defaultRailCollapsed ?? false)\n const [railWidth, setRailWidth] = useState(defaultRailWidth)\n // Reported rather than stored. Kept in a ref so the effect below fires on a\n // real change instead of on every render an inline callback would cause.\n const onRailChangeRef = useRef(onRailChange)\n onRailChangeRef.current = onRailChange\n useEffect(() => {\n onRailChangeRef.current?.({ width: railWidth, collapsed: railCollapsed })\n }, [railWidth, railCollapsed])\n const [editorHeight, setEditorHeight] = useState(360)\n\n // Closing every tab returns the agent to the full column; opening one again\n // should restore the height the user had chosen, so this is not reset here.\n const hasFiles = files.files.length > 0\n\n // A narrow viewport cannot spare 260px of rail beside a transcript, so there\n // the rail overlays the workspace instead of sitting next to it — and starts\n // out of the way.\n const overlayRail = !wide\n const railOpen = tree.available && !railCollapsed\n useEffect(() => {\n if (overlayRail) setRailCollapsed(true)\n }, [overlayRail])\n\n // Closing a dirty tab is the one destructive thing the strip can do, and the\n // edits are not recoverable once the tab is gone. `confirm` rather than a\n // styled dialog on purpose: it is modal, it cannot be missed, and a\n // custom one here would be a second modal system inside a component an\n // embedder already renders inside their own.\n const closeTab = useCallback(\n (path: string) => {\n const file = files.files.find((f) => f.path === path)\n if (file && isDirty(file)) {\n const name = file.name\n if (!window.confirm(`${name} has unsaved changes. Close it and lose them?`)) return\n }\n files.close(path)\n },\n [files],\n )\n\n const column = useRef<HTMLDivElement>(null)\n const columnHeight = useElementHeight(column)\n const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN)\n\n // The embedder's header is app chrome and belongs above everything — but only\n // `SessionPanel` can *build* the `⋯` menu it is handed, and only if it is the\n // one calling the render-prop. So the panel still calls it, in its own tree,\n // and the result is portalled up here. That keeps `SessionPanel` untouched and\n // keeps the menu's own context (it is a Base UI popup) intact.\n const [topBar, setTopBar] = useState<HTMLDivElement | null>(null)\n const hoisted: SessionPanelProps['header'] =\n header === undefined || topBar === null\n ? undefined\n : typeof header === 'function'\n ? (slots) => createPortal(header(slots), topBar)\n : createPortal(header, topBar)\n\n return (\n <div\n data-slot='session-workspace'\n className={cn('flex h-full min-h-0 w-full flex-col overflow-hidden bg-bg', className)}>\n {header !== undefined ? <div ref={setTopBar} className='shrink-0' /> : null}\n <div className='relative flex min-h-0 flex-1'>\n {railOpen ? (\n <FileTree\n tree={tree}\n search={search}\n activePath={files.activePath}\n onOpenFile={files.open}\n onCollapse={() => setRailCollapsed(true)}\n style={{ width: overlayRail ? Math.min(railWidth, 320) : railWidth }}\n className={cn(\n 'shrink-0 border-r border-border',\n overlayRail && 'absolute inset-y-0 left-0 z-20 shadow-lg',\n )}\n />\n ) : tree.available ? (\n // Collapsed: a slim strip that keeps the rail one click away. In flow\n // rather than floating over the panel, so it can never land on top of an\n // embedder's own header controls.\n <div className='flex w-8 shrink-0 flex-col items-center border-r border-border bg-surface pt-1.5'>\n <Button\n variant='ghost'\n size='icon-sm'\n aria-label='Show project files'\n onClick={() => setRailCollapsed(false)}>\n <PanelLeftOpen className='size-4 text-fg-3' />\n </Button>\n </div>\n ) : null}\n {/* No splitter over an overlay rail — dragging a drawer's edge on a phone\n fights the scroll it is sitting on top of. */}\n {railOpen && !overlayRail ? (\n <Splitter\n orientation='vertical'\n value={railWidth}\n onValueChange={setRailWidth}\n min={RAIL_MIN}\n max={RAIL_MAX}\n aria-label='Resize the file tree'\n />\n ) : null}\n\n <div ref={column} className='flex min-h-0 min-w-0 flex-1 flex-col'>\n {/* Slot 1 of 3. The `? :` leaves a null here when nothing is open, which\n is what holds the agent's slot below and keeps it from remounting. */}\n {hasFiles ? (\n <div\n className='flex min-h-0 shrink-0 flex-col overflow-hidden'\n style={{ height: Math.min(editorHeight, editorMax || editorHeight) }}>\n <EditorTabs\n files={files.files}\n activePath={files.activePath}\n onActivate={files.activate}\n onClose={closeTab}\n />\n <FileViewer\n file={files.active}\n canWrite={canWrite}\n onChange={files.edit}\n onSave={(path) => void files.save(path)}\n onRevert={files.revert}\n onReload={files.reload}\n onOverwrite={(path) => void files.overwrite(path)}\n onDismissConflict={files.dismissConflict}\n />\n </div>\n ) : null}\n {/* Slot 2 of 3. */}\n {hasFiles ? (\n <Splitter\n orientation='horizontal'\n value={Math.min(editorHeight, editorMax || editorHeight)}\n onValueChange={setEditorHeight}\n min={EDITOR_MIN}\n max={editorMax}\n aria-label='Resize the open file'\n />\n ) : null}\n {/* Slot 3 of 3 — always here, always at this index. */}\n <SessionPanel\n client={client}\n sessionId={sessionId}\n header={hoisted}\n transcriptVariant={transcriptVariant}\n transcriptDensity={transcriptDensity}\n statusPlacement={statusPlacement}\n unseen={unseen}\n readOnly={readOnly}\n onVitals={onVitals}\n className='min-h-0 flex-1'\n />\n </div>\n\n {/* Tapping away closes the drawer, which is the only way back to the\n transcript on a narrow screen. */}\n {overlayRail && railOpen ? (\n <button\n type='button'\n aria-label='Close the file tree'\n onClick={() => setRailCollapsed(true)}\n className='absolute inset-0 z-10 bg-black/30'\n />\n ) : null}\n </div>\n </div>\n )\n}\n\n/** Live height of an element, for a splitter that needs to know how much room it\n * is dividing. Zero until the first observation, which callers treat as\n * \"unmeasured\" rather than as a real bound. */\nfunction useElementHeight(ref: RefObject<HTMLElement | null>): number {\n const [height, setHeight] = useState(0)\n useLayoutEffect(() => {\n const element = ref.current\n if (!element) return\n const observer = new ResizeObserver(([entry]) => {\n if (entry) setHeight(entry.contentRect.height)\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [ref])\n return height\n}\n\n/** Whether there is room for a rail beside the content. Presentation only — the\n * workspace's actual state lives in the hooks from `@workerdeck/react`. */\nfunction useIsWide(): boolean {\n const [wide, setWide] = useState(true)\n useEffect(() => {\n const query = window.matchMedia('(min-width: 768px)')\n const sync = () => setWide(query.matches)\n sync()\n query.addEventListener('change', sync)\n return () => query.removeEventListener('change', sync)\n }, [])\n return wide\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerdeck/ui",
3
- "version": "0.11.0",
3
+ "version": "0.13.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.11.0",
42
- "@workerdeck/protocol": "0.11.0",
43
- "@workerdeck/react": "0.11.0"
41
+ "@workerdeck/react": "0.13.0",
42
+ "@workerdeck/client": "0.13.0",
43
+ "@workerdeck/protocol": "0.13.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "monaco-editor": "^0.56.0",