@workerdeck/ui 0.23.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-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- 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 +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -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 +3 -9
- 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 +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- 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 +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
4
|
-
import {
|
|
5
|
-
useHostFileRoots,
|
|
6
|
-
useHostFileSearch,
|
|
7
|
-
useHostFileTree,
|
|
8
|
-
useOpenFiles,
|
|
9
|
-
useSessionInfo,
|
|
10
|
-
isDirty,
|
|
11
|
-
} from '@workerdeck/react'
|
|
4
|
+
import { useHostFileRoots, useHostFileSearch, useHostFileTree, useOpenFiles, useSessionInfo, isDirty } from '@workerdeck/react'
|
|
12
5
|
import { PanelLeftOpen } from 'lucide-react'
|
|
13
6
|
import { cn } from '../../lib/utils.ts'
|
|
14
7
|
import { Button } from '../ui/Button.tsx'
|
|
@@ -22,90 +15,35 @@ import { resolveAgainstCwd, usePathLinks } from './use-path-links.ts'
|
|
|
22
15
|
export interface SessionWorkspaceProps {
|
|
23
16
|
client: WorkerDeckClient
|
|
24
17
|
sessionId: string | undefined
|
|
25
|
-
/** Passed straight through to {@link SessionPanel} — including the render-prop
|
|
26
|
-
* form that claims the session-actions menu. */
|
|
27
18
|
header?: SessionPanelProps['header']
|
|
28
|
-
/**
|
|
29
|
-
* Panel seams the workspace does not interpret, forwarded verbatim.
|
|
30
|
-
*
|
|
31
|
-
* They are listed rather than spread so the workspace stays explicit about
|
|
32
|
-
* what it passes on: the panel owns the session's one attach, and a seam that
|
|
33
|
-
* silently arrived here would be a second place to look for why a session
|
|
34
|
-
* renders the way it does.
|
|
35
|
-
*/
|
|
36
19
|
transcriptVariant?: SessionPanelProps['transcriptVariant']
|
|
37
20
|
transcriptDensity?: SessionPanelProps['transcriptDensity']
|
|
38
21
|
transcriptFont?: SessionPanelProps['transcriptFont']
|
|
39
|
-
/** The overview ruler in place of the scrollbar — see `SessionPanel`. */
|
|
40
22
|
scrubber?: SessionPanelProps['scrubber']
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
bookmarks?: SessionPanelProps['bookmarks']
|
|
24
|
+
onToggleBookmark?: SessionPanelProps['onToggleBookmark']
|
|
43
25
|
stickyPrompt?: SessionPanelProps['stickyPrompt']
|
|
44
|
-
/** Take the panel body over with one sub-agent's work — see `SessionPanel`. */
|
|
45
26
|
openSubagent?: SessionPanelProps['openSubagent']
|
|
46
|
-
/** Travel to a row in the conversation without framing anything — where a
|
|
47
|
-
* **task** press lands, as opposed to `openSubagent`'s takeover. See
|
|
48
|
-
* `SessionPanel`. */
|
|
49
27
|
reveal?: SessionPanelProps['reveal']
|
|
50
|
-
/** The outward half of `openSubagent`: which sub-agent the panel now has
|
|
51
|
-
* framed, or `undefined` for the conversation — see `SessionPanel`. */
|
|
52
28
|
onSubagentChange?: SessionPanelProps['onSubagentChange']
|
|
53
|
-
/** Which end of the panel the status bar sits at — see `SessionPanel`. */
|
|
54
29
|
statusPlacement?: SessionPanelProps['statusPlacement']
|
|
55
30
|
controlsSurface?: SessionPanelProps['controlsSurface']
|
|
56
|
-
/** Base font size in whole pixels — see `SessionPanel.fontSize`. */
|
|
57
31
|
fontSize?: SessionPanelProps['fontSize']
|
|
58
|
-
/** Link click handler — see `SessionPanel.onLinkClick`. */
|
|
59
32
|
onLinkClick?: SessionPanelProps['onLinkClick']
|
|
60
33
|
unseen?: SessionPanelProps['unseen']
|
|
61
|
-
/** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
|
|
62
|
-
* {@link SessionPanel}; the file tree and editor are unaffected, since reading
|
|
63
|
-
* a run's files is the point of a read-only view. */
|
|
64
34
|
readOnly?: SessionPanelProps['readOnly']
|
|
65
35
|
onVitals?: SessionPanelProps['onVitals']
|
|
66
|
-
/** Rail width in pixels on first render. */
|
|
67
36
|
defaultRailWidth?: number
|
|
68
|
-
/** Start with the file rail collapsed even on a wide viewport. */
|
|
69
37
|
defaultRailCollapsed?: boolean
|
|
70
|
-
/**
|
|
71
|
-
* The rail moved. Paired with the two defaults so an embedder can persist the
|
|
72
|
-
* layout — the workspace deliberately does not, because *where* to keep it (a
|
|
73
|
-
* Memento, localStorage, a workspace file) is the embedder's call, and a
|
|
74
|
-
* component that picked one would be wrong in the other hosts.
|
|
75
|
-
*/
|
|
76
38
|
onRailChange?: (rail: { width: number; collapsed: boolean }) => void
|
|
77
39
|
className?: string
|
|
78
40
|
}
|
|
79
41
|
|
|
80
42
|
const RAIL_MIN = 180
|
|
81
43
|
const RAIL_MAX = 520
|
|
82
|
-
/** How little of the agent column the editor may leave. Below this the composer
|
|
83
|
-
* and a line of transcript stop fitting together, which is the point at which the
|
|
84
|
-
* agent has stopped being usable rather than merely small. */
|
|
85
44
|
const AGENT_MIN = 220
|
|
86
45
|
const EDITOR_MIN = 120
|
|
87
46
|
|
|
88
|
-
/**
|
|
89
|
-
* A VS Code-shaped workspace around a live session: file tree on the left, open
|
|
90
|
-
* files above, the agent below.
|
|
91
|
-
*
|
|
92
|
-
* **Strictly additive.** {@link SessionPanel} is untouched and still the whole
|
|
93
|
-
* session surface on its own — an embedder picks one or the other, and one that
|
|
94
|
-
* has its own file tree keeps using the panel.
|
|
95
|
-
*
|
|
96
|
-
* Two things here are load-bearing and easy to break:
|
|
97
|
-
*
|
|
98
|
-
* 1. **The editor region is absent from the layout when nothing is open**, not
|
|
99
|
-
* collapsed to zero height. A zero-height pane leaves a draggable splitter and
|
|
100
|
-
* parks the composer at an odd offset; absence is what makes the agent's
|
|
101
|
-
* "claims the full column" state actually look like the panel alone.
|
|
102
|
-
* 2. **`SessionPanel` keeps its position in the tree across that transition.** It
|
|
103
|
-
* holds the WebSocket attach and the entire transcript, so moving it between
|
|
104
|
-
* parents — or wrapping it conditionally — would remount it and drop the
|
|
105
|
-
* session's rendered history on the floor the first time someone opens a file.
|
|
106
|
-
* The conditional children before it are `? :` expressions that leave a null in
|
|
107
|
-
* their slot, which is exactly what keeps its index stable.
|
|
108
|
-
*/
|
|
109
47
|
export function SessionWorkspace({
|
|
110
48
|
client,
|
|
111
49
|
sessionId,
|
|
@@ -114,7 +52,8 @@ export function SessionWorkspace({
|
|
|
114
52
|
transcriptDensity,
|
|
115
53
|
transcriptFont,
|
|
116
54
|
scrubber,
|
|
117
|
-
|
|
55
|
+
bookmarks,
|
|
56
|
+
onToggleBookmark,
|
|
118
57
|
stickyPrompt,
|
|
119
58
|
openSubagent,
|
|
120
59
|
reveal,
|
|
@@ -131,26 +70,18 @@ export function SessionWorkspace({
|
|
|
131
70
|
onRailChange,
|
|
132
71
|
className,
|
|
133
72
|
}: SessionWorkspaceProps) {
|
|
134
|
-
// The cwd is the tree's root, and it comes from the registry rather than from
|
|
135
|
-
// the panel: reading it off the panel's own session hook would mean attaching
|
|
136
|
-
// a second WebSocket client, and the tool bridge asks the *first* attached
|
|
137
|
-
// client — a second one changes who answers.
|
|
138
73
|
const { info } = useSessionInfo(client, sessionId)
|
|
139
74
|
const cwd = info?.cwd
|
|
140
75
|
|
|
141
76
|
const tree = useHostFileTree(client, cwd)
|
|
142
77
|
const search = useHostFileSearch(client, cwd)
|
|
143
78
|
const files = useOpenFiles(client)
|
|
144
|
-
// Writing is a separate server opt-in from reading and defaults off, so the
|
|
145
|
-
// editor asks before it offers to save anything.
|
|
146
79
|
const { canWrite } = useHostFileRoots(client)
|
|
147
80
|
|
|
148
|
-
|
|
149
|
-
// Nothing here can save on the user's behalf when the tab is going away, so
|
|
150
|
-
// the browser's own guard is the last line. Registered only while there is
|
|
151
|
-
// something to lose — an unconditional handler makes every navigation prompt.
|
|
152
81
|
useEffect(() => {
|
|
153
|
-
if (!files.hasUnsaved)
|
|
82
|
+
if (!files.hasUnsaved) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
154
85
|
const warn = (event: BeforeUnloadEvent) => event.preventDefault()
|
|
155
86
|
window.addEventListener('beforeunload', warn)
|
|
156
87
|
return () => window.removeEventListener('beforeunload', warn)
|
|
@@ -159,8 +90,6 @@ export function SessionWorkspace({
|
|
|
159
90
|
const wide = useIsWide()
|
|
160
91
|
const [railCollapsed, setRailCollapsed] = useState(defaultRailCollapsed ?? false)
|
|
161
92
|
const [railWidth, setRailWidth] = useState(defaultRailWidth)
|
|
162
|
-
// Reported rather than stored. Kept in a ref so the effect below fires on a
|
|
163
|
-
// real change instead of on every render an inline callback would cause.
|
|
164
93
|
const onRailChangeRef = useRef(onRailChange)
|
|
165
94
|
onRailChangeRef.current = onRailChange
|
|
166
95
|
useEffect(() => {
|
|
@@ -168,30 +97,24 @@ export function SessionWorkspace({
|
|
|
168
97
|
}, [railWidth, railCollapsed])
|
|
169
98
|
const [editorHeight, setEditorHeight] = useState(360)
|
|
170
99
|
|
|
171
|
-
// Closing every tab returns the agent to the full column; opening one again
|
|
172
|
-
// should restore the height the user had chosen, so this is not reset here.
|
|
173
100
|
const hasFiles = files.files.length > 0
|
|
174
101
|
|
|
175
|
-
// A narrow viewport cannot spare 260px of rail beside a transcript, so there
|
|
176
|
-
// the rail overlays the workspace instead of sitting next to it — and starts
|
|
177
|
-
// out of the way.
|
|
178
102
|
const overlayRail = !wide
|
|
179
103
|
const railOpen = tree.available && !railCollapsed
|
|
180
104
|
useEffect(() => {
|
|
181
|
-
if (overlayRail)
|
|
105
|
+
if (overlayRail) {
|
|
106
|
+
setRailCollapsed(true)
|
|
107
|
+
}
|
|
182
108
|
}, [overlayRail])
|
|
183
109
|
|
|
184
|
-
// Closing a dirty tab is the one destructive thing the strip can do, and the
|
|
185
|
-
// edits are not recoverable once the tab is gone. `confirm` rather than a
|
|
186
|
-
// styled dialog on purpose: it is modal, it cannot be missed, and a
|
|
187
|
-
// custom one here would be a second modal system inside a component an
|
|
188
|
-
// embedder already renders inside their own.
|
|
189
110
|
const closeTab = useCallback(
|
|
190
111
|
(path: string) => {
|
|
191
112
|
const file = files.files.find((f) => f.path === path)
|
|
192
113
|
if (file && isDirty(file)) {
|
|
193
114
|
const name = file.name
|
|
194
|
-
if (!window.confirm(`${name} has unsaved changes. Close it and lose them?`))
|
|
115
|
+
if (!window.confirm(`${name} has unsaved changes. Close it and lose them?`)) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
195
118
|
}
|
|
196
119
|
files.close(path)
|
|
197
120
|
},
|
|
@@ -200,16 +123,7 @@ export function SessionWorkspace({
|
|
|
200
123
|
|
|
201
124
|
const column = useRef<HTMLDivElement>(null)
|
|
202
125
|
const columnHeight = useElementHeight(column)
|
|
203
|
-
|
|
204
|
-
// the workspace is the only surface that *has* an editor, which is why this
|
|
205
|
-
// lives here and not in the panel. Gated on the tree being available: without
|
|
206
|
-
// a host filesystem there is nothing to open, and a link that cannot resolve
|
|
207
|
-
// is worse than plain text. Monaco is excluded because Cmd+click already
|
|
208
|
-
// means go-to-definition in there, and every identifier would match.
|
|
209
|
-
const openPath = useCallback(
|
|
210
|
-
({ path }: { path: string }) => files.open(resolveAgainstCwd(path, cwd)),
|
|
211
|
-
[files.open, cwd],
|
|
212
|
-
)
|
|
126
|
+
const openPath = useCallback(({ path }: { path: string }) => files.open(resolveAgainstCwd(path, cwd)), [files.open, cwd])
|
|
213
127
|
usePathLinks({
|
|
214
128
|
container: column,
|
|
215
129
|
onOpen: openPath,
|
|
@@ -218,11 +132,6 @@ export function SessionWorkspace({
|
|
|
218
132
|
})
|
|
219
133
|
const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN)
|
|
220
134
|
|
|
221
|
-
// The embedder's header is app chrome and belongs above everything — but only
|
|
222
|
-
// `SessionPanel` can *build* the `⋯` menu it is handed, and only if it is the
|
|
223
|
-
// one calling the render-prop. So the panel still calls it, in its own tree,
|
|
224
|
-
// and the result is portalled up here. That keeps `SessionPanel` untouched and
|
|
225
|
-
// keeps the menu's own context (it is a Base UI popup) intact.
|
|
226
135
|
const [topBar, setTopBar] = useState<HTMLDivElement | null>(null)
|
|
227
136
|
const hoisted: SessionPanelProps['header'] =
|
|
228
137
|
header === undefined || topBar === null
|
|
@@ -232,121 +141,98 @@ export function SessionWorkspace({
|
|
|
232
141
|
: createPortal(header, topBar)
|
|
233
142
|
|
|
234
143
|
return (
|
|
235
|
-
<div
|
|
236
|
-
|
|
237
|
-
className=
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
) : tree.available ? (
|
|
254
|
-
// Collapsed: a slim strip that keeps the rail one click away. In flow
|
|
255
|
-
// rather than floating over the panel, so it can never land on top of an
|
|
256
|
-
// embedder's own header controls.
|
|
257
|
-
<div className='flex w-8 shrink-0 flex-col items-center border-r border-border bg-surface pt-1.5'>
|
|
258
|
-
<Button
|
|
259
|
-
variant='ghost'
|
|
260
|
-
size='icon-sm'
|
|
261
|
-
aria-label='Show project files'
|
|
262
|
-
onClick={() => setRailCollapsed(false)}>
|
|
263
|
-
<PanelLeftOpen className='size-4 text-fg-3' />
|
|
264
|
-
</Button>
|
|
265
|
-
</div>
|
|
266
|
-
) : null}
|
|
267
|
-
{/* No splitter over an overlay rail — dragging a drawer's edge on a phone
|
|
268
|
-
fights the scroll it is sitting on top of. */}
|
|
269
|
-
{railOpen && !overlayRail ? (
|
|
270
|
-
<Splitter
|
|
271
|
-
orientation='vertical'
|
|
272
|
-
value={railWidth}
|
|
273
|
-
onValueChange={setRailWidth}
|
|
274
|
-
min={RAIL_MIN}
|
|
275
|
-
max={RAIL_MAX}
|
|
276
|
-
defaultValue={defaultRailWidth}
|
|
277
|
-
aria-label='Resize the file tree'
|
|
278
|
-
/>
|
|
279
|
-
) : null}
|
|
280
|
-
|
|
281
|
-
<div ref={column} className='flex min-h-0 min-w-0 flex-1 flex-col'>
|
|
282
|
-
{/* Slot 1 of 3. The `? :` leaves a null here when nothing is open, which
|
|
283
|
-
is what holds the agent's slot below and keeps it from remounting. */}
|
|
284
|
-
{hasFiles ? (
|
|
285
|
-
<div
|
|
286
|
-
className='flex min-h-0 shrink-0 flex-col overflow-hidden'
|
|
287
|
-
style={{ height: Math.min(editorHeight, editorMax || editorHeight) }}>
|
|
288
|
-
<EditorTabs
|
|
289
|
-
files={files.files}
|
|
290
|
-
activePath={files.activePath}
|
|
291
|
-
onActivate={files.activate}
|
|
292
|
-
onClose={closeTab}
|
|
293
|
-
/>
|
|
294
|
-
<FileViewer
|
|
295
|
-
file={files.active}
|
|
296
|
-
canWrite={canWrite}
|
|
297
|
-
onChange={files.edit}
|
|
298
|
-
onSave={(path) => void files.save(path)}
|
|
299
|
-
onRevert={files.revert}
|
|
300
|
-
onReload={files.reload}
|
|
301
|
-
onOverwrite={(path) => void files.overwrite(path)}
|
|
302
|
-
onDismissConflict={files.dismissConflict}
|
|
303
|
-
/>
|
|
144
|
+
<div data-slot="session-workspace" className={cn('flex h-full min-h-0 w-full flex-col overflow-hidden bg-bg', className)}>
|
|
145
|
+
{header !== undefined ? <div ref={setTopBar} className="shrink-0" /> : null}
|
|
146
|
+
<div className="relative flex min-h-0 flex-1">
|
|
147
|
+
{railOpen ? (
|
|
148
|
+
<FileTree
|
|
149
|
+
tree={tree}
|
|
150
|
+
search={search}
|
|
151
|
+
activePath={files.activePath}
|
|
152
|
+
onOpenFile={files.open}
|
|
153
|
+
onCollapse={() => setRailCollapsed(true)}
|
|
154
|
+
style={{ width: overlayRail ? Math.min(railWidth, 320) : railWidth }}
|
|
155
|
+
className={cn('shrink-0 border-r border-border', overlayRail && 'absolute inset-y-0 left-0 z-20 shadow-lg')}
|
|
156
|
+
/>
|
|
157
|
+
) : tree.available ? (
|
|
158
|
+
<div className="flex w-8 shrink-0 flex-col items-center border-r border-border bg-surface pt-1.5">
|
|
159
|
+
<Button variant="ghost" size="icon-sm" aria-label="Show project files" onClick={() => setRailCollapsed(false)}>
|
|
160
|
+
<PanelLeftOpen className="size-4 text-fg-3" />
|
|
161
|
+
</Button>
|
|
304
162
|
</div>
|
|
305
163
|
) : null}
|
|
306
|
-
{
|
|
307
|
-
{hasFiles ? (
|
|
164
|
+
{railOpen && !overlayRail ? (
|
|
308
165
|
<Splitter
|
|
309
|
-
orientation=
|
|
310
|
-
value={
|
|
311
|
-
onValueChange={
|
|
312
|
-
min={
|
|
313
|
-
max={
|
|
314
|
-
|
|
166
|
+
orientation="vertical"
|
|
167
|
+
value={railWidth}
|
|
168
|
+
onValueChange={setRailWidth}
|
|
169
|
+
min={RAIL_MIN}
|
|
170
|
+
max={RAIL_MAX}
|
|
171
|
+
defaultValue={defaultRailWidth}
|
|
172
|
+
aria-label="Resize the file tree"
|
|
315
173
|
/>
|
|
316
174
|
) : null}
|
|
317
|
-
{/* Slot 3 of 3 — always here, always at this index. */}
|
|
318
|
-
<SessionPanel
|
|
319
|
-
client={client}
|
|
320
|
-
sessionId={sessionId}
|
|
321
|
-
header={hoisted}
|
|
322
|
-
transcriptVariant={transcriptVariant}
|
|
323
|
-
transcriptDensity={transcriptDensity}
|
|
324
|
-
transcriptFont={transcriptFont}
|
|
325
|
-
scrubber={scrubber}
|
|
326
|
-
scrubberMarks={scrubberMarks}
|
|
327
|
-
stickyPrompt={stickyPrompt}
|
|
328
|
-
openSubagent={openSubagent}
|
|
329
|
-
reveal={reveal}
|
|
330
|
-
onSubagentChange={onSubagentChange}
|
|
331
|
-
controlsSurface={controlsSurface}
|
|
332
|
-
fontSize={fontSize}
|
|
333
|
-
onLinkClick={onLinkClick}
|
|
334
|
-
statusPlacement={statusPlacement}
|
|
335
|
-
unseen={unseen}
|
|
336
|
-
readOnly={readOnly}
|
|
337
|
-
onVitals={onVitals}
|
|
338
|
-
className='min-h-0 flex-1'
|
|
339
|
-
/>
|
|
340
|
-
</div>
|
|
341
175
|
|
|
342
|
-
{
|
|
343
|
-
|
|
176
|
+
<div ref={column} className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
177
|
+
{hasFiles ? (
|
|
178
|
+
<div
|
|
179
|
+
className="flex min-h-0 shrink-0 flex-col overflow-hidden"
|
|
180
|
+
style={{ height: Math.min(editorHeight, editorMax || editorHeight) }}
|
|
181
|
+
>
|
|
182
|
+
<EditorTabs files={files.files} activePath={files.activePath} onActivate={files.activate} onClose={closeTab} />
|
|
183
|
+
<FileViewer
|
|
184
|
+
file={files.active}
|
|
185
|
+
canWrite={canWrite}
|
|
186
|
+
onChange={files.edit}
|
|
187
|
+
onSave={(path) => void files.save(path)}
|
|
188
|
+
onRevert={files.revert}
|
|
189
|
+
onReload={files.reload}
|
|
190
|
+
onOverwrite={(path) => void files.overwrite(path)}
|
|
191
|
+
onDismissConflict={files.dismissConflict}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
) : null}
|
|
195
|
+
{hasFiles ? (
|
|
196
|
+
<Splitter
|
|
197
|
+
orientation="horizontal"
|
|
198
|
+
value={Math.min(editorHeight, editorMax || editorHeight)}
|
|
199
|
+
onValueChange={setEditorHeight}
|
|
200
|
+
min={EDITOR_MIN}
|
|
201
|
+
max={editorMax}
|
|
202
|
+
aria-label="Resize the open file"
|
|
203
|
+
/>
|
|
204
|
+
) : null}
|
|
205
|
+
<SessionPanel
|
|
206
|
+
client={client}
|
|
207
|
+
sessionId={sessionId}
|
|
208
|
+
header={hoisted}
|
|
209
|
+
transcriptVariant={transcriptVariant}
|
|
210
|
+
transcriptDensity={transcriptDensity}
|
|
211
|
+
transcriptFont={transcriptFont}
|
|
212
|
+
scrubber={scrubber}
|
|
213
|
+
bookmarks={bookmarks}
|
|
214
|
+
onToggleBookmark={onToggleBookmark}
|
|
215
|
+
stickyPrompt={stickyPrompt}
|
|
216
|
+
openSubagent={openSubagent}
|
|
217
|
+
reveal={reveal}
|
|
218
|
+
onSubagentChange={onSubagentChange}
|
|
219
|
+
controlsSurface={controlsSurface}
|
|
220
|
+
fontSize={fontSize}
|
|
221
|
+
onLinkClick={onLinkClick}
|
|
222
|
+
statusPlacement={statusPlacement}
|
|
223
|
+
unseen={unseen}
|
|
224
|
+
readOnly={readOnly}
|
|
225
|
+
onVitals={onVitals}
|
|
226
|
+
className="min-h-0 flex-1"
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
344
230
|
{overlayRail && railOpen ? (
|
|
345
231
|
<button
|
|
346
|
-
type=
|
|
347
|
-
aria-label=
|
|
232
|
+
type="button"
|
|
233
|
+
aria-label="Close the file tree"
|
|
348
234
|
onClick={() => setRailCollapsed(true)}
|
|
349
|
-
className=
|
|
235
|
+
className="absolute inset-0 z-10 bg-black/30"
|
|
350
236
|
/>
|
|
351
237
|
) : null}
|
|
352
238
|
</div>
|
|
@@ -354,16 +240,17 @@ export function SessionWorkspace({
|
|
|
354
240
|
)
|
|
355
241
|
}
|
|
356
242
|
|
|
357
|
-
/** Live height of an element, for a splitter that needs to know how much room it
|
|
358
|
-
* is dividing. Zero until the first observation, which callers treat as
|
|
359
|
-
* "unmeasured" rather than as a real bound. */
|
|
360
243
|
function useElementHeight(ref: RefObject<HTMLElement | null>): number {
|
|
361
244
|
const [height, setHeight] = useState(0)
|
|
362
245
|
useLayoutEffect(() => {
|
|
363
246
|
const element = ref.current
|
|
364
|
-
if (!element)
|
|
247
|
+
if (!element) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
365
250
|
const observer = new ResizeObserver(([entry]) => {
|
|
366
|
-
if (entry)
|
|
251
|
+
if (entry) {
|
|
252
|
+
setHeight(entry.contentRect.height)
|
|
253
|
+
}
|
|
367
254
|
})
|
|
368
255
|
observer.observe(element)
|
|
369
256
|
return () => observer.disconnect()
|
|
@@ -371,8 +258,6 @@ function useElementHeight(ref: RefObject<HTMLElement | null>): number {
|
|
|
371
258
|
return height
|
|
372
259
|
}
|
|
373
260
|
|
|
374
|
-
/** Whether there is room for a rail beside the content. Presentation only — the
|
|
375
|
-
* workspace's actual state lives in the hooks from `@workerdeck/react`. */
|
|
376
261
|
function useIsWide(): boolean {
|
|
377
262
|
const [wide, setWide] = useState(true)
|
|
378
263
|
useEffect(() => {
|
|
@@ -9,14 +9,10 @@ export interface SkillsDialogProps {
|
|
|
9
9
|
skills: SkillInfo[] | undefined
|
|
10
10
|
open: boolean
|
|
11
11
|
onOpenChange: (open: boolean) => void
|
|
12
|
-
/** Insert a skill's opening message into the composer, if the host offers
|
|
13
|
-
* that. Omit and the dialog is read-only. */
|
|
14
12
|
onUse?: (skill: SkillInfo) => void
|
|
15
13
|
className?: string
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
/** Where the skill came from. The engine's set is open, so an unrecognised
|
|
19
|
-
* scope renders as itself rather than being forced into a bucket. */
|
|
20
16
|
const SCOPE_LABEL: Record<string, string> = {
|
|
21
17
|
user: 'Personal',
|
|
22
18
|
repo: 'This project',
|
|
@@ -24,19 +20,6 @@ const SCOPE_LABEL: Record<string, string> = {
|
|
|
24
20
|
admin: 'Managed',
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
/**
|
|
28
|
-
* What this session's engine can do beyond its own tools: the skills it found,
|
|
29
|
-
* grouped by where they came from, with one drilled-down view each.
|
|
30
|
-
*
|
|
31
|
-
* The framing matters more here than in most panels. A skill is **not** a
|
|
32
|
-
* command — the model decides to use one by reading its description, and there
|
|
33
|
-
* is no wire syntax that invokes it. So this screen is a *discovery* surface,
|
|
34
|
-
* and the one action it offers ("Use this skill") is honest about being a
|
|
35
|
-
* drafting aid: it types a message for the operator to edit and send.
|
|
36
|
-
*
|
|
37
|
-
* Fed from the session's `skills` event rather than a REST route, because that
|
|
38
|
-
* is the channel the engine refreshes on its own when a skill changes on disk.
|
|
39
|
-
*/
|
|
40
23
|
export function SkillsDialog({ skills, open, onOpenChange, onUse, className }: SkillsDialogProps) {
|
|
41
24
|
const [selected, setSelected] = useState<string | undefined>()
|
|
42
25
|
const skill = skills?.find((s) => s.name === selected)
|
|
@@ -45,9 +28,12 @@ export function SkillsDialog({ skills, open, onOpenChange, onUse, className }: S
|
|
|
45
28
|
<Dialog
|
|
46
29
|
open={open}
|
|
47
30
|
onOpenChange={(next) => {
|
|
48
|
-
if (!next)
|
|
31
|
+
if (!next) {
|
|
32
|
+
setSelected(undefined)
|
|
33
|
+
}
|
|
49
34
|
onOpenChange(next)
|
|
50
|
-
}}
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
51
37
|
<DialogContent className={className}>
|
|
52
38
|
<DialogHeader
|
|
53
39
|
title={skill ? (skill.displayName ?? skill.name) : 'Skills'}
|
|
@@ -60,8 +46,8 @@ export function SkillsDialog({ skills, open, onOpenChange, onUse, className }: S
|
|
|
60
46
|
}
|
|
61
47
|
actions={
|
|
62
48
|
skill ? (
|
|
63
|
-
<Button variant=
|
|
64
|
-
<ChevronLeft className=
|
|
49
|
+
<Button variant="ghost" size="xs" onClick={() => setSelected(undefined)}>
|
|
50
|
+
<ChevronLeft className="size-3.5" />
|
|
65
51
|
Back
|
|
66
52
|
</Button>
|
|
67
53
|
) : undefined
|
|
@@ -79,59 +65,41 @@ export function SkillsDialog({ skills, open, onOpenChange, onUse, className }: S
|
|
|
79
65
|
)
|
|
80
66
|
}
|
|
81
67
|
|
|
82
|
-
function SkillList({
|
|
83
|
-
skills,
|
|
84
|
-
onSelect,
|
|
85
|
-
}: {
|
|
86
|
-
skills: SkillInfo[] | undefined
|
|
87
|
-
onSelect: (name: string) => void
|
|
88
|
-
}) {
|
|
68
|
+
function SkillList({ skills, onSelect }: { skills: SkillInfo[] | undefined; onSelect: (name: string) => void }) {
|
|
89
69
|
if (!skills) {
|
|
90
|
-
|
|
91
|
-
// not spawn until the session has something to do. Saying so beats an empty
|
|
92
|
-
// list that reads as "you have no skills".
|
|
93
|
-
return (
|
|
94
|
-
<p className='py-6 text-center text-body-sm text-fg-4'>
|
|
95
|
-
Skills are listed once the session connects — send a message first.
|
|
96
|
-
</p>
|
|
97
|
-
)
|
|
70
|
+
return <p className="py-6 text-center text-body-sm text-fg-4">Skills are listed once the session connects — send a message first.</p>
|
|
98
71
|
}
|
|
99
72
|
if (skills.length === 0) {
|
|
100
|
-
return <p className=
|
|
73
|
+
return <p className="py-6 text-center text-body-sm text-fg-4">This session found no skills.</p>
|
|
101
74
|
}
|
|
102
75
|
const scopes = [...new Set(skills.map((s) => s.scope ?? 'other'))]
|
|
103
76
|
return (
|
|
104
|
-
<div className=
|
|
77
|
+
<div className="flex flex-col gap-4">
|
|
105
78
|
{scopes.map((scope) => (
|
|
106
79
|
<div key={scope}>
|
|
107
|
-
<h3 className=
|
|
108
|
-
<ul className=
|
|
80
|
+
<h3 className="text-label font-medium text-fg-3">{SCOPE_LABEL[scope] ?? scope}</h3>
|
|
81
|
+
<ul className="mt-1 flex flex-col">
|
|
109
82
|
{skills
|
|
110
83
|
.filter((s) => (s.scope ?? 'other') === scope)
|
|
111
84
|
.map((s) => (
|
|
112
85
|
<li key={s.name}>
|
|
113
86
|
<button
|
|
114
|
-
type=
|
|
87
|
+
type="button"
|
|
115
88
|
onClick={() => onSelect(s.name)}
|
|
116
|
-
className=
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<span className='block truncate text-label text-fg-4'>
|
|
123
|
-
{s.shortDescription ?? s.description}
|
|
124
|
-
</span>
|
|
89
|
+
className="flex w-full items-start gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-surface-hover"
|
|
90
|
+
>
|
|
91
|
+
<span className="min-w-0 flex-1">
|
|
92
|
+
<span className="block truncate text-body-sm text-fg-1">{s.displayName ?? s.name}</span>
|
|
93
|
+
{(s.shortDescription ?? s.description) ? (
|
|
94
|
+
<span className="block truncate text-label text-fg-4">{s.shortDescription ?? s.description}</span>
|
|
125
95
|
) : null}
|
|
126
96
|
</span>
|
|
127
|
-
{/* Listed but switched off: a different answer from absent,
|
|
128
|
-
and the one an operator hunting for a missing skill needs. */}
|
|
129
97
|
{!s.enabled ? (
|
|
130
|
-
<Badge variant=
|
|
98
|
+
<Badge variant="neutral" className="mt-0.5 shrink-0">
|
|
131
99
|
off
|
|
132
100
|
</Badge>
|
|
133
101
|
) : null}
|
|
134
|
-
<ChevronRight className=
|
|
102
|
+
<ChevronRight className="mt-0.5 size-3.5 shrink-0 text-fg-4" />
|
|
135
103
|
</button>
|
|
136
104
|
</li>
|
|
137
105
|
))}
|
|
@@ -142,22 +110,12 @@ function SkillList({
|
|
|
142
110
|
)
|
|
143
111
|
}
|
|
144
112
|
|
|
145
|
-
function SkillView({
|
|
146
|
-
skill,
|
|
147
|
-
onUse,
|
|
148
|
-
onUsed,
|
|
149
|
-
}: {
|
|
150
|
-
skill: SkillInfo
|
|
151
|
-
onUse?: (skill: SkillInfo) => void
|
|
152
|
-
onUsed: () => void
|
|
153
|
-
}) {
|
|
113
|
+
function SkillView({ skill, onUse, onUsed }: { skill: SkillInfo; onUse?: (skill: SkillInfo) => void; onUsed: () => void }) {
|
|
154
114
|
return (
|
|
155
|
-
<div className=
|
|
115
|
+
<div className="flex flex-col gap-4">
|
|
156
116
|
<div>
|
|
157
|
-
{skill.scope ?
|
|
158
|
-
|
|
159
|
-
) : null}
|
|
160
|
-
<DialogRow label='Status'>
|
|
117
|
+
{skill.scope ? <DialogRow label="Scope">{SCOPE_LABEL[skill.scope] ?? skill.scope}</DialogRow> : null}
|
|
118
|
+
<DialogRow label="Status">
|
|
161
119
|
<Badge variant={skill.enabled ? 'success' : 'neutral'} dot>
|
|
162
120
|
{skill.enabled ? 'enabled' : 'disabled'}
|
|
163
121
|
</Badge>
|
|
@@ -165,29 +123,26 @@ function SkillView({
|
|
|
165
123
|
</div>
|
|
166
124
|
{skill.description ? (
|
|
167
125
|
<div>
|
|
168
|
-
<h3 className=
|
|
169
|
-
|
|
170
|
-
paraphrasing it here would describe a different skill than the one
|
|
171
|
-
the agent is reading about. */}
|
|
172
|
-
<p className='mt-1 text-body-sm whitespace-pre-wrap text-fg-2'>{skill.description}</p>
|
|
126
|
+
<h3 className="text-label font-medium text-fg-3">Description</h3>
|
|
127
|
+
<p className="mt-1 text-body-sm whitespace-pre-wrap text-fg-2">{skill.description}</p>
|
|
173
128
|
</div>
|
|
174
129
|
) : (
|
|
175
|
-
<p className=
|
|
130
|
+
<p className="text-body-sm text-fg-4">This skill carries no description.</p>
|
|
176
131
|
)}
|
|
177
132
|
{onUse && skill.enabled ? (
|
|
178
133
|
<div>
|
|
179
134
|
<Button
|
|
180
|
-
variant=
|
|
181
|
-
size=
|
|
135
|
+
variant="outline"
|
|
136
|
+
size="sm"
|
|
182
137
|
onClick={() => {
|
|
183
138
|
onUse(skill)
|
|
184
139
|
onUsed()
|
|
185
|
-
}}
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
186
142
|
Use this skill
|
|
187
143
|
</Button>
|
|
188
|
-
<p className=
|
|
189
|
-
Writes an opening message into the composer for you to edit — it isn’t sent, and there
|
|
190
|
-
is no command that runs a skill directly.
|
|
144
|
+
<p className="mt-1.5 text-label text-fg-4">
|
|
145
|
+
Writes an opening message into the composer for you to edit — it isn’t sent, and there is no command that runs a skill directly.
|
|
191
146
|
</p>
|
|
192
147
|
</div>
|
|
193
148
|
) : null}
|