@workerdeck/ui 0.23.0 → 1.1.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-DUt2VzXG.mjs → SessionPanel-I5QSLLb0.mjs} +2266 -3459
  2. package/build/SessionPanel-I5QSLLb0.mjs.map +1 -0
  3. package/build/SessionPanel-yNO60Tzw.d.mts +151 -0
  4. package/build/format-C_lpFq0l.d.mts +16 -0
  5. package/build/format.d.mts +8 -76
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +291 -1695
  8. package/build/index.mjs +6 -301
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +32 -23
  11. package/build/{status-BE-zg88x.mjs → status-Dm9INVTt.mjs} +17 -88
  12. package/build/status-Dm9INVTt.mjs.map +1 -0
  13. package/build/workspace.d.mts +8 -187
  14. package/build/workspace.mjs +13 -144
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +24 -24
  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 +3 -9
  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 +315 -996
  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 +104 -219
  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 +96 -32
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-BobynUo4.d.mts +0 -669
  143. package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. package/build/status-BE-zg88x.mjs.map +0 -1
@@ -1,152 +1,50 @@
1
- import { r as SessionPanelProps } from "./SessionPanel-BobynUo4.mjs";
2
- import * as _$react from "react";
1
+ import { r as SessionPanelProps } from "./SessionPanel-yNO60Tzw.mjs";
3
2
  import { CSSProperties } from "react";
4
3
  import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
5
4
  import { WorkerDeckClient } from "@workerdeck/client";
6
-
7
5
  //#region src/components/agent/SessionWorkspace.d.ts
8
6
  interface SessionWorkspaceProps {
9
7
  client: WorkerDeckClient;
10
8
  sessionId: string | undefined;
11
- /** Passed straight through to {@link SessionPanel} — including the render-prop
12
- * form that claims the session-actions menu. */
13
9
  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
10
  transcriptVariant?: SessionPanelProps['transcriptVariant'];
23
11
  transcriptDensity?: SessionPanelProps['transcriptDensity'];
24
12
  transcriptFont?: SessionPanelProps['transcriptFont'];
25
- /** The overview ruler in place of the scrollbar — see `SessionPanel`. */
26
13
  scrubber?: SessionPanelProps['scrubber'];
27
- scrubberMarks?: SessionPanelProps['scrubberMarks'];
28
- /** The last prompt pinned above the transcript — see `SessionPanel`. */
14
+ bookmarks?: SessionPanelProps['bookmarks'];
15
+ onToggleBookmark?: SessionPanelProps['onToggleBookmark'];
29
16
  stickyPrompt?: SessionPanelProps['stickyPrompt'];
30
- /** Take the panel body over with one sub-agent's work — see `SessionPanel`. */
31
17
  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
18
  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
19
  onSubagentChange?: SessionPanelProps['onSubagentChange'];
39
- /** Which end of the panel the status bar sits at — see `SessionPanel`. */
40
20
  statusPlacement?: SessionPanelProps['statusPlacement'];
41
21
  controlsSurface?: SessionPanelProps['controlsSurface'];
42
- /** Base font size in whole pixels — see `SessionPanel.fontSize`. */
43
22
  fontSize?: SessionPanelProps['fontSize'];
44
- /** Link click handler — see `SessionPanel.onLinkClick`. */
45
23
  onLinkClick?: SessionPanelProps['onLinkClick'];
46
24
  unseen?: SessionPanelProps['unseen'];
47
- /** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
48
- * {@link SessionPanel}; the file tree and editor are unaffected, since reading
49
- * a run's files is the point of a read-only view. */
50
25
  readOnly?: SessionPanelProps['readOnly'];
51
26
  onVitals?: SessionPanelProps['onVitals'];
52
- /** Rail width in pixels on first render. */
53
27
  defaultRailWidth?: number;
54
- /** Start with the file rail collapsed even on a wide viewport. */
55
28
  defaultRailCollapsed?: boolean;
56
- /**
57
- * The rail moved. Paired with the two defaults so an embedder can persist the
58
- * layout — the workspace deliberately does not, because *where* to keep it (a
59
- * Memento, localStorage, a workspace file) is the embedder's call, and a
60
- * component that picked one would be wrong in the other hosts.
61
- */
62
29
  onRailChange?: (rail: {
63
30
  width: number;
64
31
  collapsed: boolean;
65
32
  }) => void;
66
33
  className?: string;
67
34
  }
68
- /**
69
- * A VS Code-shaped workspace around a live session: file tree on the left, open
70
- * files above, the agent below.
71
- *
72
- * **Strictly additive.** {@link SessionPanel} is untouched and still the whole
73
- * session surface on its own — an embedder picks one or the other, and one that
74
- * has its own file tree keeps using the panel.
75
- *
76
- * Two things here are load-bearing and easy to break:
77
- *
78
- * 1. **The editor region is absent from the layout when nothing is open**, not
79
- * collapsed to zero height. A zero-height pane leaves a draggable splitter and
80
- * parks the composer at an odd offset; absence is what makes the agent's
81
- * "claims the full column" state actually look like the panel alone.
82
- * 2. **`SessionPanel` keeps its position in the tree across that transition.** It
83
- * holds the WebSocket attach and the entire transcript, so moving it between
84
- * parents — or wrapping it conditionally — would remount it and drop the
85
- * session's rendered history on the floor the first time someone opens a file.
86
- * The conditional children before it are `? :` expressions that leave a null in
87
- * their slot, which is exactly what keeps its index stable.
88
- */
89
- declare function SessionWorkspace({
90
- client,
91
- sessionId,
92
- header,
93
- transcriptVariant,
94
- transcriptDensity,
95
- transcriptFont,
96
- scrubber,
97
- scrubberMarks,
98
- stickyPrompt,
99
- openSubagent,
100
- reveal,
101
- onSubagentChange,
102
- statusPlacement,
103
- controlsSurface,
104
- fontSize,
105
- onLinkClick,
106
- unseen,
107
- readOnly,
108
- onVitals,
109
- defaultRailWidth,
110
- defaultRailCollapsed,
111
- onRailChange,
112
- className
113
- }: SessionWorkspaceProps): _$react.JSX.Element;
35
+ declare function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, transcriptFont, scrubber, bookmarks, onToggleBookmark, stickyPrompt, openSubagent, reveal, onSubagentChange, statusPlacement, controlsSurface, fontSize, onLinkClick, unseen, readOnly, onVitals, defaultRailWidth, defaultRailCollapsed, onRailChange, className }: SessionWorkspaceProps): import("react").JSX.Element;
114
36
  //#endregion
115
37
  //#region src/components/agent/FileTree.d.ts
116
38
  interface FileTreeProps {
117
- /** Tree state from `useHostFileTree` — this component holds none of its own. */
118
39
  tree: UseHostFileTreeResult;
119
- /** Optional search from `useHostFileSearch`; omit and the box is not offered. */
120
40
  search?: UseHostFileSearchResult;
121
- /** Path of the focused file, highlighted in the tree. */
122
41
  activePath?: string;
123
42
  onOpenFile: (path: string) => void;
124
- /** Offered as a button in the header when given. */
125
43
  onCollapse?: () => void;
126
44
  style?: CSSProperties;
127
45
  className?: string;
128
46
  }
129
- /**
130
- * The workspace's left rail: an expandable tree of the session's project,
131
- * with a search box over the same fuzzy route `@file` completion uses.
132
- *
133
- * Presentational by construction — every piece of state it renders comes from
134
- * the hooks in `@workerdeck/react`, and the only thing it owns is the search
135
- * query, which is the text in its own input.
136
- *
137
- * Searching replaces the tree with matches rather than filtering it: the route
138
- * answers with paths from all over the project, and threading those back into
139
- * tree positions would mean expanding a dozen directories to show six results.
140
- */
141
- declare function FileTree({
142
- tree,
143
- search,
144
- activePath,
145
- onOpenFile,
146
- onCollapse,
147
- style,
148
- className
149
- }: FileTreeProps): _$react.JSX.Element;
47
+ declare function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, className }: FileTreeProps): import("react").JSX.Element;
150
48
  //#endregion
151
49
  //#region src/components/agent/EditorTabs.d.ts
152
50
  interface EditorTabsProps {
@@ -156,109 +54,32 @@ interface EditorTabsProps {
156
54
  onClose: (path: string) => void;
157
55
  className?: string;
158
56
  }
159
- /**
160
- * The open-file tab strip.
161
- *
162
- * Hand-rolled rather than built on `@base-ui/react`'s `Tabs`: a VS Code tab
163
- * carries a close button, and a `<button>` inside a `<button>` is invalid HTML —
164
- * getting the primitive to render something else costs more than the roving
165
- * tabindex it would have provided. So that part is here, explicitly, along with
166
- * the two affordances that actually make a tab strip feel right: middle-click to
167
- * close, and the active tab scrolling itself into view.
168
- *
169
- * Deliberately state-free. Which files are open, which is focused and what
170
- * closing does are all decided by `useOpenFiles`.
171
- */
172
- declare function EditorTabs({
173
- files,
174
- activePath,
175
- onActivate,
176
- onClose,
177
- className
178
- }: EditorTabsProps): _$react.JSX.Element;
57
+ declare function EditorTabs({ files, activePath, onActivate, onClose, className }: EditorTabsProps): import("react").JSX.Element;
179
58
  //#endregion
180
59
  //#region src/components/agent/FileViewer.d.ts
181
60
  interface FileViewerProps {
182
61
  file: OpenFile | undefined;
183
- /** From `/fs/roots`. False renders the editor read-only rather than letting
184
- * someone type into a file this gateway will refuse to write. */
185
62
  canWrite?: boolean;
186
63
  onChange?: (path: string, content: string) => void;
187
64
  onSave?: (path: string) => void;
188
- /** Discard this tab's edits — local only, no re-read. */
189
65
  onRevert?: (path: string) => void;
190
- /** Take the version on disk, discarding this tab's edits. */
191
66
  onReload?: (path: string) => void;
192
- /** Take this tab's version, over whatever is on disk now. */
193
67
  onOverwrite?: (path: string) => void;
194
68
  onDismissConflict?: (path: string) => void;
195
69
  className?: string;
196
70
  }
197
- /**
198
- * The focused file: Monaco, plus the states a file can be in that are not
199
- * "here is some text".
200
- *
201
- * No path row — the tab's tooltip carries the path and the size, and a line of
202
- * monospace above every file is chrome that never earns its height.
203
- */
204
- declare function FileViewer({
205
- file,
206
- canWrite,
207
- onChange,
208
- onSave,
209
- onRevert,
210
- onReload,
211
- onOverwrite,
212
- onDismissConflict,
213
- className
214
- }: FileViewerProps): _$react.JSX.Element | null;
71
+ declare function FileViewer({ file, canWrite, onChange, onSave, onRevert, onReload, onOverwrite, onDismissConflict, className }: FileViewerProps): import("react").JSX.Element | null;
215
72
  //#endregion
216
73
  //#region src/components/agent/CodeEditor.d.ts
217
74
  interface CodeEditorProps {
218
- /** Absolute path — decides the language and identifies the model. */
219
75
  path: string;
220
- /** Text to show. Applied to the model when it differs from what is on screen,
221
- * so an external reload lands without fighting the user's cursor. */
222
76
  value: string;
223
77
  onChange?: (value: string) => void;
224
- /** Ctrl/Cmd+S. Wired inside Monaco because the editor swallows keydown. */
225
78
  onSave?: () => void;
226
79
  readOnly?: boolean;
227
80
  className?: string;
228
81
  }
229
- /**
230
- * Monaco — VS Code's own editor — behind a small React surface.
231
- *
232
- * Two deliberate choices, both about `@workerdeck/ui` being a **published
233
- * library** rather than an app:
234
- *
235
- * 1. **Loaded on demand.** The `import()` is inside an effect, so Monaco is a
236
- * separate chunk that arrives when someone first opens a file. A dashboard
237
- * that never opens one never pays for it, and Monaco's ~90 language grammars
238
- * are themselves lazy (each `registerLanguage` carries an `import()` loader),
239
- * so opening a `.ts` file fetches the TypeScript grammar and nothing else.
240
- * 2. **No `MonacoEnvironment` is configured here, and none is needed.** Workers
241
- * in a library become every embedder's bootstrapping problem, and
242
- * `packages/web` ships prebuilt static files at a domain root, which is
243
- * exactly where hardcoded worker URLs break. The editor is configured so it
244
- * never asks for one: `wordBasedSuggestions` and `quickSuggestions` off,
245
- * no diff editor. A host that wants the worker-backed language services
246
- * (TypeScript IntelliSense, JSON schema validation) sets `MonacoEnvironment`
247
- * itself before the first file is opened — Monaco is a singleton and nothing
248
- * here fights that.
249
- *
250
- * One model per path, kept across tab switches, so undo history and view state
251
- * survive clicking away and back — which is most of what makes tabs feel like
252
- * tabs rather than like re-opening a file.
253
- */
254
- declare function CodeEditor({
255
- path,
256
- value,
257
- onChange,
258
- onSave,
259
- readOnly,
260
- className
261
- }: CodeEditorProps): _$react.JSX.Element;
82
+ declare function CodeEditor({ path, value, onChange, onSave, readOnly, className }: CodeEditorProps): import("react").JSX.Element;
262
83
  //#endregion
263
84
  export { CodeEditor, type CodeEditorProps, EditorTabs, type EditorTabsProps, FileTree, type FileTreeProps, FileViewer, type FileViewerProps, SessionWorkspace, type SessionWorkspaceProps };
264
85
  //# sourceMappingURL=workspace.d.mts.map
@@ -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-DUt2VzXG.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-I5QSLLb0.mjs";
2
+ import { f as formatBytes } from "./status-Dm9INVTt.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,19 @@ 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
- return EXTENSION_LANGUAGES[name.slice(name.lastIndexOf(".") + 1).toLowerCase()] ?? "plaintext";
400
+ const extension = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
401
+ return EXTENSION_LANGUAGES[extension] ?? "plaintext";
484
402
  }
485
- /** Files whose type is their whole name. */
486
403
  const FILENAME_LANGUAGES = {
487
404
  dockerfile: "dockerfile",
488
405
  makefile: "plaintext",
489
406
  ".gitignore": "plaintext",
490
407
  ".env": "shell"
491
408
  };
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
409
  const EXTENSION_LANGUAGES = {
495
410
  ts: "typescript",
496
411
  tsx: "typescript",
@@ -548,13 +463,6 @@ const EXTENSION_LANGUAGES = {
548
463
  };
549
464
  //#endregion
550
465
  //#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
466
  function FileViewer({ file, canWrite, onChange, onSave, onRevert, onReload, onOverwrite, onDismissConflict, className }) {
559
467
  if (!file) return null;
560
468
  return /* @__PURE__ */ jsxs("div", {
@@ -666,32 +574,27 @@ function Centred({ children }) {
666
574
  }
667
575
  //#endregion
668
576
  //#region src/components/agent/use-path-links.ts
669
- /** Marks the element under the pointer while the modifier is held. */
670
577
  const LINKISH = "wd-path-link";
671
- /** Absolute, or relative with at least one separator. `:12` is a line number. */
672
578
  const PATH_RE = /^(\/[^\s:'"`()[\]{}]+|[\w.@~-]+(?:\/[\w.@~-]+)+)(?::(\d+))?$/;
673
- /** A bare filename — only trusted inside a code element. */
674
579
  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
580
  const COVERAGE = .6;
677
581
  function matchPath(text, inCode) {
678
582
  const trimmed = text.trim();
679
- if (!trimmed || trimmed.endsWith("/")) return void 0;
583
+ if (!trimmed || trimmed.endsWith("/")) return;
680
584
  const hit = PATH_RE.exec(trimmed) ?? (inCode ? FILE_RE.exec(trimmed) : null);
681
- if (!hit) return void 0;
585
+ if (!hit) return;
682
586
  return {
683
587
  path: hit[1],
684
588
  line: hit[2] ? Number(hit[2]) : void 0
685
589
  };
686
590
  }
687
- /** The path a click on this element means, if any. */
688
591
  function hitFor(element) {
689
- if (!element) return void 0;
592
+ if (!element) return;
690
593
  const inCode = element.closest("code") !== null;
691
594
  const text = element.textContent ?? "";
692
595
  const hit = matchPath(text, inCode);
693
- if (!hit) return void 0;
694
- if (!inCode && hit.path.length < text.trim().length * COVERAGE) return void 0;
596
+ if (!hit) return;
597
+ if (!inCode && hit.path.length < text.trim().length * COVERAGE) return;
695
598
  return hit;
696
599
  }
697
600
  function usePathLinks({ container, onOpen, enabled = true, ignore }) {
@@ -747,12 +650,6 @@ function usePathLinks({ container, onOpen, enabled = true, ignore }) {
747
650
  ignore
748
651
  ]);
749
652
  }
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
653
  function resolveAgainstCwd(path, cwd) {
757
654
  if (path.startsWith("/")) return path;
758
655
  if (!cwd) return path;
@@ -762,33 +659,9 @@ function resolveAgainstCwd(path, cwd) {
762
659
  //#region src/components/agent/SessionWorkspace.tsx
763
660
  const RAIL_MIN = 180;
764
661
  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
662
  const AGENT_MIN = 220;
769
663
  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, fontSize, onLinkClick, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
664
+ 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
665
  const { info } = useSessionInfo(client, sessionId);
793
666
  const cwd = info?.cwd;
794
667
  const tree = useHostFileTree(client, cwd);
@@ -913,7 +786,8 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
913
786
  transcriptDensity,
914
787
  transcriptFont,
915
788
  scrubber,
916
- scrubberMarks,
789
+ bookmarks,
790
+ onToggleBookmark,
917
791
  stickyPrompt,
918
792
  openSubagent,
919
793
  reveal,
@@ -939,9 +813,6 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
939
813
  })]
940
814
  });
941
815
  }
942
- /** Live height of an element, for a splitter that needs to know how much room it
943
- * is dividing. Zero until the first observation, which callers treat as
944
- * "unmeasured" rather than as a real bound. */
945
816
  function useElementHeight(ref) {
946
817
  const [height, setHeight] = useState(0);
947
818
  useLayoutEffect(() => {
@@ -955,8 +826,6 @@ function useElementHeight(ref) {
955
826
  }, [ref]);
956
827
  return height;
957
828
  }
958
- /** Whether there is room for a rail beside the content. Presentation only — the
959
- * workspace's actual state lives in the hooks from `@workerdeck/react`. */
960
829
  function useIsWide() {
961
830
  const [wide, setWide] = useState(true);
962
831
  useEffect(() => {