@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,31 +1,22 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
3
3
|
import type { HostDirEntry, HostFileMatch } from '@workerdeck/protocol'
|
|
4
|
-
import { ChevronLeft, File,
|
|
4
|
+
import { ChevronLeft, File, Search } from 'lucide-react'
|
|
5
5
|
import { Button } from '../ui/Button.tsx'
|
|
6
6
|
import { CodeBlock } from '../ui/CodeBlock.tsx'
|
|
7
7
|
import { Dialog, DialogBody, DialogContent, DialogHeader } from '../ui/Dialog.tsx'
|
|
8
8
|
import { Input } from '../ui/Input.tsx'
|
|
9
9
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
10
10
|
import { formatBytes } from '../../lib/format.ts'
|
|
11
|
+
import { EntryIcon } from './EntryIcon.tsx'
|
|
11
12
|
|
|
12
13
|
export interface HostFilesDialogProps {
|
|
13
14
|
client: WorkerDeckClient
|
|
14
|
-
/** The session's working directory — the browser is rooted here. */
|
|
15
15
|
cwd: string | undefined
|
|
16
16
|
open: boolean
|
|
17
17
|
onOpenChange: (open: boolean) => void
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Browse the project the session is working in.
|
|
22
|
-
*
|
|
23
|
-
* Deliberately rooted at the session's cwd rather than at the server's
|
|
24
|
-
* `hostFiles.roots`: the roots are the *security* boundary (the server enforces
|
|
25
|
-
* them on every request), but what someone wants while watching an agent is this
|
|
26
|
-
* project's tree. Read-only — writing is a separate server opt-in and not
|
|
27
|
-
* something a session viewer should be doing behind the agent's back.
|
|
28
|
-
*/
|
|
29
20
|
export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDialogProps) {
|
|
30
21
|
const [path, setPath] = useState<string | undefined>(cwd)
|
|
31
22
|
const [entries, setEntries] = useState<HostDirEntry[]>([])
|
|
@@ -54,22 +45,23 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
54
45
|
[client],
|
|
55
46
|
)
|
|
56
47
|
|
|
57
|
-
// Opening resets the browser to the session's directory. Navigation from here
|
|
58
|
-
// is explicit (`list`) rather than an effect on `path`, so walking into a
|
|
59
|
-
// folder is one request and not two.
|
|
60
48
|
useEffect(() => {
|
|
61
|
-
if (!open)
|
|
49
|
+
if (!open) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
62
52
|
setFile(undefined)
|
|
63
53
|
setQuery('')
|
|
64
54
|
setMatches(undefined)
|
|
65
55
|
setError(undefined)
|
|
66
|
-
if (cwd)
|
|
56
|
+
if (cwd) {
|
|
57
|
+
void list(cwd)
|
|
58
|
+
}
|
|
67
59
|
}, [open, cwd, list])
|
|
68
60
|
|
|
69
|
-
// Debounced, and only while there is something to search for; an empty box is
|
|
70
|
-
// "show me the directory again", not "search for everything".
|
|
71
61
|
useEffect(() => {
|
|
72
|
-
if (!open || !cwd)
|
|
62
|
+
if (!open || !cwd) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
73
65
|
const q = query.trim()
|
|
74
66
|
if (!q) {
|
|
75
67
|
setMatches(undefined)
|
|
@@ -92,10 +84,7 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
92
84
|
setFile({
|
|
93
85
|
path: response.path,
|
|
94
86
|
bytes: response.bytes,
|
|
95
|
-
content:
|
|
96
|
-
response.encoding === 'utf8'
|
|
97
|
-
? response.content
|
|
98
|
-
: '(binary file — not shown)',
|
|
87
|
+
content: response.encoding === 'utf8' ? response.content : '(binary file — not shown)',
|
|
99
88
|
})
|
|
100
89
|
} catch (e) {
|
|
101
90
|
setError(e instanceof Error ? e.message : 'Could not read that file')
|
|
@@ -109,87 +98,79 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
109
98
|
|
|
110
99
|
return (
|
|
111
100
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
112
|
-
<DialogContent size=
|
|
101
|
+
<DialogContent size="lg">
|
|
113
102
|
<DialogHeader
|
|
114
103
|
title={file ? file.path.split('/').pop()! : 'Files'}
|
|
115
104
|
description={file ? file.path : path}
|
|
116
105
|
actions={
|
|
117
106
|
file ? (
|
|
118
|
-
<Button variant=
|
|
119
|
-
<ChevronLeft className=
|
|
107
|
+
<Button variant="ghost" size="xs" onClick={() => setFile(undefined)}>
|
|
108
|
+
<ChevronLeft className="size-3.5" />
|
|
120
109
|
Back
|
|
121
110
|
</Button>
|
|
122
111
|
) : parent ? (
|
|
123
|
-
<Button variant=
|
|
124
|
-
<ChevronLeft className=
|
|
112
|
+
<Button variant="ghost" size="xs" onClick={() => void list(parent)}>
|
|
113
|
+
<ChevronLeft className="size-3.5" />
|
|
125
114
|
Up
|
|
126
115
|
</Button>
|
|
127
116
|
) : null
|
|
128
117
|
}
|
|
129
118
|
/>
|
|
130
|
-
<DialogBody className=
|
|
131
|
-
{error ?
|
|
132
|
-
<div className='rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger'>{error}</div>
|
|
133
|
-
) : null}
|
|
119
|
+
<DialogBody className="flex flex-col gap-3">
|
|
120
|
+
{error ? <div className="rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger">{error}</div> : null}
|
|
134
121
|
|
|
135
122
|
{file ? (
|
|
136
123
|
<>
|
|
137
|
-
<p className=
|
|
124
|
+
<p className="text-label text-fg-4">{formatBytes(file.bytes)}</p>
|
|
138
125
|
<CodeBlock code={file.content} label={file.path.split('/').pop()} />
|
|
139
126
|
</>
|
|
140
127
|
) : (
|
|
141
128
|
<>
|
|
142
|
-
<div className=
|
|
143
|
-
<Search className=
|
|
129
|
+
<div className="relative">
|
|
130
|
+
<Search className="absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-fg-4" />
|
|
144
131
|
<Input
|
|
145
132
|
value={query}
|
|
146
133
|
onChange={(e) => setQuery(e.target.value)}
|
|
147
|
-
placeholder=
|
|
148
|
-
className=
|
|
134
|
+
placeholder="Search this project…"
|
|
135
|
+
className="pl-7"
|
|
149
136
|
spellCheck={false}
|
|
150
137
|
/>
|
|
151
138
|
</div>
|
|
152
139
|
{loading && shown.length === 0 ? (
|
|
153
|
-
<div className=
|
|
154
|
-
<Spinner className=
|
|
140
|
+
<div className="py-6 text-center">
|
|
141
|
+
<Spinner className="size-4 text-fg-4" />
|
|
155
142
|
</div>
|
|
156
143
|
) : shown.length === 0 ? (
|
|
157
|
-
<p className=
|
|
158
|
-
{matches ? 'No matching files.' : 'This directory is empty.'}
|
|
159
|
-
</p>
|
|
144
|
+
<p className="py-6 text-center text-body-sm text-fg-4">{matches ? 'No matching files.' : 'This directory is empty.'}</p>
|
|
160
145
|
) : (
|
|
161
|
-
<ul className=
|
|
146
|
+
<ul className="flex flex-col">
|
|
162
147
|
{matches
|
|
163
148
|
? matches.map((match) => (
|
|
164
149
|
<li key={match.path}>
|
|
165
150
|
<button
|
|
166
|
-
type=
|
|
151
|
+
type="button"
|
|
167
152
|
onClick={() => void openFile(match.path)}
|
|
168
|
-
className=
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
</span>
|
|
153
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover"
|
|
154
|
+
>
|
|
155
|
+
<File className="size-3.5 shrink-0 text-fg-4" />
|
|
156
|
+
<span className="min-w-0 flex-1 truncate font-mono text-label text-fg-1">{match.relative}</span>
|
|
173
157
|
</button>
|
|
174
158
|
</li>
|
|
175
159
|
))
|
|
176
160
|
: entries.map((entry) => (
|
|
177
161
|
<li key={entry.path}>
|
|
178
162
|
<button
|
|
179
|
-
type=
|
|
180
|
-
onClick={() =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
className='flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover'>
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={() => (entry.type === 'dir' ? void list(entry.path) : void openFile(entry.path))}
|
|
165
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover"
|
|
166
|
+
>
|
|
184
167
|
<EntryIcon type={entry.type} />
|
|
185
|
-
<span className=
|
|
168
|
+
<span className="min-w-0 flex-1 truncate font-mono text-label text-fg-1">
|
|
186
169
|
{entry.name}
|
|
187
170
|
{entry.type === 'dir' ? '/' : ''}
|
|
188
171
|
</span>
|
|
189
172
|
{entry.bytes !== undefined ? (
|
|
190
|
-
<span className=
|
|
191
|
-
{formatBytes(entry.bytes)}
|
|
192
|
-
</span>
|
|
173
|
+
<span className="shrink-0 text-label text-fg-4">{formatBytes(entry.bytes)}</span>
|
|
193
174
|
) : null}
|
|
194
175
|
</button>
|
|
195
176
|
</li>
|
|
@@ -197,9 +178,7 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
197
178
|
</ul>
|
|
198
179
|
)}
|
|
199
180
|
{truncated && !matches ? (
|
|
200
|
-
<p className=
|
|
201
|
-
More entries than the server will return — use the search box.
|
|
202
|
-
</p>
|
|
181
|
+
<p className="text-label text-fg-4">More entries than the server will return — use the search box.</p>
|
|
203
182
|
) : null}
|
|
204
183
|
</>
|
|
205
184
|
)}
|
|
@@ -208,11 +187,3 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
208
187
|
</Dialog>
|
|
209
188
|
)
|
|
210
189
|
}
|
|
211
|
-
|
|
212
|
-
/** A symlink is reported as itself and never silently resolved — following it is
|
|
213
|
-
* the next request's problem, and that request is refused if it escapes the roots. */
|
|
214
|
-
function EntryIcon({ type }: { type: HostDirEntry['type'] }) {
|
|
215
|
-
if (type === 'dir') return <Folder className='size-3.5 shrink-0 text-accent' />
|
|
216
|
-
if (type === 'symlink') return <Link2 className='size-3.5 shrink-0 text-fg-4' />
|
|
217
|
-
return <File className='size-3.5 shrink-0 text-fg-4' />
|
|
218
|
-
}
|
|
@@ -1,39 +1,18 @@
|
|
|
1
1
|
import { cn } from '../../lib/utils.ts'
|
|
2
2
|
|
|
3
3
|
export interface LoaderProps {
|
|
4
|
-
/** Overrides the cycling verb — for a state that has one true name
|
|
5
|
-
* ("Starting session…"). */
|
|
6
4
|
label?: string
|
|
7
|
-
/** When the current run began, for the elapsed clock. Absent = no clock.
|
|
8
|
-
* Unused by the card treatment; kept so the `cards` and `terminal` loaders
|
|
9
|
-
* take the same call site in `Transcript`. */
|
|
10
5
|
startedAt?: number
|
|
11
|
-
/** Context tokens in play. Same story as `startedAt`. */
|
|
12
6
|
tokens?: number
|
|
13
7
|
className?: string
|
|
14
8
|
}
|
|
15
9
|
|
|
16
|
-
/**
|
|
17
|
-
* "The agent is working and hasn't produced output yet."
|
|
18
|
-
*
|
|
19
|
-
* Three dots pulsing in CSS — the chat convention, for the `cards` variant. The
|
|
20
|
-
* terminal theme has its own working line (`terminal/items.tsx`'s `WorkingRow`,
|
|
21
|
-
* with the mark's pulse in the gutter and the run's readings beside the verb),
|
|
22
|
-
* because there it is a *row of the transcript* rather than a spinner floating
|
|
23
|
-
* over one.
|
|
24
|
-
*/
|
|
25
10
|
export function Loader({ label, className }: LoaderProps) {
|
|
26
11
|
return (
|
|
27
|
-
<div
|
|
28
|
-
|
|
29
|
-
className={cn('flex items-center gap-2 py-1 text-body-sm text-fg-4', className)}>
|
|
30
|
-
<span className='flex items-center gap-1'>
|
|
12
|
+
<div data-slot="loader" className={cn('flex items-center gap-2 py-1 text-body-sm text-fg-4', className)}>
|
|
13
|
+
<span className="flex items-center gap-1">
|
|
31
14
|
{[0, 1, 2].map((i) => (
|
|
32
|
-
<span
|
|
33
|
-
key={i}
|
|
34
|
-
className='size-1.5 animate-pulse rounded-full bg-fg-4'
|
|
35
|
-
style={{ animationDelay: `${i * 160}ms` }}
|
|
36
|
-
/>
|
|
15
|
+
<span key={i} className="size-1.5 animate-pulse rounded-full bg-fg-4" style={{ animationDelay: `${i * 160}ms` }} />
|
|
37
16
|
))}
|
|
38
17
|
</span>
|
|
39
18
|
{label ? <span>{label}</span> : null}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
2
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
3
|
-
import type {
|
|
4
|
-
McpServerActionRequest,
|
|
5
|
-
McpServerStatusInfo,
|
|
6
|
-
McpServerToolInfo,
|
|
7
|
-
} from '@workerdeck/protocol'
|
|
3
|
+
import type { McpServerActionRequest, McpServerStatusInfo, McpServerToolInfo } from '@workerdeck/protocol'
|
|
8
4
|
import { ChevronLeft, ChevronRight, Power, PowerOff, RotateCw } from 'lucide-react'
|
|
9
5
|
import { Badge, type BadgeProps } from '../ui/Badge.tsx'
|
|
10
6
|
import { Button } from '../ui/Button.tsx'
|
|
@@ -17,18 +13,10 @@ export interface McpDialogProps {
|
|
|
17
13
|
sessionId: string | undefined
|
|
18
14
|
open: boolean
|
|
19
15
|
onOpenChange: (open: boolean) => void
|
|
20
|
-
/**
|
|
21
|
-
* Whether this engine can reconnect/enable/disable a server
|
|
22
|
-
* (`EngineCapabilities.mcpServerActions`). False renders the panel read-only:
|
|
23
|
-
* codex reports rich status but exposes no per-server action, and buttons
|
|
24
|
-
* that 501 are worse than buttons that aren't there.
|
|
25
|
-
*/
|
|
26
16
|
canManageServers?: boolean
|
|
27
17
|
className?: string
|
|
28
18
|
}
|
|
29
19
|
|
|
30
|
-
/** The engine's status vocabulary is open — anything unrecognised renders
|
|
31
|
-
* neutrally rather than being forced into one of these. */
|
|
32
20
|
const STATUS_VARIANT: Record<string, NonNullable<BadgeProps['variant']>> = {
|
|
33
21
|
connected: 'success',
|
|
34
22
|
failed: 'danger',
|
|
@@ -37,37 +25,18 @@ const STATUS_VARIANT: Record<string, NonNullable<BadgeProps['variant']>> = {
|
|
|
37
25
|
disabled: 'neutral',
|
|
38
26
|
}
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
* The session's MCP servers, at the CLI's own `/mcp` depth: servers → one server
|
|
42
|
-
* → its tools → one tool, with Reconnect / Enable / Disable where they apply.
|
|
43
|
-
*
|
|
44
|
-
* Two things vary by engine rather than being fixed here. **The actions** exist
|
|
45
|
-
* only where the engine has them (`canManageServers`): codex reports rich status
|
|
46
|
-
* but has no per-server reconnect or toggle, so its panel is read-only. And
|
|
47
|
-
* **tool parameters** appear only where the engine reports a schema — codex
|
|
48
|
-
* returns each tool's full JSON Schema, the Agent SDK returns none at all, so
|
|
49
|
-
* the tool view either renders it or says why it can't, rather than leaving a
|
|
50
|
-
* silent gap or claiming the absence is universal.
|
|
51
|
-
*/
|
|
52
|
-
export function McpDialog({
|
|
53
|
-
client,
|
|
54
|
-
sessionId,
|
|
55
|
-
open,
|
|
56
|
-
onOpenChange,
|
|
57
|
-
canManageServers = true,
|
|
58
|
-
className,
|
|
59
|
-
}: McpDialogProps) {
|
|
28
|
+
export function McpDialog({ client, sessionId, open, onOpenChange, canManageServers = true, className }: McpDialogProps) {
|
|
60
29
|
const [servers, setServers] = useState<McpServerStatusInfo[] | undefined>()
|
|
61
30
|
const [error, setError] = useState<string | undefined>()
|
|
62
31
|
const [loading, setLoading] = useState(false)
|
|
63
32
|
const [busyServer, setBusyServer] = useState<string | undefined>()
|
|
64
|
-
// The drill-down, by name rather than by object — an action replaces the whole
|
|
65
|
-
// list, and a held reference would go stale on the first Reconnect.
|
|
66
33
|
const [selectedServer, setSelectedServer] = useState<string | undefined>()
|
|
67
34
|
const [selectedTool, setSelectedTool] = useState<string | undefined>()
|
|
68
35
|
|
|
69
36
|
const load = useCallback(async () => {
|
|
70
|
-
if (!sessionId)
|
|
37
|
+
if (!sessionId) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
71
40
|
setLoading(true)
|
|
72
41
|
setError(undefined)
|
|
73
42
|
try {
|
|
@@ -80,14 +49,18 @@ export function McpDialog({
|
|
|
80
49
|
}, [client, sessionId])
|
|
81
50
|
|
|
82
51
|
useEffect(() => {
|
|
83
|
-
if (!open)
|
|
52
|
+
if (!open) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
84
55
|
setSelectedServer(undefined)
|
|
85
56
|
setSelectedTool(undefined)
|
|
86
57
|
void load()
|
|
87
58
|
}, [open, load])
|
|
88
59
|
|
|
89
60
|
const act = async (name: string, action: McpServerActionRequest['action']) => {
|
|
90
|
-
if (!sessionId)
|
|
61
|
+
if (!sessionId) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
91
64
|
setBusyServer(name)
|
|
92
65
|
setError(undefined)
|
|
93
66
|
try {
|
|
@@ -108,32 +81,23 @@ export function McpDialog({
|
|
|
108
81
|
<DialogContent className={className}>
|
|
109
82
|
<DialogHeader
|
|
110
83
|
title={title}
|
|
111
|
-
description={
|
|
112
|
-
tool ? `${server?.name} tool` : server ? server.serverInfo?.name : undefined
|
|
113
|
-
}
|
|
84
|
+
description={tool ? `${server?.name} tool` : server ? server.serverInfo?.name : undefined}
|
|
114
85
|
actions={
|
|
115
86
|
selectedServer ? (
|
|
116
|
-
<Button
|
|
117
|
-
|
|
118
|
-
size='xs'
|
|
119
|
-
onClick={() => (selectedTool ? setSelectedTool(undefined) : setSelectedServer(undefined))}>
|
|
120
|
-
<ChevronLeft className='size-3.5' />
|
|
87
|
+
<Button variant="ghost" size="xs" onClick={() => (selectedTool ? setSelectedTool(undefined) : setSelectedServer(undefined))}>
|
|
88
|
+
<ChevronLeft className="size-3.5" />
|
|
121
89
|
Back
|
|
122
90
|
</Button>
|
|
123
91
|
) : (
|
|
124
|
-
<Button variant=
|
|
125
|
-
{loading ? <Spinner className=
|
|
92
|
+
<Button variant="ghost" size="xs" onClick={() => void load()} disabled={loading}>
|
|
93
|
+
{loading ? <Spinner className="size-3 text-current" /> : <RotateCw className="size-3" />}
|
|
126
94
|
Refresh
|
|
127
95
|
</Button>
|
|
128
96
|
)
|
|
129
97
|
}
|
|
130
98
|
/>
|
|
131
99
|
<DialogBody>
|
|
132
|
-
{error ?
|
|
133
|
-
<div className='mb-3 rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger'>
|
|
134
|
-
{error}
|
|
135
|
-
</div>
|
|
136
|
-
) : null}
|
|
100
|
+
{error ? <div className="mb-3 rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger">{error}</div> : null}
|
|
137
101
|
{tool ? (
|
|
138
102
|
<ToolView tool={tool} />
|
|
139
103
|
) : server ? (
|
|
@@ -144,18 +108,8 @@ export function McpDialog({
|
|
|
144
108
|
onAct={(action) => void act(server.name, action)}
|
|
145
109
|
onSelectTool={setSelectedTool}
|
|
146
110
|
/>
|
|
147
|
-
) : error && !servers ? (
|
|
148
|
-
|
|
149
|
-
// the list here would print "No MCP servers configured" underneath
|
|
150
|
-
// it — a claim about the operator's config that a failed request
|
|
151
|
-
// gives us no standing to make.
|
|
152
|
-
null
|
|
153
|
-
) : (
|
|
154
|
-
<ServerList
|
|
155
|
-
servers={servers}
|
|
156
|
-
loading={loading}
|
|
157
|
-
onSelect={setSelectedServer}
|
|
158
|
-
/>
|
|
111
|
+
) : error && !servers ? null : (
|
|
112
|
+
<ServerList servers={servers} loading={loading} onSelect={setSelectedServer} />
|
|
159
113
|
)}
|
|
160
114
|
</DialogBody>
|
|
161
115
|
</DialogContent>
|
|
@@ -174,42 +128,36 @@ function ServerList({
|
|
|
174
128
|
}) {
|
|
175
129
|
if (loading && !servers) {
|
|
176
130
|
return (
|
|
177
|
-
<div className=
|
|
178
|
-
<Spinner className=
|
|
131
|
+
<div className="py-6 text-center">
|
|
132
|
+
<Spinner className="size-4 text-fg-4" />
|
|
179
133
|
</div>
|
|
180
134
|
)
|
|
181
135
|
}
|
|
182
136
|
if (!servers?.length) {
|
|
183
|
-
return
|
|
184
|
-
<p className='py-6 text-center text-body-sm text-fg-4'>
|
|
185
|
-
No MCP servers configured for this session.
|
|
186
|
-
</p>
|
|
187
|
-
)
|
|
137
|
+
return <p className="py-6 text-center text-body-sm text-fg-4">No MCP servers configured for this session.</p>
|
|
188
138
|
}
|
|
189
|
-
// Grouped by where they were configured, like the CLI's own screen.
|
|
190
139
|
const scopes = [...new Set(servers.map((s) => s.scope ?? 'other'))]
|
|
191
140
|
return (
|
|
192
|
-
<div className=
|
|
141
|
+
<div className="flex flex-col gap-4">
|
|
193
142
|
{scopes.map((scope) => (
|
|
194
143
|
<div key={scope}>
|
|
195
|
-
<h3 className=
|
|
196
|
-
<ul className=
|
|
144
|
+
<h3 className="text-label font-medium text-fg-3 capitalize">{scope}</h3>
|
|
145
|
+
<ul className="mt-1 flex flex-col">
|
|
197
146
|
{servers
|
|
198
147
|
.filter((s) => (s.scope ?? 'other') === scope)
|
|
199
148
|
.map((s) => (
|
|
200
149
|
<li key={s.name}>
|
|
201
150
|
<button
|
|
202
|
-
type=
|
|
151
|
+
type="button"
|
|
203
152
|
onClick={() => onSelect(s.name)}
|
|
204
|
-
className=
|
|
205
|
-
|
|
206
|
-
{s.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<Badge variant={STATUS_VARIANT[s.status] ?? 'neutral'} dot className='shrink-0'>
|
|
153
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-surface-hover"
|
|
154
|
+
>
|
|
155
|
+
<span className="min-w-0 flex-1 truncate text-body-sm text-fg-1">{s.name}</span>
|
|
156
|
+
{s.tools?.length ? <span className="shrink-0 text-label text-fg-4">{s.tools.length} tools</span> : null}
|
|
157
|
+
<Badge variant={STATUS_VARIANT[s.status] ?? 'neutral'} dot className="shrink-0">
|
|
210
158
|
{s.status}
|
|
211
159
|
</Badge>
|
|
212
|
-
<ChevronRight className=
|
|
160
|
+
<ChevronRight className="size-3.5 shrink-0 text-fg-4" />
|
|
213
161
|
</button>
|
|
214
162
|
</li>
|
|
215
163
|
))}
|
|
@@ -235,78 +183,64 @@ function ServerView({
|
|
|
235
183
|
}) {
|
|
236
184
|
const disabled = server.status === 'disabled'
|
|
237
185
|
return (
|
|
238
|
-
<div className=
|
|
186
|
+
<div className="flex flex-col gap-4">
|
|
239
187
|
<div>
|
|
240
|
-
<DialogRow label=
|
|
188
|
+
<DialogRow label="Status">
|
|
241
189
|
<Badge variant={STATUS_VARIANT[server.status] ?? 'neutral'} dot>
|
|
242
190
|
{server.status}
|
|
243
191
|
</Badge>
|
|
244
192
|
</DialogRow>
|
|
245
|
-
{server.transport ? <DialogRow label=
|
|
246
|
-
{server.scope ? <DialogRow label=
|
|
193
|
+
{server.transport ? <DialogRow label="Transport">{server.transport}</DialogRow> : null}
|
|
194
|
+
{server.scope ? <DialogRow label="Scope">{server.scope}</DialogRow> : null}
|
|
247
195
|
{server.serverInfo ? (
|
|
248
|
-
<DialogRow label=
|
|
196
|
+
<DialogRow label="Server">
|
|
249
197
|
{server.serverInfo.name} {server.serverInfo.version}
|
|
250
198
|
</DialogRow>
|
|
251
199
|
) : null}
|
|
252
200
|
{server.command ? (
|
|
253
|
-
<DialogRow label=
|
|
201
|
+
<DialogRow label="Command" mono>
|
|
254
202
|
{[server.command, ...(server.args ?? [])].join(' ')}
|
|
255
203
|
</DialogRow>
|
|
256
204
|
) : null}
|
|
257
205
|
{server.url ? (
|
|
258
|
-
<DialogRow label=
|
|
206
|
+
<DialogRow label="URL" mono>
|
|
259
207
|
{server.url}
|
|
260
208
|
</DialogRow>
|
|
261
209
|
) : null}
|
|
262
210
|
</div>
|
|
263
211
|
|
|
264
|
-
{server.error ?
|
|
265
|
-
<div className='rounded-md bg-danger-bg px-3 py-2 text-body-sm break-words text-danger'>
|
|
266
|
-
{server.error}
|
|
267
|
-
</div>
|
|
268
|
-
) : null}
|
|
212
|
+
{server.error ? <div className="rounded-md bg-danger-bg px-3 py-2 text-body-sm break-words text-danger">{server.error}</div> : null}
|
|
269
213
|
|
|
270
|
-
{/* Absent, not disabled, when the engine has no per-server action: a
|
|
271
|
-
greyed-out Reconnect invites the question "why can't I?" on every
|
|
272
|
-
visit, where nothing at all reads as "this engine works differently". */}
|
|
273
214
|
{canManage ? (
|
|
274
|
-
<div className=
|
|
275
|
-
<Button variant=
|
|
276
|
-
{busy ? <Spinner className=
|
|
215
|
+
<div className="flex gap-2">
|
|
216
|
+
<Button variant="outline" size="sm" disabled={busy} onClick={() => onAct('reconnect')}>
|
|
217
|
+
{busy ? <Spinner className="size-3 text-current" /> : <RotateCw className="size-3" />}
|
|
277
218
|
Reconnect
|
|
278
219
|
</Button>
|
|
279
|
-
<Button
|
|
280
|
-
|
|
281
|
-
size='sm'
|
|
282
|
-
disabled={busy}
|
|
283
|
-
onClick={() => onAct(disabled ? 'enable' : 'disable')}>
|
|
284
|
-
{disabled ? <Power className='size-3' /> : <PowerOff className='size-3' />}
|
|
220
|
+
<Button variant="outline" size="sm" disabled={busy} onClick={() => onAct(disabled ? 'enable' : 'disable')}>
|
|
221
|
+
{disabled ? <Power className="size-3" /> : <PowerOff className="size-3" />}
|
|
285
222
|
{disabled ? 'Enable' : 'Disable'}
|
|
286
223
|
</Button>
|
|
287
224
|
</div>
|
|
288
225
|
) : null}
|
|
289
226
|
|
|
290
227
|
<div>
|
|
291
|
-
<h3 className=
|
|
228
|
+
<h3 className="text-label font-medium text-fg-3">Tools</h3>
|
|
292
229
|
{!server.tools?.length ? (
|
|
293
|
-
<p className=
|
|
294
|
-
{server.status === 'connected'
|
|
295
|
-
? 'This server exposes no tools.'
|
|
296
|
-
: 'Tools are listed once the server connects.'}
|
|
230
|
+
<p className="py-2 text-body-sm text-fg-4">
|
|
231
|
+
{server.status === 'connected' ? 'This server exposes no tools.' : 'Tools are listed once the server connects.'}
|
|
297
232
|
</p>
|
|
298
233
|
) : (
|
|
299
|
-
<ul className=
|
|
234
|
+
<ul className="mt-1 flex flex-col">
|
|
300
235
|
{server.tools.map((t) => (
|
|
301
236
|
<li key={t.name}>
|
|
302
237
|
<button
|
|
303
|
-
type=
|
|
238
|
+
type="button"
|
|
304
239
|
onClick={() => onSelectTool(t.name)}
|
|
305
|
-
className=
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<ChevronRight className='size-3.5 shrink-0 text-fg-4' />
|
|
240
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-surface-hover"
|
|
241
|
+
>
|
|
242
|
+
<span className="min-w-0 flex-1 truncate font-mono text-label text-fg-1">{t.name}</span>
|
|
243
|
+
<ChevronRight className="size-3.5 shrink-0 text-fg-4" />
|
|
310
244
|
</button>
|
|
311
245
|
</li>
|
|
312
246
|
))}
|
|
@@ -320,42 +254,35 @@ function ServerView({
|
|
|
320
254
|
function ToolView({ tool }: { tool: McpServerToolInfo }) {
|
|
321
255
|
const annotations = tool.annotations
|
|
322
256
|
return (
|
|
323
|
-
<div className=
|
|
257
|
+
<div className="flex flex-col gap-3">
|
|
324
258
|
{tool.description ? (
|
|
325
|
-
<p className=
|
|
259
|
+
<p className="text-body-sm whitespace-pre-wrap text-fg-2">{tool.description}</p>
|
|
326
260
|
) : (
|
|
327
|
-
<p className=
|
|
261
|
+
<p className="text-body-sm text-fg-4">This tool carries no description.</p>
|
|
328
262
|
)}
|
|
329
263
|
{annotations ? (
|
|
330
|
-
<div className=
|
|
331
|
-
{annotations.readOnly ? <Badge variant=
|
|
332
|
-
{annotations.destructive ? <Badge variant=
|
|
333
|
-
{annotations.openWorld ? <Badge variant=
|
|
264
|
+
<div className="flex flex-wrap gap-1.5">
|
|
265
|
+
{annotations.readOnly ? <Badge variant="success">read-only</Badge> : null}
|
|
266
|
+
{annotations.destructive ? <Badge variant="danger">destructive</Badge> : null}
|
|
267
|
+
{annotations.openWorld ? <Badge variant="warning">open world</Badge> : null}
|
|
334
268
|
</div>
|
|
335
269
|
) : null}
|
|
336
|
-
{/* Engine-dependent, and said as such: codex returns each tool's full
|
|
337
|
-
JSON Schema, the Agent SDK returns none at all. So this is a real
|
|
338
|
-
section where one exists and an explanation where it doesn't — never a
|
|
339
|
-
silent gap, and never a claim that no engine has them. */}
|
|
340
270
|
{tool.inputSchema !== undefined ? (
|
|
341
271
|
<div>
|
|
342
|
-
<h3 className=
|
|
343
|
-
<pre className=
|
|
272
|
+
<h3 className="text-label font-medium text-fg-3">Parameters</h3>
|
|
273
|
+
<pre className="mt-1 max-h-64 overflow-auto rounded-md bg-surface px-3 py-2 text-label text-fg-2">
|
|
344
274
|
{safeSchema(tool.inputSchema)}
|
|
345
275
|
</pre>
|
|
346
276
|
</div>
|
|
347
277
|
) : (
|
|
348
278
|
<p className={cn('text-label text-fg-4')}>
|
|
349
|
-
Parameters aren’t available: this engine names and describes each tool but reports no
|
|
350
|
-
input schema.
|
|
279
|
+
Parameters aren’t available: this engine names and describes each tool but reports no input schema.
|
|
351
280
|
</p>
|
|
352
281
|
)}
|
|
353
282
|
</div>
|
|
354
283
|
)
|
|
355
284
|
}
|
|
356
285
|
|
|
357
|
-
/** The schema is an opaque JSON document from another process — pretty-print it,
|
|
358
|
-
* and never let an unserializable value take the dialog down with it. */
|
|
359
286
|
function safeSchema(schema: unknown): string {
|
|
360
287
|
try {
|
|
361
288
|
return JSON.stringify(schema, null, 2)
|