@workerdeck/ui 0.15.0 → 0.16.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 (65) hide show
  1. package/README.md +53 -0
  2. package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-B9CHoq8x.d.mts} +158 -21
  3. package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DII9MmQ8.mjs} +4254 -1661
  4. package/build/SessionPanel-DII9MmQ8.mjs.map +1 -0
  5. package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
  6. package/build/format.d.mts +39 -4
  7. package/build/format.mjs +2 -118
  8. package/build/index.d.mts +442 -45
  9. package/build/index.mjs +105 -2
  10. package/build/index.mjs.map +1 -1
  11. package/build/status-Ydzi7n6j.mjs +143 -0
  12. package/build/status-Ydzi7n6j.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -1
  14. package/build/workspace.mjs +108 -5
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +14 -7
  17. package/src/components/agent/Composer.tsx +189 -89
  18. package/src/components/agent/Conversation.tsx +12 -12
  19. package/src/components/agent/FileCard.tsx +0 -26
  20. package/src/components/agent/FileTree.tsx +9 -8
  21. package/src/components/agent/Loader.tsx +22 -72
  22. package/src/components/agent/Message.tsx +11 -46
  23. package/src/components/agent/PermissionPrompt.tsx +0 -92
  24. package/src/components/agent/QuestionPrompt.tsx +0 -122
  25. package/src/components/agent/Reasoning.tsx +5 -19
  26. package/src/components/agent/Response.tsx +1 -132
  27. package/src/components/agent/SessionPanel.tsx +224 -28
  28. package/src/components/agent/SessionWorkspace.tsx +29 -0
  29. package/src/components/agent/StatusBar.tsx +20 -4
  30. package/src/components/agent/ToolCallCard.tsx +17 -111
  31. package/src/components/agent/Transcript.tsx +710 -203
  32. package/src/components/agent/UsageDialog.tsx +20 -106
  33. package/src/components/agent/UsageMeters.tsx +133 -0
  34. package/src/components/agent/pulse.tsx +3 -2
  35. package/src/components/agent/transcript-rows.ts +82 -0
  36. package/src/components/agent/transcript-variant.tsx +29 -51
  37. package/src/components/agent/use-height-epoch.ts +60 -0
  38. package/src/components/agent/use-path-links.ts +147 -0
  39. package/src/components/agent/use-transcript-jumps.ts +190 -0
  40. package/src/components/prompt-area/cursor-helpers.ts +65 -0
  41. package/src/components/prompt-area/use-prompt-area.ts +16 -10
  42. package/src/components/terminal/PermissionPrompt.tsx +119 -0
  43. package/src/components/terminal/QuestionPrompt.tsx +322 -0
  44. package/src/components/terminal/StatusLine.tsx +159 -0
  45. package/src/components/terminal/TerminalTranscript.tsx +147 -0
  46. package/src/components/terminal/affordances.tsx +118 -0
  47. package/src/components/terminal/diff.tsx +130 -0
  48. package/src/components/terminal/height.ts +727 -0
  49. package/src/components/terminal/items.tsx +449 -0
  50. package/src/components/terminal/markdown.tsx +191 -0
  51. package/src/components/terminal/press.tsx +120 -0
  52. package/src/components/terminal/prompt.tsx +343 -0
  53. package/src/components/terminal/result-preview.ts +72 -0
  54. package/src/components/terminal/row.tsx +132 -0
  55. package/src/components/terminal/scrubber.tsx +663 -0
  56. package/src/components/terminal/surface.tsx +80 -0
  57. package/src/components/terminal/tool-run.ts +91 -0
  58. package/src/index.ts +34 -0
  59. package/src/lib/status.ts +59 -3
  60. package/src/lib/tool-icon.ts +14 -0
  61. package/src/styles/terminal.css +1011 -0
  62. package/src/styles/theme.css +41 -0
  63. package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
  64. package/build/format.mjs.map +0 -1
  65. package/src/components/agent/line-prompt.tsx +0 -249
@@ -1,4 +1,4 @@
1
- import { r as SessionPanelProps } from "./SessionPanel-J2U8v88q.mjs";
1
+ import { r as SessionPanelProps } from "./SessionPanel-B9CHoq8x.mjs";
2
2
  import * as _$react from "react";
3
3
  import { CSSProperties } from "react";
4
4
  import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
@@ -22,6 +22,11 @@ interface SessionWorkspaceProps {
22
22
  transcriptVariant?: SessionPanelProps['transcriptVariant'];
23
23
  transcriptDensity?: SessionPanelProps['transcriptDensity'];
24
24
  transcriptFont?: SessionPanelProps['transcriptFont'];
25
+ /** The overview ruler in place of the scrollbar — see `SessionPanel`. */
26
+ scrubber?: SessionPanelProps['scrubber'];
27
+ scrubberMarks?: SessionPanelProps['scrubberMarks'];
28
+ /** The last prompt pinned above the transcript — see `SessionPanel`. */
29
+ stickyPrompt?: SessionPanelProps['stickyPrompt'];
25
30
  /** Which end of the panel the status bar sits at — see `SessionPanel`. */
26
31
  statusPlacement?: SessionPanelProps['statusPlacement'];
27
32
  controlsSurface?: SessionPanelProps['controlsSurface'];
@@ -75,6 +80,9 @@ declare function SessionWorkspace({
75
80
  transcriptVariant,
76
81
  transcriptDensity,
77
82
  transcriptFont,
83
+ scrubber,
84
+ scrubberMarks,
85
+ stickyPrompt,
78
86
  statusPlacement,
79
87
  controlsSurface,
80
88
  unseen,
@@ -1,4 +1,4 @@
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-DI1NO4l8.mjs";
1
+ import { Bt as Button, Ht as cn, Lt as Input, dt as Splitter, gt as Tip, pt as Spinner, t as SessionPanel, vt as TooltipProvider } from "./SessionPanel-DII9MmQ8.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";
@@ -180,7 +180,7 @@ function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, cla
180
180
  }) : null
181
181
  ]
182
182
  }) : /* @__PURE__ */ jsx("span", {
183
- className: "min-w-0 flex-1 truncate px-1 font-mono text-label text-fg-4",
183
+ className: "min-w-0 flex-1 truncate px-1 text-label text-fg-4",
184
184
  children: tree.root
185
185
  }),
186
186
  /* @__PURE__ */ jsx(Button, {
@@ -267,11 +267,11 @@ function Row({ label, detail, title, icon, chevron, indent = 0, active, onClick
267
267
  }),
268
268
  icon,
269
269
  /* @__PURE__ */ jsx("span", {
270
- className: "shrink-0 truncate font-mono text-label",
270
+ className: "shrink-0 truncate text-label",
271
271
  children: label
272
272
  }),
273
273
  detail ? /* @__PURE__ */ jsx("span", {
274
- className: "min-w-0 flex-1 truncate font-mono text-label text-fg-4",
274
+ className: "min-w-0 flex-1 truncate text-label text-fg-4",
275
275
  children: detail
276
276
  }) : null
277
277
  ]
@@ -665,6 +665,100 @@ function Centred({ children }) {
665
665
  });
666
666
  }
667
667
  //#endregion
668
+ //#region src/components/agent/use-path-links.ts
669
+ /** Marks the element under the pointer while the modifier is held. */
670
+ const LINKISH = "wd-path-link";
671
+ /** Absolute, or relative with at least one separator. `:12` is a line number. */
672
+ const PATH_RE = /^(\/[^\s:'"`()[\]{}]+|[\w.@~-]+(?:\/[\w.@~-]+)+)(?::(\d+))?$/;
673
+ /** A bare filename — only trusted inside a code element. */
674
+ const FILE_RE = /^([\w.@-]+\.[A-Za-z0-9]{1,10})(?::(\d+))?$/;
675
+ /** How much of an element's text the match must cover, outside code. */
676
+ const COVERAGE = .6;
677
+ function matchPath(text, inCode) {
678
+ const trimmed = text.trim();
679
+ if (!trimmed || trimmed.endsWith("/")) return void 0;
680
+ const hit = PATH_RE.exec(trimmed) ?? (inCode ? FILE_RE.exec(trimmed) : null);
681
+ if (!hit) return void 0;
682
+ return {
683
+ path: hit[1],
684
+ line: hit[2] ? Number(hit[2]) : void 0
685
+ };
686
+ }
687
+ /** The path a click on this element means, if any. */
688
+ function hitFor(element) {
689
+ if (!element) return void 0;
690
+ const inCode = element.closest("code") !== null;
691
+ const text = element.textContent ?? "";
692
+ const hit = matchPath(text, inCode);
693
+ if (!hit) return void 0;
694
+ if (!inCode && hit.path.length < text.trim().length * COVERAGE) return void 0;
695
+ return hit;
696
+ }
697
+ function usePathLinks({ container, onOpen, enabled = true, ignore }) {
698
+ useEffect(() => {
699
+ const root = container.current;
700
+ if (!root || !enabled) return;
701
+ const excluded = (element) => ignore !== void 0 && element?.closest(ignore) !== null && element !== void 0;
702
+ let marked;
703
+ const unmark = () => {
704
+ marked?.classList.remove(LINKISH);
705
+ marked = void 0;
706
+ };
707
+ const onClick = (event) => {
708
+ if (!event.metaKey && !event.ctrlKey) return;
709
+ const target = event.target instanceof HTMLElement ? event.target : void 0;
710
+ if (excluded(target)) return;
711
+ const hit = hitFor(target);
712
+ if (!hit) return;
713
+ event.preventDefault();
714
+ event.stopPropagation();
715
+ unmark();
716
+ onOpen(hit);
717
+ };
718
+ const onMove = (event) => {
719
+ if (!event.metaKey && !event.ctrlKey) return unmark();
720
+ const element = event.target instanceof HTMLElement ? event.target : void 0;
721
+ if (element === marked) return;
722
+ unmark();
723
+ if (!element || excluded(element) || !hitFor(element)) return;
724
+ marked = element;
725
+ element.classList.add(LINKISH);
726
+ };
727
+ const onKey = (event) => {
728
+ if (!event.metaKey && !event.ctrlKey) unmark();
729
+ };
730
+ root.addEventListener("click", onClick, true);
731
+ root.addEventListener("mousemove", onMove);
732
+ document.addEventListener("keydown", onKey);
733
+ document.addEventListener("keyup", onKey);
734
+ window.addEventListener("blur", unmark);
735
+ return () => {
736
+ unmark();
737
+ root.removeEventListener("click", onClick, true);
738
+ root.removeEventListener("mousemove", onMove);
739
+ document.removeEventListener("keydown", onKey);
740
+ document.removeEventListener("keyup", onKey);
741
+ window.removeEventListener("blur", unmark);
742
+ };
743
+ }, [
744
+ container,
745
+ onOpen,
746
+ enabled,
747
+ ignore
748
+ ]);
749
+ }
750
+ /**
751
+ * A transcript path against the session's cwd.
752
+ *
753
+ * Absolute wins outright. Everything else is relative to where the agent is
754
+ * working, which is the only root that makes `worker.mjs` mean anything.
755
+ */
756
+ function resolveAgainstCwd(path, cwd) {
757
+ if (path.startsWith("/")) return path;
758
+ if (!cwd) return path;
759
+ return `${cwd.replace(/\/$/, "")}/${path.replace(/^\.\//, "")}`;
760
+ }
761
+ //#endregion
668
762
  //#region src/components/agent/SessionWorkspace.tsx
669
763
  const RAIL_MIN = 180;
670
764
  const RAIL_MAX = 520;
@@ -694,7 +788,7 @@ const EDITOR_MIN = 120;
694
788
  * The conditional children before it are `? :` expressions that leave a null in
695
789
  * their slot, which is exactly what keeps its index stable.
696
790
  */
697
- function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, transcriptFont, statusPlacement, controlsSurface, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
791
+ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, transcriptFont, scrubber, scrubberMarks, stickyPrompt, statusPlacement, controlsSurface, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
698
792
  const { info } = useSessionInfo(client, sessionId);
699
793
  const cwd = info?.cwd;
700
794
  const tree = useHostFileTree(client, cwd);
@@ -735,6 +829,12 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
735
829
  }, [files]);
736
830
  const column = useRef(null);
737
831
  const columnHeight = useElementHeight(column);
832
+ usePathLinks({
833
+ container: column,
834
+ onOpen: useCallback(({ path }) => files.open(resolveAgainstCwd(path, cwd)), [files.open, cwd]),
835
+ enabled: tree.available,
836
+ ignore: ".monaco-editor"
837
+ });
738
838
  const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN);
739
839
  const [topBar, setTopBar] = useState(null);
740
840
  const hoisted = header === void 0 || topBar === null ? void 0 : typeof header === "function" ? (slots) => createPortal(header(slots), topBar) : createPortal(header, topBar);
@@ -812,6 +912,9 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
812
912
  transcriptVariant,
813
913
  transcriptDensity,
814
914
  transcriptFont,
915
+ scrubber,
916
+ scrubberMarks,
917
+ stickyPrompt,
815
918
  controlsSurface,
816
919
  statusPlacement,
817
920
  unseen,
@@ -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 /**\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 transcriptFont?: SessionPanelProps['transcriptFont']\n /** Which end of the panel the status bar sits at — see `SessionPanel`. */\n statusPlacement?: SessionPanelProps['statusPlacement']\n controlsSurface?: SessionPanelProps['controlsSurface']\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 transcriptFont,\n statusPlacement,\n controlsSurface,\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 defaultValue={defaultRailWidth}\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 transcriptFont={transcriptFont}\n controlsSurface={controlsSurface}\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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"}
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/use-path-links.ts","../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 text-label text-fg-4'>{tree.root}</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 The rail reads in the **UI font**, never mono: it is workbench chrome\n — a list you scan — and the editors it sits beside (VS Code, Finder)\n all set filenames in their UI face. Mono is for content on a grid, and\n nothing here is on one. It is also independent of `transcriptFont`,\n which scopes to the session panel alone. */}\n <span className='shrink-0 truncate text-label'>{label}</span>\n {detail ? <span className='min-w-0 flex-1 truncate text-label text-fg-4'>{detail}</span> : 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 { useEffect, type RefObject } from 'react'\n\n/**\n * Cmd/Ctrl+click a file path in the transcript to open it.\n *\n * The agent names files constantly — in prose, in tables, in tool arguments —\n * and every one of them is a place the reader wants to go. They already *look*\n * like links (inline code renders in the accent), so the affordance is half\n * promised before anything is wired: this makes it true.\n *\n * **Detection is deliberately conservative**, because the cost is asymmetric. A\n * missed path is a path you select and open yourself; a false positive is a\n * word that lights up under the modifier and then does nothing, which teaches\n * the reader not to trust any of them. So:\n *\n * - A trailing `/` means a directory. Directories are not openable in a text\n * editor, and offering it is worse than staying quiet.\n * - Outside a `<code>` element the match must be *most* of what the element\n * says, or a paragraph mentioning `src/index.ts` in passing would underline\n * whole sentences.\n * - A bare filename (`worker.mjs`) is only a path **inside code**, and only\n * with an extension. `18 B` and `Type` are table cells, not files.\n *\n * The modifier is the editor's own: hold it and what would open lights up\n * (`wd-path-link`), which is the only honest way to show which of the blue\n * things on screen are actually reachable.\n */\nexport type PathHit = { path: string; line?: number }\n\n/** Marks the element under the pointer while the modifier is held. */\nconst LINKISH = 'wd-path-link'\n\n/** Absolute, or relative with at least one separator. `:12` is a line number. */\nconst PATH_RE = /^(\\/[^\\s:'\"`()[\\]{}]+|[\\w.@~-]+(?:\\/[\\w.@~-]+)+)(?::(\\d+))?$/\n/** A bare filename — only trusted inside a code element. */\nconst FILE_RE = /^([\\w.@-]+\\.[A-Za-z0-9]{1,10})(?::(\\d+))?$/\n\n/** How much of an element's text the match must cover, outside code. */\nconst COVERAGE = 0.6\n\nexport function matchPath(text: string, inCode: boolean): PathHit | undefined {\n const trimmed = text.trim()\n if (!trimmed || trimmed.endsWith('/')) return undefined\n const hit = PATH_RE.exec(trimmed) ?? (inCode ? FILE_RE.exec(trimmed) : null)\n if (!hit) return undefined\n return { path: hit[1]!, line: hit[2] ? Number(hit[2]) : undefined }\n}\n\n/** The path a click on this element means, if any. */\nfunction hitFor(element: HTMLElement | undefined): PathHit | undefined {\n if (!element) return undefined\n const inCode = element.closest('code') !== null\n const text = element.textContent ?? ''\n const hit = matchPath(text, inCode)\n if (!hit) return undefined\n if (!inCode && hit.path.length < text.trim().length * COVERAGE) return undefined\n return hit\n}\n\nexport function usePathLinks({\n container,\n onOpen,\n enabled = true,\n ignore,\n}: {\n /** The subtree to listen in — normally the panel. */\n container: RefObject<HTMLElement | null>\n onOpen: (hit: PathHit) => void\n /** Off when there is no host filesystem to open from: a link that cannot\n * resolve is worse than plain text. */\n enabled?: boolean\n /**\n * A selector for subtrees to leave alone. Anything with its own meaning for\n * the modifier needs this — a code editor's Cmd+click is go-to-definition,\n * and every identifier in it would otherwise match as a filename.\n */\n ignore?: string\n}) {\n useEffect(() => {\n const root = container.current\n if (!root || !enabled) return\n const excluded = (element: HTMLElement | undefined) =>\n ignore !== undefined && element?.closest(ignore) !== null && element !== undefined\n\n let marked: HTMLElement | undefined\n const unmark = () => {\n marked?.classList.remove(LINKISH)\n marked = undefined\n }\n\n const onClick = (event: MouseEvent) => {\n if (!event.metaKey && !event.ctrlKey) return\n const target = event.target instanceof HTMLElement ? event.target : undefined\n if (excluded(target)) return\n const hit = hitFor(target)\n if (!hit) return\n // Capture phase and stopped here: the row underneath is usually pressable\n // (a tool call expands), and opening a file is not expanding it.\n event.preventDefault()\n event.stopPropagation()\n unmark()\n onOpen(hit)\n }\n\n const onMove = (event: MouseEvent) => {\n if (!event.metaKey && !event.ctrlKey) return unmark()\n const element = event.target instanceof HTMLElement ? event.target : undefined\n if (element === marked) return\n unmark()\n if (!element || excluded(element) || !hitFor(element)) return\n marked = element\n element.classList.add(LINKISH)\n }\n\n // Releasing the modifier has to clear it: an underline that outlives the key\n // promises a click that will not work.\n const onKey = (event: KeyboardEvent) => {\n if (!event.metaKey && !event.ctrlKey) unmark()\n }\n\n root.addEventListener('click', onClick, true)\n root.addEventListener('mousemove', onMove)\n document.addEventListener('keydown', onKey)\n document.addEventListener('keyup', onKey)\n window.addEventListener('blur', unmark)\n return () => {\n unmark()\n root.removeEventListener('click', onClick, true)\n root.removeEventListener('mousemove', onMove)\n document.removeEventListener('keydown', onKey)\n document.removeEventListener('keyup', onKey)\n window.removeEventListener('blur', unmark)\n }\n }, [container, onOpen, enabled, ignore])\n}\n\n/**\n * A transcript path against the session's cwd.\n *\n * Absolute wins outright. Everything else is relative to where the agent is\n * working, which is the only root that makes `worker.mjs` mean anything.\n */\nexport function resolveAgainstCwd(path: string, cwd: string | undefined): string {\n if (path.startsWith('/')) return path\n if (!cwd) return path\n return `${cwd.replace(/\\/$/, '')}/${path.replace(/^\\.\\//, '')}`\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'\nimport { resolveAgainstCwd, usePathLinks } from './use-path-links.ts'\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 transcriptFont?: SessionPanelProps['transcriptFont']\n /** The overview ruler in place of the scrollbar — see `SessionPanel`. */\n scrubber?: SessionPanelProps['scrubber']\n scrubberMarks?: SessionPanelProps['scrubberMarks']\n /** The last prompt pinned above the transcript — see `SessionPanel`. */\n stickyPrompt?: SessionPanelProps['stickyPrompt']\n /** Which end of the panel the status bar sits at — see `SessionPanel`. */\n statusPlacement?: SessionPanelProps['statusPlacement']\n controlsSurface?: SessionPanelProps['controlsSurface']\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 transcriptFont,\n scrubber,\n scrubberMarks,\n stickyPrompt,\n statusPlacement,\n controlsSurface,\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\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 // Cmd/Ctrl+click a file the agent named and it opens in the editor above —\n // the workspace is the only surface that *has* an editor, which is why this\n // lives here and not in the panel. Gated on the tree being available: without\n // a host filesystem there is nothing to open, and a link that cannot resolve\n // is worse than plain text. Monaco is excluded because Cmd+click already\n // means go-to-definition in there, and every identifier would match.\n const openPath = useCallback(\n ({ path }: { path: string }) => files.open(resolveAgainstCwd(path, cwd)),\n [files.open, cwd],\n )\n usePathLinks({\n container: column,\n onOpen: openPath,\n enabled: tree.available,\n ignore: '.monaco-editor',\n })\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 defaultValue={defaultRailWidth}\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 transcriptFont={transcriptFont}\n scrubber={scrubber}\n scrubberMarks={scrubberMarks}\n stickyPrompt={stickyPrompt}\n controlsSurface={controlsSurface}\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.15.0",
3
+ "version": "0.16.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.15.0",
42
- "@workerdeck/protocol": "0.15.0",
43
- "@workerdeck/react": "0.15.0"
41
+ "@workerdeck/client": "0.16.0",
42
+ "@workerdeck/protocol": "0.16.0",
43
+ "@workerdeck/react": "0.16.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "monaco-editor": "^0.56.0",
@@ -53,14 +53,20 @@
53
53
  }
54
54
  },
55
55
  "devDependencies": {
56
+ "@fontsource/inter": "^5.2.8",
57
+ "@fontsource/jetbrains-mono": "^5.2.8",
58
+ "@tailwindcss/vite": "^4.3.1",
56
59
  "@types/node": "^22.10.0",
57
- "monaco-editor": "^0.56.0",
58
60
  "@types/react": "^19.2.0",
59
61
  "@types/react-dom": "^19.2.0",
62
+ "@vitejs/plugin-react-swc": "^3.9.0",
63
+ "monaco-editor": "^0.56.0",
60
64
  "react": "^19.2.0",
61
65
  "react-dom": "^19.2.0",
62
66
  "rimraf": "^6.1.3",
63
- "tsdown": "^0.21.10"
67
+ "tailwindcss": "^4.3.1",
68
+ "tsdown": "^0.21.10",
69
+ "vite": "^6.3.0"
64
70
  },
65
71
  "author": "Tobias Strebitzer",
66
72
  "repository": {
@@ -86,6 +92,7 @@
86
92
  "scripts": {
87
93
  "clean": "rimraf build",
88
94
  "build": "tsdown",
89
- "typecheck": "tsgo -p tsconfig.json"
95
+ "typecheck": "tsgo -p tsconfig.json",
96
+ "dev": "vite"
90
97
  }
91
98
  }