@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.
- package/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +80 -24
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +9 -124
- package/build/workspace.mjs +13 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +4 -6
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +345 -923
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +108 -215
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +136 -19
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-13l25ubU.d.mts +0 -609
- package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SessionPanelProps } from "./SessionPanel-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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,
|
package/build/workspace.mjs
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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,
|
|
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
|
|
582
|
+
if (!trimmed || trimmed.endsWith("/")) return;
|
|
680
583
|
const hit = PATH_RE.exec(trimmed) ?? (inCode ? FILE_RE.exec(trimmed) : null);
|
|
681
|
-
if (!hit) return
|
|
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
|
|
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
|
|
694
|
-
if (!inCode && hit.path.length < text.trim().length * COVERAGE) return
|
|
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
|
-
|
|
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(() => {
|