@workerdeck/ui 0.22.0 → 1.0.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 (145) hide show
  1. package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +80 -24
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -124
  14. package/build/workspace.mjs +13 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +4 -6
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +345 -923
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +108 -215
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +136 -19
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-13l25ubU.d.mts +0 -609
  143. package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { r as SessionPanelProps } from "./SessionPanel-13l25ubU.mjs";
1
+ import { r as SessionPanelProps } from "./SessionPanel-E_L_ldQ5.mjs";
2
2
  import * as _$react from "react";
3
3
  import { CSSProperties } from "react";
4
4
  import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
@@ -8,80 +8,32 @@ import { WorkerDeckClient } from "@workerdeck/client";
8
8
  interface SessionWorkspaceProps {
9
9
  client: WorkerDeckClient;
10
10
  sessionId: string | undefined;
11
- /** Passed straight through to {@link SessionPanel} — including the render-prop
12
- * form that claims the session-actions menu. */
13
11
  header?: SessionPanelProps['header'];
14
- /**
15
- * Panel seams the workspace does not interpret, forwarded verbatim.
16
- *
17
- * They are listed rather than spread so the workspace stays explicit about
18
- * what it passes on: the panel owns the session's one attach, and a seam that
19
- * silently arrived here would be a second place to look for why a session
20
- * renders the way it does.
21
- */
22
12
  transcriptVariant?: SessionPanelProps['transcriptVariant'];
23
13
  transcriptDensity?: SessionPanelProps['transcriptDensity'];
24
14
  transcriptFont?: SessionPanelProps['transcriptFont'];
25
- /** The overview ruler in place of the scrollbar — see `SessionPanel`. */
26
15
  scrubber?: SessionPanelProps['scrubber'];
27
- scrubberMarks?: SessionPanelProps['scrubberMarks'];
28
- /** The last prompt pinned above the transcript — see `SessionPanel`. */
16
+ bookmarks?: SessionPanelProps['bookmarks'];
17
+ onToggleBookmark?: SessionPanelProps['onToggleBookmark'];
29
18
  stickyPrompt?: SessionPanelProps['stickyPrompt'];
30
- /** Take the panel body over with one sub-agent's work — see `SessionPanel`. */
31
19
  openSubagent?: SessionPanelProps['openSubagent'];
32
- /** Travel to a row in the conversation without framing anything — where a
33
- * **task** press lands, as opposed to `openSubagent`'s takeover. See
34
- * `SessionPanel`. */
35
20
  reveal?: SessionPanelProps['reveal'];
36
- /** The outward half of `openSubagent`: which sub-agent the panel now has
37
- * framed, or `undefined` for the conversation — see `SessionPanel`. */
38
21
  onSubagentChange?: SessionPanelProps['onSubagentChange'];
39
- /** Which end of the panel the status bar sits at — see `SessionPanel`. */
40
22
  statusPlacement?: SessionPanelProps['statusPlacement'];
41
23
  controlsSurface?: SessionPanelProps['controlsSurface'];
24
+ fontSize?: SessionPanelProps['fontSize'];
25
+ onLinkClick?: SessionPanelProps['onLinkClick'];
42
26
  unseen?: SessionPanelProps['unseen'];
43
- /** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
44
- * {@link SessionPanel}; the file tree and editor are unaffected, since reading
45
- * a run's files is the point of a read-only view. */
46
27
  readOnly?: SessionPanelProps['readOnly'];
47
28
  onVitals?: SessionPanelProps['onVitals'];
48
- /** Rail width in pixels on first render. */
49
29
  defaultRailWidth?: number;
50
- /** Start with the file rail collapsed even on a wide viewport. */
51
30
  defaultRailCollapsed?: boolean;
52
- /**
53
- * The rail moved. Paired with the two defaults so an embedder can persist the
54
- * layout — the workspace deliberately does not, because *where* to keep it (a
55
- * Memento, localStorage, a workspace file) is the embedder's call, and a
56
- * component that picked one would be wrong in the other hosts.
57
- */
58
31
  onRailChange?: (rail: {
59
32
  width: number;
60
33
  collapsed: boolean;
61
34
  }) => void;
62
35
  className?: string;
63
36
  }
64
- /**
65
- * A VS Code-shaped workspace around a live session: file tree on the left, open
66
- * files above, the agent below.
67
- *
68
- * **Strictly additive.** {@link SessionPanel} is untouched and still the whole
69
- * session surface on its own — an embedder picks one or the other, and one that
70
- * has its own file tree keeps using the panel.
71
- *
72
- * Two things here are load-bearing and easy to break:
73
- *
74
- * 1. **The editor region is absent from the layout when nothing is open**, not
75
- * collapsed to zero height. A zero-height pane leaves a draggable splitter and
76
- * parks the composer at an odd offset; absence is what makes the agent's
77
- * "claims the full column" state actually look like the panel alone.
78
- * 2. **`SessionPanel` keeps its position in the tree across that transition.** It
79
- * holds the WebSocket attach and the entire transcript, so moving it between
80
- * parents — or wrapping it conditionally — would remount it and drop the
81
- * session's rendered history on the floor the first time someone opens a file.
82
- * The conditional children before it are `? :` expressions that leave a null in
83
- * their slot, which is exactly what keeps its index stable.
84
- */
85
37
  declare function SessionWorkspace({
86
38
  client,
87
39
  sessionId,
@@ -90,13 +42,16 @@ declare function SessionWorkspace({
90
42
  transcriptDensity,
91
43
  transcriptFont,
92
44
  scrubber,
93
- scrubberMarks,
45
+ bookmarks,
46
+ onToggleBookmark,
94
47
  stickyPrompt,
95
48
  openSubagent,
96
49
  reveal,
97
50
  onSubagentChange,
98
51
  statusPlacement,
99
52
  controlsSurface,
53
+ fontSize,
54
+ onLinkClick,
100
55
  unseen,
101
56
  readOnly,
102
57
  onVitals,
@@ -108,30 +63,14 @@ declare function SessionWorkspace({
108
63
  //#endregion
109
64
  //#region src/components/agent/FileTree.d.ts
110
65
  interface FileTreeProps {
111
- /** Tree state from `useHostFileTree` — this component holds none of its own. */
112
66
  tree: UseHostFileTreeResult;
113
- /** Optional search from `useHostFileSearch`; omit and the box is not offered. */
114
67
  search?: UseHostFileSearchResult;
115
- /** Path of the focused file, highlighted in the tree. */
116
68
  activePath?: string;
117
69
  onOpenFile: (path: string) => void;
118
- /** Offered as a button in the header when given. */
119
70
  onCollapse?: () => void;
120
71
  style?: CSSProperties;
121
72
  className?: string;
122
73
  }
123
- /**
124
- * The workspace's left rail: an expandable tree of the session's project,
125
- * with a search box over the same fuzzy route `@file` completion uses.
126
- *
127
- * Presentational by construction — every piece of state it renders comes from
128
- * the hooks in `@workerdeck/react`, and the only thing it owns is the search
129
- * query, which is the text in its own input.
130
- *
131
- * Searching replaces the tree with matches rather than filtering it: the route
132
- * answers with paths from all over the project, and threading those back into
133
- * tree positions would mean expanding a dozen directories to show six results.
134
- */
135
74
  declare function FileTree({
136
75
  tree,
137
76
  search,
@@ -150,19 +89,6 @@ interface EditorTabsProps {
150
89
  onClose: (path: string) => void;
151
90
  className?: string;
152
91
  }
153
- /**
154
- * The open-file tab strip.
155
- *
156
- * Hand-rolled rather than built on `@base-ui/react`'s `Tabs`: a VS Code tab
157
- * carries a close button, and a `<button>` inside a `<button>` is invalid HTML —
158
- * getting the primitive to render something else costs more than the roving
159
- * tabindex it would have provided. So that part is here, explicitly, along with
160
- * the two affordances that actually make a tab strip feel right: middle-click to
161
- * close, and the active tab scrolling itself into view.
162
- *
163
- * Deliberately state-free. Which files are open, which is focused and what
164
- * closing does are all decided by `useOpenFiles`.
165
- */
166
92
  declare function EditorTabs({
167
93
  files,
168
94
  activePath,
@@ -174,27 +100,15 @@ declare function EditorTabs({
174
100
  //#region src/components/agent/FileViewer.d.ts
175
101
  interface FileViewerProps {
176
102
  file: OpenFile | undefined;
177
- /** From `/fs/roots`. False renders the editor read-only rather than letting
178
- * someone type into a file this gateway will refuse to write. */
179
103
  canWrite?: boolean;
180
104
  onChange?: (path: string, content: string) => void;
181
105
  onSave?: (path: string) => void;
182
- /** Discard this tab's edits — local only, no re-read. */
183
106
  onRevert?: (path: string) => void;
184
- /** Take the version on disk, discarding this tab's edits. */
185
107
  onReload?: (path: string) => void;
186
- /** Take this tab's version, over whatever is on disk now. */
187
108
  onOverwrite?: (path: string) => void;
188
109
  onDismissConflict?: (path: string) => void;
189
110
  className?: string;
190
111
  }
191
- /**
192
- * The focused file: Monaco, plus the states a file can be in that are not
193
- * "here is some text".
194
- *
195
- * No path row — the tab's tooltip carries the path and the size, and a line of
196
- * monospace above every file is chrome that never earns its height.
197
- */
198
112
  declare function FileViewer({
199
113
  file,
200
114
  canWrite,
@@ -209,42 +123,13 @@ declare function FileViewer({
209
123
  //#endregion
210
124
  //#region src/components/agent/CodeEditor.d.ts
211
125
  interface CodeEditorProps {
212
- /** Absolute path — decides the language and identifies the model. */
213
126
  path: string;
214
- /** Text to show. Applied to the model when it differs from what is on screen,
215
- * so an external reload lands without fighting the user's cursor. */
216
127
  value: string;
217
128
  onChange?: (value: string) => void;
218
- /** Ctrl/Cmd+S. Wired inside Monaco because the editor swallows keydown. */
219
129
  onSave?: () => void;
220
130
  readOnly?: boolean;
221
131
  className?: string;
222
132
  }
223
- /**
224
- * Monaco — VS Code's own editor — behind a small React surface.
225
- *
226
- * Two deliberate choices, both about `@workerdeck/ui` being a **published
227
- * library** rather than an app:
228
- *
229
- * 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a
230
- * separate chunk that arrives when someone first opens a file. A dashboard
231
- * that never opens one never pays for it, and Monaco's ~90 language grammars
232
- * are themselves lazy (each `registerLanguage` carries an `import()` loader),
233
- * so opening a `.ts` file fetches the TypeScript grammar and nothing else.
234
- * 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers
235
- * in a library become every embedder's bootstrapping problem, and
236
- * `packages/web` ships prebuilt static files at a domain root, which is
237
- * exactly where hardcoded worker URLs break. The editor is configured so it
238
- * never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,
239
- * no diff editor. A host that wants the worker-backed language services
240
- * (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`
241
- * itself before the first file is opened — Monaco is a singleton and nothing
242
- * here fights that.
243
- *
244
- * One model per path, kept across tab switches, so undo history and view state
245
- * survive clicking away and back — which is most of what makes tabs feel like
246
- * tabs rather than like re-opening a file.
247
- */
248
133
  declare function CodeEditor({
249
134
  path,
250
135
  value,
@@ -1,24 +1,11 @@
1
- import { Ht as Button, Wt as cn, _t as Tip, ft as Splitter, mt as Spinner, t as SessionPanel, yt as TooltipProvider, zt as Input } from "./SessionPanel-DgwjH4Ve.mjs";
2
- import { d as formatBytes } from "./status-BE-zg88x.mjs";
1
+ import { Bt as Input, Gt as cn, R as EntryIcon, Ut as Button, bt as TooltipProvider, ht as Spinner, pt as Splitter, t as SessionPanel, vt as Tip } from "./SessionPanel-DDgBkHsi.mjs";
2
+ import { f as formatBytes } from "./status-C0HSLZas.mjs";
3
3
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
- import { ChevronRight, File, FileWarning, Folder, FolderOpen, Link2, PanelLeftClose, PanelLeftOpen, RefreshCw, Search, TriangleAlert, X } from "lucide-react";
5
+ import { ChevronRight, File, FileWarning, Folder, FolderOpen, PanelLeftClose, PanelLeftOpen, RefreshCw, Search, TriangleAlert, X } from "lucide-react";
6
6
  import { currentText, isDirty, useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useSessionInfo } from "@workerdeck/react";
7
7
  import { createPortal } from "react-dom";
8
8
  //#region src/components/agent/EditorTabs.tsx
9
- /**
10
- * The open-file tab strip.
11
- *
12
- * Hand-rolled rather than built on `@base-ui/react`'s `Tabs`: a VS Code tab
13
- * carries a close button, and a `<button>` inside a `<button>` is invalid HTML —
14
- * getting the primitive to render something else costs more than the roving
15
- * tabindex it would have provided. So that part is here, explicitly, along with
16
- * the two affordances that actually make a tab strip feel right: middle-click to
17
- * close, and the active tab scrolling itself into view.
18
- *
19
- * Deliberately state-free. Which files are open, which is focused and what
20
- * closing does are all decided by `useOpenFiles`.
21
- */
22
9
  function EditorTabs({ files, activePath, onActivate, onClose, className }) {
23
10
  return /* @__PURE__ */ jsx(TooltipProvider, {
24
11
  delay: 500,
@@ -113,21 +100,7 @@ function Tab({ file, active, onActivate, onClose, onArrow }) {
113
100
  }
114
101
  //#endregion
115
102
  //#region src/components/agent/FileTree.tsx
116
- /** How far one level of nesting indents, in pixels. Inline rather than a Tailwind
117
- * class because depth is a number at runtime and `pl-${n}` is not a class. */
118
103
  const INDENT = 12;
119
- /**
120
- * The workspace's left rail: an expandable tree of the session's project,
121
- * with a search box over the same fuzzy route `@file` completion uses.
122
- *
123
- * Presentational by construction — every piece of state it renders comes from
124
- * the hooks in `@workerdeck/react`, and the only thing it owns is the search
125
- * query, which is the text in its own input.
126
- *
127
- * Searching replaces the tree with matches rather than filtering it: the route
128
- * answers with paths from all over the project, and threading those back into
129
- * tree positions would mean expanding a dozen directories to show six results.
130
- */
131
104
  function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, className }) {
132
105
  const [query, setQuery] = useState("");
133
106
  const [matches, setMatches] = useState();
@@ -246,8 +219,6 @@ function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, cla
246
219
  })]
247
220
  });
248
221
  }
249
- /** One clickable line. A directory and a file differ only in what the click does
250
- * and whether there is a chevron — visually they are the same row. */
251
222
  function Row({ label, detail, title, icon, chevron, indent = 0, active, onClick }) {
252
223
  const ref = useRef(null);
253
224
  useEffect(() => {
@@ -277,48 +248,15 @@ function Row({ label, detail, title, icon, chevron, indent = 0, active, onClick
277
248
  ]
278
249
  });
279
250
  }
280
- /** Last segment of a relative match. */
281
251
  function fileName(relative) {
282
252
  return relative.slice(relative.lastIndexOf("/") + 1);
283
253
  }
284
- /** Everything before it, or `undefined` for a file at the search root. */
285
254
  function directoryOf(relative) {
286
255
  const cut = relative.lastIndexOf("/");
287
256
  return cut === -1 ? void 0 : relative.slice(0, cut);
288
257
  }
289
- /** A symlink is reported as itself and never silently resolved — following it is
290
- * the next request's problem, and that request is refused if it escapes the roots. */
291
- function EntryIcon({ type }) {
292
- if (type === "symlink") return /* @__PURE__ */ jsx(Link2, { className: "size-3.5 shrink-0 text-fg-4" });
293
- return /* @__PURE__ */ jsx(File, { className: "size-3.5 shrink-0 text-fg-4" });
294
- }
295
258
  //#endregion
296
259
  //#region src/components/agent/CodeEditor.tsx
297
- /**
298
- * Monaco — VS Code's own editor — behind a small React surface.
299
- *
300
- * Two deliberate choices, both about `@workerdeck/ui` being a **published
301
- * library** rather than an app:
302
- *
303
- * 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a
304
- * separate chunk that arrives when someone first opens a file. A dashboard
305
- * that never opens one never pays for it, and Monaco's ~90 language grammars
306
- * are themselves lazy (each `registerLanguage` carries an `import()` loader),
307
- * so opening a `.ts` file fetches the TypeScript grammar and nothing else.
308
- * 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers
309
- * in a library become every embedder's bootstrapping problem, and
310
- * `packages/web` ships prebuilt static files at a domain root, which is
311
- * exactly where hardcoded worker URLs break. The editor is configured so it
312
- * never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,
313
- * no diff editor. A host that wants the worker-backed language services
314
- * (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`
315
- * itself before the first file is opened — Monaco is a singleton and nothing
316
- * here fights that.
317
- *
318
- * One model per path, kept across tab switches, so undo history and view state
319
- * survive clicking away and back — which is most of what makes tabs feel like
320
- * tabs rather than like re-opening a file.
321
- */
322
260
  function CodeEditor({ path, value, onChange, onSave, readOnly, className }) {
323
261
  const host = useRef(null);
324
262
  const editor = useRef(null);
@@ -417,12 +355,6 @@ function CodeEditor({ path, value, onChange, onSave, readOnly, className }) {
417
355
  }) : null]
418
356
  });
419
357
  }
420
- /**
421
- * Follow the `data-theme` the design tokens already swap on, so the editor is
422
- * never the one light rectangle in a dark app (or the reverse). An attribute
423
- * observer rather than a media query: the app has a manual toggle, and the
424
- * attribute is what that toggle writes.
425
- */
426
358
  function useDocumentTheme() {
427
359
  const [theme, setTheme] = useState(() => typeof document === "undefined" ? null : document.documentElement.getAttribute("data-theme"));
428
360
  useEffect(() => {
@@ -438,17 +370,9 @@ function useDocumentTheme() {
438
370
  }, []);
439
371
  return theme;
440
372
  }
441
- /** An unset attribute means the host never opted into the token themes, in which
442
- * case dark matches this package's default surface. */
443
373
  function monacoTheme(theme) {
444
374
  return theme === "light" ? "wd-light" : "wd-dark";
445
375
  }
446
- /**
447
- * Load Monaco once, register the themes, and hand back the API.
448
- *
449
- * Cached as a promise rather than a value so that several editors mounting in
450
- * the same frame share one load instead of racing three of them.
451
- */
452
376
  let monacoPromise;
453
377
  function loadMonaco() {
454
378
  monacoPromise ??= (async () => {
@@ -469,28 +393,18 @@ function loadMonaco() {
469
393
  })();
470
394
  return monacoPromise;
471
395
  }
472
- /**
473
- * Monaco's language id for a path.
474
- *
475
- * By extension, with a short table for the files that have none — Monaco's own
476
- * registry is keyed on extensions it knows, and an unmatched file falls through
477
- * to `plaintext`, which is the honest answer rather than a guess.
478
- */
479
396
  function languageOf(path) {
480
397
  const name = path.slice(path.lastIndexOf("/") + 1);
481
398
  const byName = FILENAME_LANGUAGES[name.toLowerCase()];
482
399
  if (byName) return byName;
483
400
  return EXTENSION_LANGUAGES[name.slice(name.lastIndexOf(".") + 1).toLowerCase()] ?? "plaintext";
484
401
  }
485
- /** Files whose type is their whole name. */
486
402
  const FILENAME_LANGUAGES = {
487
403
  dockerfile: "dockerfile",
488
404
  makefile: "plaintext",
489
405
  ".gitignore": "plaintext",
490
406
  ".env": "shell"
491
407
  };
492
- /** Extension → Monaco language id. Only where the id differs from the extension
493
- * or the extension is ambiguous; everything else Monaco resolves itself. */
494
408
  const EXTENSION_LANGUAGES = {
495
409
  ts: "typescript",
496
410
  tsx: "typescript",
@@ -548,13 +462,6 @@ const EXTENSION_LANGUAGES = {
548
462
  };
549
463
  //#endregion
550
464
  //#region src/components/agent/FileViewer.tsx
551
- /**
552
- * The focused file: Monaco, plus the states a file can be in that are not
553
- * "here is some text".
554
- *
555
- * No path row — the tab's tooltip carries the path and the size, and a line of
556
- * monospace above every file is chrome that never earns its height.
557
- */
558
465
  function FileViewer({ file, canWrite, onChange, onSave, onRevert, onReload, onOverwrite, onDismissConflict, className }) {
559
466
  if (!file) return null;
560
467
  return /* @__PURE__ */ jsxs("div", {
@@ -666,32 +573,27 @@ function Centred({ children }) {
666
573
  }
667
574
  //#endregion
668
575
  //#region src/components/agent/use-path-links.ts
669
- /** Marks the element under the pointer while the modifier is held. */
670
576
  const LINKISH = "wd-path-link";
671
- /** Absolute, or relative with at least one separator. `:12` is a line number. */
672
577
  const PATH_RE = /^(\/[^\s:'"`()[\]{}]+|[\w.@~-]+(?:\/[\w.@~-]+)+)(?::(\d+))?$/;
673
- /** A bare filename — only trusted inside a code element. */
674
578
  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
579
  const COVERAGE = .6;
677
580
  function matchPath(text, inCode) {
678
581
  const trimmed = text.trim();
679
- if (!trimmed || trimmed.endsWith("/")) return void 0;
582
+ if (!trimmed || trimmed.endsWith("/")) return;
680
583
  const hit = PATH_RE.exec(trimmed) ?? (inCode ? FILE_RE.exec(trimmed) : null);
681
- if (!hit) return void 0;
584
+ if (!hit) return;
682
585
  return {
683
586
  path: hit[1],
684
587
  line: hit[2] ? Number(hit[2]) : void 0
685
588
  };
686
589
  }
687
- /** The path a click on this element means, if any. */
688
590
  function hitFor(element) {
689
- if (!element) return void 0;
591
+ if (!element) return;
690
592
  const inCode = element.closest("code") !== null;
691
593
  const text = element.textContent ?? "";
692
594
  const hit = matchPath(text, inCode);
693
- if (!hit) return void 0;
694
- if (!inCode && hit.path.length < text.trim().length * COVERAGE) return void 0;
595
+ if (!hit) return;
596
+ if (!inCode && hit.path.length < text.trim().length * COVERAGE) return;
695
597
  return hit;
696
598
  }
697
599
  function usePathLinks({ container, onOpen, enabled = true, ignore }) {
@@ -747,12 +649,6 @@ function usePathLinks({ container, onOpen, enabled = true, ignore }) {
747
649
  ignore
748
650
  ]);
749
651
  }
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
652
  function resolveAgainstCwd(path, cwd) {
757
653
  if (path.startsWith("/")) return path;
758
654
  if (!cwd) return path;
@@ -762,33 +658,9 @@ function resolveAgainstCwd(path, cwd) {
762
658
  //#region src/components/agent/SessionWorkspace.tsx
763
659
  const RAIL_MIN = 180;
764
660
  const RAIL_MAX = 520;
765
- /** How little of the agent column the editor may leave. Below this the composer
766
- * and a line of transcript stop fitting together, which is the point at which the
767
- * agent has stopped being usable rather than merely small. */
768
661
  const AGENT_MIN = 220;
769
662
  const EDITOR_MIN = 120;
770
- /**
771
- * A VS Code-shaped workspace around a live session: file tree on the left, open
772
- * files above, the agent below.
773
- *
774
- * **Strictly additive.** {@link SessionPanel} is untouched and still the whole
775
- * session surface on its own — an embedder picks one or the other, and one that
776
- * has its own file tree keeps using the panel.
777
- *
778
- * Two things here are load-bearing and easy to break:
779
- *
780
- * 1. **The editor region is absent from the layout when nothing is open**, not
781
- * collapsed to zero height. A zero-height pane leaves a draggable splitter and
782
- * parks the composer at an odd offset; absence is what makes the agent's
783
- * "claims the full column" state actually look like the panel alone.
784
- * 2. **`SessionPanel` keeps its position in the tree across that transition.** It
785
- * holds the WebSocket attach and the entire transcript, so moving it between
786
- * parents — or wrapping it conditionally — would remount it and drop the
787
- * session's rendered history on the floor the first time someone opens a file.
788
- * The conditional children before it are `? :` expressions that leave a null in
789
- * their slot, which is exactly what keeps its index stable.
790
- */
791
- function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, transcriptFont, scrubber, scrubberMarks, stickyPrompt, openSubagent, reveal, onSubagentChange, statusPlacement, controlsSurface, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
663
+ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, transcriptFont, scrubber, bookmarks, onToggleBookmark, stickyPrompt, openSubagent, reveal, onSubagentChange, statusPlacement, controlsSurface, fontSize, onLinkClick, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
792
664
  const { info } = useSessionInfo(client, sessionId);
793
665
  const cwd = info?.cwd;
794
666
  const tree = useHostFileTree(client, cwd);
@@ -913,12 +785,15 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
913
785
  transcriptDensity,
914
786
  transcriptFont,
915
787
  scrubber,
916
- scrubberMarks,
788
+ bookmarks,
789
+ onToggleBookmark,
917
790
  stickyPrompt,
918
791
  openSubagent,
919
792
  reveal,
920
793
  onSubagentChange,
921
794
  controlsSurface,
795
+ fontSize,
796
+ onLinkClick,
922
797
  statusPlacement,
923
798
  unseen,
924
799
  readOnly,
@@ -937,9 +812,6 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
937
812
  })]
938
813
  });
939
814
  }
940
- /** Live height of an element, for a splitter that needs to know how much room it
941
- * is dividing. Zero until the first observation, which callers treat as
942
- * "unmeasured" rather than as a real bound. */
943
815
  function useElementHeight(ref) {
944
816
  const [height, setHeight] = useState(0);
945
817
  useLayoutEffect(() => {
@@ -953,8 +825,6 @@ function useElementHeight(ref) {
953
825
  }, [ref]);
954
826
  return height;
955
827
  }
956
- /** Whether there is room for a rail beside the content. Presentation only — the
957
- * workspace's actual state lives in the hooks from `@workerdeck/react`. */
958
828
  function useIsWide() {
959
829
  const [wide, setWide] = useState(true);
960
830
  useEffect(() => {