@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
|
@@ -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,86 +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']
|
|
31
|
+
fontSize?: SessionPanelProps['fontSize']
|
|
32
|
+
onLinkClick?: SessionPanelProps['onLinkClick']
|
|
56
33
|
unseen?: SessionPanelProps['unseen']
|
|
57
|
-
/** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
|
|
58
|
-
* {@link SessionPanel}; the file tree and editor are unaffected, since reading
|
|
59
|
-
* a run's files is the point of a read-only view. */
|
|
60
34
|
readOnly?: SessionPanelProps['readOnly']
|
|
61
35
|
onVitals?: SessionPanelProps['onVitals']
|
|
62
|
-
/** Rail width in pixels on first render. */
|
|
63
36
|
defaultRailWidth?: number
|
|
64
|
-
/** Start with the file rail collapsed even on a wide viewport. */
|
|
65
37
|
defaultRailCollapsed?: boolean
|
|
66
|
-
/**
|
|
67
|
-
* The rail moved. Paired with the two defaults so an embedder can persist the
|
|
68
|
-
* layout — the workspace deliberately does not, because *where* to keep it (a
|
|
69
|
-
* Memento, localStorage, a workspace file) is the embedder's call, and a
|
|
70
|
-
* component that picked one would be wrong in the other hosts.
|
|
71
|
-
*/
|
|
72
38
|
onRailChange?: (rail: { width: number; collapsed: boolean }) => void
|
|
73
39
|
className?: string
|
|
74
40
|
}
|
|
75
41
|
|
|
76
42
|
const RAIL_MIN = 180
|
|
77
43
|
const RAIL_MAX = 520
|
|
78
|
-
/** How little of the agent column the editor may leave. Below this the composer
|
|
79
|
-
* and a line of transcript stop fitting together, which is the point at which the
|
|
80
|
-
* agent has stopped being usable rather than merely small. */
|
|
81
44
|
const AGENT_MIN = 220
|
|
82
45
|
const EDITOR_MIN = 120
|
|
83
46
|
|
|
84
|
-
/**
|
|
85
|
-
* A VS Code-shaped workspace around a live session: file tree on the left, open
|
|
86
|
-
* files above, the agent below.
|
|
87
|
-
*
|
|
88
|
-
* **Strictly additive.** {@link SessionPanel} is untouched and still the whole
|
|
89
|
-
* session surface on its own — an embedder picks one or the other, and one that
|
|
90
|
-
* has its own file tree keeps using the panel.
|
|
91
|
-
*
|
|
92
|
-
* Two things here are load-bearing and easy to break:
|
|
93
|
-
*
|
|
94
|
-
* 1. **The editor region is absent from the layout when nothing is open**, not
|
|
95
|
-
* collapsed to zero height. A zero-height pane leaves a draggable splitter and
|
|
96
|
-
* parks the composer at an odd offset; absence is what makes the agent's
|
|
97
|
-
* "claims the full column" state actually look like the panel alone.
|
|
98
|
-
* 2. **`SessionPanel` keeps its position in the tree across that transition.** It
|
|
99
|
-
* holds the WebSocket attach and the entire transcript, so moving it between
|
|
100
|
-
* parents — or wrapping it conditionally — would remount it and drop the
|
|
101
|
-
* session's rendered history on the floor the first time someone opens a file.
|
|
102
|
-
* The conditional children before it are `? :` expressions that leave a null in
|
|
103
|
-
* their slot, which is exactly what keeps its index stable.
|
|
104
|
-
*/
|
|
105
47
|
export function SessionWorkspace({
|
|
106
48
|
client,
|
|
107
49
|
sessionId,
|
|
@@ -110,13 +52,16 @@ export function SessionWorkspace({
|
|
|
110
52
|
transcriptDensity,
|
|
111
53
|
transcriptFont,
|
|
112
54
|
scrubber,
|
|
113
|
-
|
|
55
|
+
bookmarks,
|
|
56
|
+
onToggleBookmark,
|
|
114
57
|
stickyPrompt,
|
|
115
58
|
openSubagent,
|
|
116
59
|
reveal,
|
|
117
60
|
onSubagentChange,
|
|
118
61
|
statusPlacement,
|
|
119
62
|
controlsSurface,
|
|
63
|
+
fontSize,
|
|
64
|
+
onLinkClick,
|
|
120
65
|
unseen,
|
|
121
66
|
readOnly,
|
|
122
67
|
onVitals,
|
|
@@ -125,26 +70,18 @@ export function SessionWorkspace({
|
|
|
125
70
|
onRailChange,
|
|
126
71
|
className,
|
|
127
72
|
}: SessionWorkspaceProps) {
|
|
128
|
-
// The cwd is the tree's root, and it comes from the registry rather than from
|
|
129
|
-
// the panel: reading it off the panel's own session hook would mean attaching
|
|
130
|
-
// a second WebSocket client, and the tool bridge asks the *first* attached
|
|
131
|
-
// client — a second one changes who answers.
|
|
132
73
|
const { info } = useSessionInfo(client, sessionId)
|
|
133
74
|
const cwd = info?.cwd
|
|
134
75
|
|
|
135
76
|
const tree = useHostFileTree(client, cwd)
|
|
136
77
|
const search = useHostFileSearch(client, cwd)
|
|
137
78
|
const files = useOpenFiles(client)
|
|
138
|
-
// Writing is a separate server opt-in from reading and defaults off, so the
|
|
139
|
-
// editor asks before it offers to save anything.
|
|
140
79
|
const { canWrite } = useHostFileRoots(client)
|
|
141
80
|
|
|
142
|
-
|
|
143
|
-
// Nothing here can save on the user's behalf when the tab is going away, so
|
|
144
|
-
// the browser's own guard is the last line. Registered only while there is
|
|
145
|
-
// something to lose — an unconditional handler makes every navigation prompt.
|
|
146
81
|
useEffect(() => {
|
|
147
|
-
if (!files.hasUnsaved)
|
|
82
|
+
if (!files.hasUnsaved) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
148
85
|
const warn = (event: BeforeUnloadEvent) => event.preventDefault()
|
|
149
86
|
window.addEventListener('beforeunload', warn)
|
|
150
87
|
return () => window.removeEventListener('beforeunload', warn)
|
|
@@ -153,8 +90,6 @@ export function SessionWorkspace({
|
|
|
153
90
|
const wide = useIsWide()
|
|
154
91
|
const [railCollapsed, setRailCollapsed] = useState(defaultRailCollapsed ?? false)
|
|
155
92
|
const [railWidth, setRailWidth] = useState(defaultRailWidth)
|
|
156
|
-
// Reported rather than stored. Kept in a ref so the effect below fires on a
|
|
157
|
-
// real change instead of on every render an inline callback would cause.
|
|
158
93
|
const onRailChangeRef = useRef(onRailChange)
|
|
159
94
|
onRailChangeRef.current = onRailChange
|
|
160
95
|
useEffect(() => {
|
|
@@ -162,30 +97,24 @@ export function SessionWorkspace({
|
|
|
162
97
|
}, [railWidth, railCollapsed])
|
|
163
98
|
const [editorHeight, setEditorHeight] = useState(360)
|
|
164
99
|
|
|
165
|
-
// Closing every tab returns the agent to the full column; opening one again
|
|
166
|
-
// should restore the height the user had chosen, so this is not reset here.
|
|
167
100
|
const hasFiles = files.files.length > 0
|
|
168
101
|
|
|
169
|
-
// A narrow viewport cannot spare 260px of rail beside a transcript, so there
|
|
170
|
-
// the rail overlays the workspace instead of sitting next to it — and starts
|
|
171
|
-
// out of the way.
|
|
172
102
|
const overlayRail = !wide
|
|
173
103
|
const railOpen = tree.available && !railCollapsed
|
|
174
104
|
useEffect(() => {
|
|
175
|
-
if (overlayRail)
|
|
105
|
+
if (overlayRail) {
|
|
106
|
+
setRailCollapsed(true)
|
|
107
|
+
}
|
|
176
108
|
}, [overlayRail])
|
|
177
109
|
|
|
178
|
-
// Closing a dirty tab is the one destructive thing the strip can do, and the
|
|
179
|
-
// edits are not recoverable once the tab is gone. `confirm` rather than a
|
|
180
|
-
// styled dialog on purpose: it is modal, it cannot be missed, and a
|
|
181
|
-
// custom one here would be a second modal system inside a component an
|
|
182
|
-
// embedder already renders inside their own.
|
|
183
110
|
const closeTab = useCallback(
|
|
184
111
|
(path: string) => {
|
|
185
112
|
const file = files.files.find((f) => f.path === path)
|
|
186
113
|
if (file && isDirty(file)) {
|
|
187
114
|
const name = file.name
|
|
188
|
-
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
|
+
}
|
|
189
118
|
}
|
|
190
119
|
files.close(path)
|
|
191
120
|
},
|
|
@@ -194,16 +123,7 @@ export function SessionWorkspace({
|
|
|
194
123
|
|
|
195
124
|
const column = useRef<HTMLDivElement>(null)
|
|
196
125
|
const columnHeight = useElementHeight(column)
|
|
197
|
-
|
|
198
|
-
// the workspace is the only surface that *has* an editor, which is why this
|
|
199
|
-
// lives here and not in the panel. Gated on the tree being available: without
|
|
200
|
-
// a host filesystem there is nothing to open, and a link that cannot resolve
|
|
201
|
-
// is worse than plain text. Monaco is excluded because Cmd+click already
|
|
202
|
-
// means go-to-definition in there, and every identifier would match.
|
|
203
|
-
const openPath = useCallback(
|
|
204
|
-
({ path }: { path: string }) => files.open(resolveAgainstCwd(path, cwd)),
|
|
205
|
-
[files.open, cwd],
|
|
206
|
-
)
|
|
126
|
+
const openPath = useCallback(({ path }: { path: string }) => files.open(resolveAgainstCwd(path, cwd)), [files.open, cwd])
|
|
207
127
|
usePathLinks({
|
|
208
128
|
container: column,
|
|
209
129
|
onOpen: openPath,
|
|
@@ -212,11 +132,6 @@ export function SessionWorkspace({
|
|
|
212
132
|
})
|
|
213
133
|
const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN)
|
|
214
134
|
|
|
215
|
-
// The embedder's header is app chrome and belongs above everything — but only
|
|
216
|
-
// `SessionPanel` can *build* the `⋯` menu it is handed, and only if it is the
|
|
217
|
-
// one calling the render-prop. So the panel still calls it, in its own tree,
|
|
218
|
-
// and the result is portalled up here. That keeps `SessionPanel` untouched and
|
|
219
|
-
// keeps the menu's own context (it is a Base UI popup) intact.
|
|
220
135
|
const [topBar, setTopBar] = useState<HTMLDivElement | null>(null)
|
|
221
136
|
const hoisted: SessionPanelProps['header'] =
|
|
222
137
|
header === undefined || topBar === null
|
|
@@ -226,119 +141,98 @@ export function SessionWorkspace({
|
|
|
226
141
|
: createPortal(header, topBar)
|
|
227
142
|
|
|
228
143
|
return (
|
|
229
|
-
<div
|
|
230
|
-
|
|
231
|
-
className=
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
) : tree.available ? (
|
|
248
|
-
// Collapsed: a slim strip that keeps the rail one click away. In flow
|
|
249
|
-
// rather than floating over the panel, so it can never land on top of an
|
|
250
|
-
// embedder's own header controls.
|
|
251
|
-
<div className='flex w-8 shrink-0 flex-col items-center border-r border-border bg-surface pt-1.5'>
|
|
252
|
-
<Button
|
|
253
|
-
variant='ghost'
|
|
254
|
-
size='icon-sm'
|
|
255
|
-
aria-label='Show project files'
|
|
256
|
-
onClick={() => setRailCollapsed(false)}>
|
|
257
|
-
<PanelLeftOpen className='size-4 text-fg-3' />
|
|
258
|
-
</Button>
|
|
259
|
-
</div>
|
|
260
|
-
) : null}
|
|
261
|
-
{/* No splitter over an overlay rail — dragging a drawer's edge on a phone
|
|
262
|
-
fights the scroll it is sitting on top of. */}
|
|
263
|
-
{railOpen && !overlayRail ? (
|
|
264
|
-
<Splitter
|
|
265
|
-
orientation='vertical'
|
|
266
|
-
value={railWidth}
|
|
267
|
-
onValueChange={setRailWidth}
|
|
268
|
-
min={RAIL_MIN}
|
|
269
|
-
max={RAIL_MAX}
|
|
270
|
-
defaultValue={defaultRailWidth}
|
|
271
|
-
aria-label='Resize the file tree'
|
|
272
|
-
/>
|
|
273
|
-
) : null}
|
|
274
|
-
|
|
275
|
-
<div ref={column} className='flex min-h-0 min-w-0 flex-1 flex-col'>
|
|
276
|
-
{/* Slot 1 of 3. The `? :` leaves a null here when nothing is open, which
|
|
277
|
-
is what holds the agent's slot below and keeps it from remounting. */}
|
|
278
|
-
{hasFiles ? (
|
|
279
|
-
<div
|
|
280
|
-
className='flex min-h-0 shrink-0 flex-col overflow-hidden'
|
|
281
|
-
style={{ height: Math.min(editorHeight, editorMax || editorHeight) }}>
|
|
282
|
-
<EditorTabs
|
|
283
|
-
files={files.files}
|
|
284
|
-
activePath={files.activePath}
|
|
285
|
-
onActivate={files.activate}
|
|
286
|
-
onClose={closeTab}
|
|
287
|
-
/>
|
|
288
|
-
<FileViewer
|
|
289
|
-
file={files.active}
|
|
290
|
-
canWrite={canWrite}
|
|
291
|
-
onChange={files.edit}
|
|
292
|
-
onSave={(path) => void files.save(path)}
|
|
293
|
-
onRevert={files.revert}
|
|
294
|
-
onReload={files.reload}
|
|
295
|
-
onOverwrite={(path) => void files.overwrite(path)}
|
|
296
|
-
onDismissConflict={files.dismissConflict}
|
|
297
|
-
/>
|
|
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>
|
|
298
162
|
</div>
|
|
299
163
|
) : null}
|
|
300
|
-
{
|
|
301
|
-
{hasFiles ? (
|
|
164
|
+
{railOpen && !overlayRail ? (
|
|
302
165
|
<Splitter
|
|
303
|
-
orientation=
|
|
304
|
-
value={
|
|
305
|
-
onValueChange={
|
|
306
|
-
min={
|
|
307
|
-
max={
|
|
308
|
-
|
|
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"
|
|
309
173
|
/>
|
|
310
174
|
) : null}
|
|
311
|
-
{/* Slot 3 of 3 — always here, always at this index. */}
|
|
312
|
-
<SessionPanel
|
|
313
|
-
client={client}
|
|
314
|
-
sessionId={sessionId}
|
|
315
|
-
header={hoisted}
|
|
316
|
-
transcriptVariant={transcriptVariant}
|
|
317
|
-
transcriptDensity={transcriptDensity}
|
|
318
|
-
transcriptFont={transcriptFont}
|
|
319
|
-
scrubber={scrubber}
|
|
320
|
-
scrubberMarks={scrubberMarks}
|
|
321
|
-
stickyPrompt={stickyPrompt}
|
|
322
|
-
openSubagent={openSubagent}
|
|
323
|
-
reveal={reveal}
|
|
324
|
-
onSubagentChange={onSubagentChange}
|
|
325
|
-
controlsSurface={controlsSurface}
|
|
326
|
-
statusPlacement={statusPlacement}
|
|
327
|
-
unseen={unseen}
|
|
328
|
-
readOnly={readOnly}
|
|
329
|
-
onVitals={onVitals}
|
|
330
|
-
className='min-h-0 flex-1'
|
|
331
|
-
/>
|
|
332
|
-
</div>
|
|
333
175
|
|
|
334
|
-
{
|
|
335
|
-
|
|
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
|
+
|
|
336
230
|
{overlayRail && railOpen ? (
|
|
337
231
|
<button
|
|
338
|
-
type=
|
|
339
|
-
aria-label=
|
|
232
|
+
type="button"
|
|
233
|
+
aria-label="Close the file tree"
|
|
340
234
|
onClick={() => setRailCollapsed(true)}
|
|
341
|
-
className=
|
|
235
|
+
className="absolute inset-0 z-10 bg-black/30"
|
|
342
236
|
/>
|
|
343
237
|
) : null}
|
|
344
238
|
</div>
|
|
@@ -346,16 +240,17 @@ export function SessionWorkspace({
|
|
|
346
240
|
)
|
|
347
241
|
}
|
|
348
242
|
|
|
349
|
-
/** Live height of an element, for a splitter that needs to know how much room it
|
|
350
|
-
* is dividing. Zero until the first observation, which callers treat as
|
|
351
|
-
* "unmeasured" rather than as a real bound. */
|
|
352
243
|
function useElementHeight(ref: RefObject<HTMLElement | null>): number {
|
|
353
244
|
const [height, setHeight] = useState(0)
|
|
354
245
|
useLayoutEffect(() => {
|
|
355
246
|
const element = ref.current
|
|
356
|
-
if (!element)
|
|
247
|
+
if (!element) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
357
250
|
const observer = new ResizeObserver(([entry]) => {
|
|
358
|
-
if (entry)
|
|
251
|
+
if (entry) {
|
|
252
|
+
setHeight(entry.contentRect.height)
|
|
253
|
+
}
|
|
359
254
|
})
|
|
360
255
|
observer.observe(element)
|
|
361
256
|
return () => observer.disconnect()
|
|
@@ -363,8 +258,6 @@ function useElementHeight(ref: RefObject<HTMLElement | null>): number {
|
|
|
363
258
|
return height
|
|
364
259
|
}
|
|
365
260
|
|
|
366
|
-
/** Whether there is room for a rail beside the content. Presentation only — the
|
|
367
|
-
* workspace's actual state lives in the hooks from `@workerdeck/react`. */
|
|
368
261
|
function useIsWide(): boolean {
|
|
369
262
|
const [wide, setWide] = useState(true)
|
|
370
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}
|