@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,87 +1,231 @@
|
|
|
1
1
|
import type { ProjectIcon as ProjectIconSpec } from '@workerdeck/protocol'
|
|
2
2
|
import {
|
|
3
|
-
Anchor,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
Anchor,
|
|
4
|
+
Atom,
|
|
5
|
+
Beaker,
|
|
6
|
+
Bike,
|
|
7
|
+
Binary,
|
|
8
|
+
Blocks,
|
|
9
|
+
Bot,
|
|
10
|
+
Box,
|
|
11
|
+
Brain,
|
|
12
|
+
Briefcase,
|
|
13
|
+
Brush,
|
|
14
|
+
Bug,
|
|
15
|
+
Building2,
|
|
16
|
+
Cable,
|
|
17
|
+
Camera,
|
|
18
|
+
Car,
|
|
19
|
+
Cat,
|
|
20
|
+
CircuitBoard,
|
|
21
|
+
Cloud,
|
|
22
|
+
Code,
|
|
23
|
+
Coffee,
|
|
24
|
+
Cog,
|
|
25
|
+
Compass,
|
|
26
|
+
Container,
|
|
27
|
+
Cpu,
|
|
28
|
+
CreditCard,
|
|
29
|
+
Database,
|
|
30
|
+
Diamond,
|
|
31
|
+
Dog,
|
|
32
|
+
Feather,
|
|
33
|
+
Film,
|
|
34
|
+
Flame,
|
|
35
|
+
FlaskConical,
|
|
36
|
+
Folder,
|
|
37
|
+
Gamepad2,
|
|
38
|
+
Gauge,
|
|
39
|
+
Gem,
|
|
40
|
+
Ghost,
|
|
41
|
+
Gift,
|
|
42
|
+
GitBranch,
|
|
43
|
+
Globe,
|
|
44
|
+
GraduationCap,
|
|
45
|
+
Hammer,
|
|
46
|
+
Hexagon,
|
|
47
|
+
House,
|
|
48
|
+
Image,
|
|
49
|
+
Key,
|
|
50
|
+
Landmark,
|
|
51
|
+
Laptop,
|
|
52
|
+
Layers,
|
|
53
|
+
Leaf,
|
|
54
|
+
Library,
|
|
55
|
+
Lightbulb,
|
|
56
|
+
Lock,
|
|
57
|
+
Mail,
|
|
58
|
+
Map,
|
|
59
|
+
MessageCircle,
|
|
60
|
+
Mic,
|
|
61
|
+
Monitor,
|
|
62
|
+
Moon,
|
|
63
|
+
Mountain,
|
|
64
|
+
Music,
|
|
65
|
+
Network,
|
|
66
|
+
Newspaper,
|
|
67
|
+
Notebook,
|
|
68
|
+
Orbit,
|
|
69
|
+
Package,
|
|
70
|
+
Palette,
|
|
71
|
+
PenTool,
|
|
72
|
+
Phone,
|
|
73
|
+
PiggyBank,
|
|
74
|
+
Plane,
|
|
75
|
+
Plug,
|
|
76
|
+
Puzzle,
|
|
77
|
+
Radar,
|
|
78
|
+
Radio,
|
|
79
|
+
Receipt,
|
|
80
|
+
Rocket,
|
|
81
|
+
Ruler,
|
|
82
|
+
Satellite,
|
|
83
|
+
Scale,
|
|
84
|
+
Scissors,
|
|
85
|
+
Server,
|
|
86
|
+
Shield,
|
|
87
|
+
Ship,
|
|
88
|
+
ShoppingCart,
|
|
89
|
+
Signal,
|
|
90
|
+
Smartphone,
|
|
91
|
+
Sparkles,
|
|
92
|
+
Sprout,
|
|
93
|
+
Square,
|
|
94
|
+
Star,
|
|
95
|
+
Sun,
|
|
96
|
+
Table,
|
|
97
|
+
Tag,
|
|
98
|
+
Target,
|
|
99
|
+
Telescope,
|
|
100
|
+
Terminal,
|
|
101
|
+
TestTube,
|
|
102
|
+
TreePine,
|
|
103
|
+
Trophy,
|
|
104
|
+
Truck,
|
|
105
|
+
Umbrella,
|
|
106
|
+
Users,
|
|
107
|
+
Wallet,
|
|
108
|
+
Wand,
|
|
109
|
+
Waves,
|
|
110
|
+
Webhook,
|
|
111
|
+
Wifi,
|
|
112
|
+
Wrench,
|
|
113
|
+
Zap,
|
|
14
114
|
} from 'lucide-react'
|
|
15
115
|
import type { ComponentType } from 'react'
|
|
16
116
|
import { cn } from '../../lib/utils.ts'
|
|
17
117
|
|
|
18
|
-
/**
|
|
19
|
-
* A project's icon — the render side of protocol's `ProjectIcon`, for a list
|
|
20
|
-
* row or a group header.
|
|
21
|
-
*
|
|
22
|
-
* **The glyph arm is a curated set, and that is the design rather than a
|
|
23
|
-
* shortcut.** The gateway validates a glyph name by *shape* only (lucide's
|
|
24
|
-
* lowercase-kebab convention) and explicitly declines to grow an icon catalog,
|
|
25
|
-
* so the contract already says a client must fall back on a name it does not
|
|
26
|
-
* know.
|
|
27
|
-
*
|
|
28
|
-
* The alternative was measured rather than argued, against the VS Code
|
|
29
|
-
* sidebar's own bundle: `sidebar.js` is **31 KB** with one glyph, **77 KB**
|
|
30
|
-
* with the 110 below (~418 bytes each), and **927 KB** with a
|
|
31
|
-
* `import * as` over lucide's ~1,600 — which is what it costs, because a
|
|
32
|
-
* namespace import defeats tree-shaking by construction. (`DynamicIcon` is the
|
|
33
|
-
* third option and worse in a different currency: ~1,600 chunk files shipped
|
|
34
|
-
* inside the `.vsix`.) Twelve times the bundle to render a name nobody has
|
|
35
|
-
* declared yet is not a trade worth making; 46 KB to cover the names people
|
|
36
|
-
* actually use is.
|
|
37
|
-
*
|
|
38
|
-
* The set is chosen to cover what people call a project; an unlisted-but-valid
|
|
39
|
-
* name draws {@link Folder}, which is what the row drew before this feature
|
|
40
|
-
* existed. **Grow it freely** — each addition is ~400 bytes and the fallback
|
|
41
|
-
* means a missing one is a shrug, not a hole.
|
|
42
|
-
*
|
|
43
|
-
* **The image arm draws nothing until its bytes arrive**, and takes them as a
|
|
44
|
-
* resolved `src` rather than fetching: the wire carries only an address
|
|
45
|
-
* (media type, size, content hash), and *who* fetches it differs per client —
|
|
46
|
-
* a VS Code webview cannot reach a gateway at all and must be handed a data
|
|
47
|
-
* URL by its extension host, while a browser can fetch and `createObjectURL`.
|
|
48
|
-
* A component that fetched would be wrong on one of them. Absent `src` renders
|
|
49
|
-
* nothing rather than a placeholder box: an icon is decoration beside a name
|
|
50
|
-
* that is already there, and a box that becomes a picture a beat later is more
|
|
51
|
-
* movement than the picture is worth.
|
|
52
|
-
*
|
|
53
|
-
* One thing an image icon cannot do, and a glyph can: **take the row's
|
|
54
|
-
* colour.** An `<img>`-embedded SVG is its own document, so `currentColor` does
|
|
55
|
-
* not reach it and its own `prefers-color-scheme` resolves against the *OS*
|
|
56
|
-
* rather than the host's theme (measured: it does resolve, which is the
|
|
57
|
-
* surprise — so a monochrome mark tuned for both schemes is right on a dark OS
|
|
58
|
-
* in a dark editor and wrong on a light OS in a dark one). A repo that wants a
|
|
59
|
-
* mark that always matches its row should declare a glyph; one that wants its
|
|
60
|
-
* brand should declare the image and accept that it is a picture.
|
|
61
|
-
*/
|
|
62
118
|
const GLYPHS: Record<string, ComponentType<{ className?: string }>> = {
|
|
63
|
-
anchor: Anchor,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
119
|
+
anchor: Anchor,
|
|
120
|
+
atom: Atom,
|
|
121
|
+
beaker: Beaker,
|
|
122
|
+
bike: Bike,
|
|
123
|
+
binary: Binary,
|
|
124
|
+
blocks: Blocks,
|
|
125
|
+
bot: Bot,
|
|
126
|
+
box: Box,
|
|
127
|
+
brain: Brain,
|
|
128
|
+
briefcase: Briefcase,
|
|
129
|
+
brush: Brush,
|
|
130
|
+
bug: Bug,
|
|
131
|
+
'building-2': Building2,
|
|
132
|
+
cable: Cable,
|
|
133
|
+
camera: Camera,
|
|
134
|
+
car: Car,
|
|
135
|
+
cat: Cat,
|
|
136
|
+
'circuit-board': CircuitBoard,
|
|
137
|
+
cloud: Cloud,
|
|
138
|
+
code: Code,
|
|
139
|
+
coffee: Coffee,
|
|
140
|
+
cog: Cog,
|
|
141
|
+
compass: Compass,
|
|
142
|
+
container: Container,
|
|
143
|
+
cpu: Cpu,
|
|
144
|
+
'credit-card': CreditCard,
|
|
145
|
+
database: Database,
|
|
146
|
+
diamond: Diamond,
|
|
147
|
+
dog: Dog,
|
|
148
|
+
feather: Feather,
|
|
149
|
+
film: Film,
|
|
150
|
+
flame: Flame,
|
|
151
|
+
'flask-conical': FlaskConical,
|
|
152
|
+
folder: Folder,
|
|
153
|
+
'gamepad-2': Gamepad2,
|
|
154
|
+
gauge: Gauge,
|
|
155
|
+
gem: Gem,
|
|
156
|
+
ghost: Ghost,
|
|
157
|
+
gift: Gift,
|
|
158
|
+
'git-branch': GitBranch,
|
|
159
|
+
globe: Globe,
|
|
160
|
+
'graduation-cap': GraduationCap,
|
|
161
|
+
hammer: Hammer,
|
|
162
|
+
hexagon: Hexagon,
|
|
163
|
+
house: House,
|
|
164
|
+
image: Image,
|
|
165
|
+
key: Key,
|
|
166
|
+
landmark: Landmark,
|
|
167
|
+
laptop: Laptop,
|
|
168
|
+
layers: Layers,
|
|
169
|
+
leaf: Leaf,
|
|
170
|
+
library: Library,
|
|
171
|
+
lightbulb: Lightbulb,
|
|
172
|
+
lock: Lock,
|
|
173
|
+
mail: Mail,
|
|
174
|
+
map: Map,
|
|
175
|
+
'message-circle': MessageCircle,
|
|
176
|
+
mic: Mic,
|
|
177
|
+
monitor: Monitor,
|
|
178
|
+
moon: Moon,
|
|
179
|
+
mountain: Mountain,
|
|
180
|
+
music: Music,
|
|
181
|
+
network: Network,
|
|
182
|
+
newspaper: Newspaper,
|
|
183
|
+
notebook: Notebook,
|
|
184
|
+
orbit: Orbit,
|
|
185
|
+
package: Package,
|
|
186
|
+
palette: Palette,
|
|
187
|
+
'pen-tool': PenTool,
|
|
188
|
+
phone: Phone,
|
|
189
|
+
'piggy-bank': PiggyBank,
|
|
190
|
+
plane: Plane,
|
|
191
|
+
plug: Plug,
|
|
192
|
+
puzzle: Puzzle,
|
|
193
|
+
radar: Radar,
|
|
194
|
+
radio: Radio,
|
|
195
|
+
receipt: Receipt,
|
|
196
|
+
rocket: Rocket,
|
|
197
|
+
ruler: Ruler,
|
|
198
|
+
satellite: Satellite,
|
|
199
|
+
scale: Scale,
|
|
200
|
+
scissors: Scissors,
|
|
201
|
+
server: Server,
|
|
202
|
+
shield: Shield,
|
|
203
|
+
ship: Ship,
|
|
204
|
+
'shopping-cart': ShoppingCart,
|
|
205
|
+
signal: Signal,
|
|
206
|
+
smartphone: Smartphone,
|
|
207
|
+
sparkles: Sparkles,
|
|
208
|
+
sprout: Sprout,
|
|
209
|
+
square: Square,
|
|
210
|
+
star: Star,
|
|
211
|
+
sun: Sun,
|
|
212
|
+
table: Table,
|
|
213
|
+
tag: Tag,
|
|
214
|
+
target: Target,
|
|
215
|
+
telescope: Telescope,
|
|
216
|
+
terminal: Terminal,
|
|
217
|
+
'test-tube': TestTube,
|
|
218
|
+
'tree-pine': TreePine,
|
|
219
|
+
trophy: Trophy,
|
|
220
|
+
truck: Truck,
|
|
221
|
+
umbrella: Umbrella,
|
|
222
|
+
users: Users,
|
|
223
|
+
wallet: Wallet,
|
|
224
|
+
wand: Wand,
|
|
225
|
+
waves: Waves,
|
|
226
|
+
webhook: Webhook,
|
|
227
|
+
wifi: Wifi,
|
|
228
|
+
wrench: Wrench,
|
|
85
229
|
zap: Zap,
|
|
86
230
|
}
|
|
87
231
|
|
|
@@ -92,27 +236,18 @@ export function ProjectIcon({
|
|
|
92
236
|
className,
|
|
93
237
|
}: {
|
|
94
238
|
icon: ProjectIconSpec | undefined
|
|
95
|
-
/** Resolved bytes for the `image` arm — an object URL or a data URL. The
|
|
96
|
-
* caller fetches (see the note above); absent draws nothing. */
|
|
97
239
|
src?: string
|
|
98
|
-
/** The project's name, for the alt text. */
|
|
99
240
|
name?: string
|
|
100
241
|
className?: string
|
|
101
242
|
}) {
|
|
102
|
-
if (!icon)
|
|
243
|
+
if (!icon) {
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
103
246
|
if (icon.type === 'image') {
|
|
104
|
-
if (!src)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
alt=''
|
|
109
|
-
aria-hidden
|
|
110
|
-
title={name}
|
|
111
|
-
// `object-contain` because a declared icon is whatever aspect the repo
|
|
112
|
-
// checked in, and a squashed logo reads worse than a letterboxed one.
|
|
113
|
-
className={cn('inline-block size-3 shrink-0 object-contain', className)}
|
|
114
|
-
/>
|
|
115
|
-
)
|
|
247
|
+
if (!src) {
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
return <img src={src} alt="" aria-hidden title={name} className={cn('inline-block size-3 shrink-0 object-contain', className)} />
|
|
116
251
|
}
|
|
117
252
|
const Glyph = GLYPHS[icon.name] ?? Folder
|
|
118
253
|
return <Glyph className={cn('inline-block size-3 shrink-0', className)} />
|
|
@@ -2,33 +2,27 @@ import { Fragment } from 'react'
|
|
|
2
2
|
import { scanPromptTokens } from '@workerdeck/react'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* A sent message, with its `@file` and `/command` tokens styled the way the CLI
|
|
7
|
-
* writes them: monospace and tinted, no background — the bubble already has one,
|
|
8
|
-
* and a second fill inside it reads as a button.
|
|
9
|
-
*
|
|
10
|
-
* Literal text, never markdown: what was typed is what was sent. The one pass
|
|
11
|
-
* over it is this, so a message reads the same after sending as it did in the
|
|
12
|
-
* composer. Two tokens, two meanings — a file is a reference, a command is an
|
|
13
|
-
* action — so they are told apart by hue rather than by shape alone.
|
|
14
|
-
*/
|
|
15
5
|
export function PromptTokenText({ text, className }: { text: string; className?: string }) {
|
|
16
6
|
const tokens = scanPromptTokens(text)
|
|
17
|
-
if (tokens.length === 0)
|
|
7
|
+
if (tokens.length === 0) {
|
|
8
|
+
return <span className={className}>{text}</span>
|
|
9
|
+
}
|
|
18
10
|
const parts: React.ReactNode[] = []
|
|
19
11
|
let cursor = 0
|
|
20
12
|
for (const [index, token] of tokens.entries()) {
|
|
21
|
-
if (token.start > cursor)
|
|
13
|
+
if (token.start > cursor) {
|
|
14
|
+
parts.push(text.slice(cursor, token.start))
|
|
15
|
+
}
|
|
22
16
|
parts.push(
|
|
23
|
-
<span
|
|
24
|
-
key={`${token.start}-${index}`}
|
|
25
|
-
className={cn('font-mono', token.kind === 'file' ? 'text-accent' : 'text-info')}>
|
|
17
|
+
<span key={`${token.start}-${index}`} className={cn('font-mono', token.kind === 'file' ? 'text-accent' : 'text-info')}>
|
|
26
18
|
{token.text}
|
|
27
19
|
</span>,
|
|
28
20
|
)
|
|
29
21
|
cursor = token.end
|
|
30
22
|
}
|
|
31
|
-
if (cursor < text.length)
|
|
23
|
+
if (cursor < text.length) {
|
|
24
|
+
parts.push(text.slice(cursor))
|
|
25
|
+
}
|
|
32
26
|
return (
|
|
33
27
|
<span className={className}>
|
|
34
28
|
{parts.map((part, index) => (
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import type {
|
|
3
|
-
PermissionRequest,
|
|
4
|
-
QuestionBehavior,
|
|
5
|
-
UserQuestion,
|
|
6
|
-
UserQuestionOption,
|
|
7
|
-
} from '@workerdeck/protocol'
|
|
2
|
+
import type { PermissionRequest, QuestionBehavior, UserQuestion, UserQuestionOption } from '@workerdeck/protocol'
|
|
8
3
|
import { MessageCircleQuestion, X } from 'lucide-react'
|
|
9
4
|
import { Badge } from '../ui/Badge.tsx'
|
|
10
5
|
import { Button } from '../ui/Button.tsx'
|
|
@@ -17,23 +12,23 @@ export type QuestionBehaviorMeta = {
|
|
|
17
12
|
description: string
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
/** The AskUserQuestion policies surfaced on job/session creation forms. */
|
|
21
15
|
export const QUESTION_BEHAVIORS: QuestionBehaviorMeta[] = [
|
|
22
16
|
{ value: 'auto', label: 'Auto-answer', description: 'pick each question’s recommended option' },
|
|
23
17
|
{ value: 'ask', label: 'Ask', description: 'wait for a watcher or webhook controller to answer' },
|
|
24
18
|
{ value: 'deny', label: 'Disabled', description: 'the agent is told to decide on its own' },
|
|
25
19
|
]
|
|
26
20
|
|
|
27
|
-
/** Extract well-formed questions from an AskUserQuestion permission request's input. */
|
|
28
21
|
export function parseUserQuestions(input: Record<string, unknown>): UserQuestion[] {
|
|
29
22
|
const raw = Array.isArray(input.questions) ? input.questions : []
|
|
30
23
|
return raw.flatMap((entry): UserQuestion[] => {
|
|
31
24
|
const q = entry as Partial<UserQuestion>
|
|
32
|
-
if (typeof q.question !== 'string' || !Array.isArray(q.options))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
if (options.length === 0)
|
|
25
|
+
if (typeof q.question !== 'string' || !Array.isArray(q.options)) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
const options = q.options.filter((o): o is UserQuestionOption => typeof (o as UserQuestionOption | undefined)?.label === 'string')
|
|
29
|
+
if (options.length === 0) {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
37
32
|
return [
|
|
38
33
|
{
|
|
39
34
|
question: q.question,
|
|
@@ -49,33 +44,24 @@ type Selection = { labels: string[]; other: string; otherActive: boolean }
|
|
|
49
44
|
|
|
50
45
|
const EMPTY_SELECTION: Selection = { labels: [], other: '', otherActive: false }
|
|
51
46
|
|
|
52
|
-
/** A question's answer string: chosen label(s) (multi-select comma-joined), with any
|
|
53
|
-
* free-text "Other" appended — the value the CLI expects in `updatedInput.answers`. */
|
|
54
47
|
function answerFor(selection: Selection): string {
|
|
55
48
|
const parts = [...selection.labels]
|
|
56
|
-
if (selection.otherActive && selection.other.trim())
|
|
49
|
+
if (selection.otherActive && selection.other.trim()) {
|
|
50
|
+
parts.push(selection.other.trim())
|
|
51
|
+
}
|
|
57
52
|
return parts.join(', ')
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
export interface QuestionPromptProps {
|
|
61
|
-
/** A pending permission whose toolName is 'AskUserQuestion'. */
|
|
62
56
|
request: PermissionRequest
|
|
63
|
-
/** Allow the tool with `updatedInput` (the original input plus `answers`). */
|
|
64
57
|
onAnswer: (requestId: string, updatedInput: Record<string, unknown>) => void
|
|
65
|
-
/** Deny the tool — the model proceeds without an answer. */
|
|
66
58
|
onDismiss: (requestId: string, message?: string) => void
|
|
67
59
|
className?: string
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
/** Interactive form for the AskUserQuestion tool: option buttons per question
|
|
71
|
-
* (multi-select where the question allows it), a free-text "Other" escape hatch,
|
|
72
|
-
* and the focused option's preview. Falls back to nothing renderable → the caller
|
|
73
|
-
* should show a generic PermissionPrompt if `parseUserQuestions` finds no questions. */
|
|
74
62
|
export function QuestionPrompt({ request, onAnswer, onDismiss, className }: QuestionPromptProps) {
|
|
75
63
|
const questions = parseUserQuestions(request.input)
|
|
76
|
-
const [selections, setSelections] = useState<Selection[]>(() =>
|
|
77
|
-
questions.map(() => EMPTY_SELECTION),
|
|
78
|
-
)
|
|
64
|
+
const [selections, setSelections] = useState<Selection[]>(() => questions.map(() => EMPTY_SELECTION))
|
|
79
65
|
|
|
80
66
|
const update = (index: number, patch: Partial<Selection>) => {
|
|
81
67
|
setSelections((prev) => prev.map((s, i) => (i === index ? { ...s, ...patch } : s)))
|
|
@@ -85,9 +71,7 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
85
71
|
const current = selections[index] ?? EMPTY_SELECTION
|
|
86
72
|
if (multiSelect) {
|
|
87
73
|
update(index, {
|
|
88
|
-
labels: current.labels.includes(label)
|
|
89
|
-
? current.labels.filter((l) => l !== label)
|
|
90
|
-
: [...current.labels, label],
|
|
74
|
+
labels: current.labels.includes(label) ? current.labels.filter((l) => l !== label) : [...current.labels, label],
|
|
91
75
|
})
|
|
92
76
|
} else {
|
|
93
77
|
update(index, { labels: current.labels[0] === label ? [] : [label], otherActive: false })
|
|
@@ -105,58 +89,52 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
105
89
|
}
|
|
106
90
|
|
|
107
91
|
return (
|
|
108
|
-
<div
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<MessageCircleQuestion className='mt-0.5 size-4 shrink-0 text-info' />
|
|
113
|
-
<div className='flex min-w-0 flex-1 flex-col gap-3'>
|
|
92
|
+
<div data-slot="question-prompt" className={cn('rounded-lg border border-info/40 bg-info-bg p-3', className)}>
|
|
93
|
+
<div className="flex items-start gap-2.5">
|
|
94
|
+
<MessageCircleQuestion className="mt-0.5 size-4 shrink-0 text-info" />
|
|
95
|
+
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
|
114
96
|
{questions.map((q, index) => {
|
|
115
97
|
const selection = selections[index] ?? EMPTY_SELECTION
|
|
116
98
|
return (
|
|
117
|
-
<div key={index} className=
|
|
118
|
-
<div className=
|
|
119
|
-
{q.header ? <Badge variant=
|
|
120
|
-
<span className=
|
|
99
|
+
<div key={index} className="flex flex-col gap-1.5">
|
|
100
|
+
<div className="flex items-center gap-2">
|
|
101
|
+
{q.header ? <Badge variant="info">{q.header}</Badge> : null}
|
|
102
|
+
<span className="text-body-sm font-medium text-fg-1">{q.question}</span>
|
|
121
103
|
</div>
|
|
122
|
-
<div className=
|
|
104
|
+
<div className="flex flex-col gap-1">
|
|
123
105
|
{q.options.map((option) => {
|
|
124
106
|
const selected = selection.labels.includes(option.label)
|
|
125
107
|
return (
|
|
126
108
|
<button
|
|
127
109
|
key={option.label}
|
|
128
|
-
type=
|
|
110
|
+
type="button"
|
|
129
111
|
onClick={() => toggle(index, option.label, q.multiSelect === true)}
|
|
130
112
|
className={cn(
|
|
131
113
|
'rounded-md border px-2.5 py-1.5 text-left transition-colors',
|
|
132
|
-
selected
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<span className=
|
|
137
|
-
{option.label}
|
|
138
|
-
</span>
|
|
139
|
-
{option.description ? (
|
|
140
|
-
<span className='block text-label text-fg-4'>{option.description}</span>
|
|
141
|
-
) : null}
|
|
114
|
+
selected ? 'border-info bg-bg' : 'border-border bg-bg/50 hover:border-border-strong hover:bg-bg',
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
<span className="block text-body-sm font-medium text-fg-1">{option.label}</span>
|
|
118
|
+
{option.description ? <span className="block text-label text-fg-4">{option.description}</span> : null}
|
|
142
119
|
{selected && option.preview ? (
|
|
143
|
-
<pre className=
|
|
120
|
+
<pre className="mt-1.5 max-h-40 overflow-auto rounded-md bg-code-bg px-2.5 py-1.5 font-mono text-label whitespace-pre-wrap text-fg-2">
|
|
144
121
|
{option.preview}
|
|
145
122
|
</pre>
|
|
146
123
|
) : null}
|
|
147
124
|
</button>
|
|
148
125
|
)
|
|
149
126
|
})}
|
|
150
|
-
<div className=
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
151
128
|
<button
|
|
152
|
-
type=
|
|
129
|
+
type="button"
|
|
153
130
|
onClick={() => update(index, { otherActive: !selection.otherActive })}
|
|
154
131
|
className={cn(
|
|
155
132
|
'shrink-0 rounded-md border px-2.5 py-1.5 text-body-sm font-medium transition-colors',
|
|
156
133
|
selection.otherActive
|
|
157
134
|
? 'border-info bg-bg text-fg-1'
|
|
158
135
|
: 'border-border bg-bg/50 text-fg-3 hover:border-border-strong hover:bg-bg',
|
|
159
|
-
)}
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
160
138
|
Other…
|
|
161
139
|
</button>
|
|
162
140
|
{selection.otherActive ? (
|
|
@@ -164,8 +142,8 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
164
142
|
autoFocus
|
|
165
143
|
value={selection.other}
|
|
166
144
|
onChange={(e) => update(index, { other: e.target.value })}
|
|
167
|
-
placeholder=
|
|
168
|
-
className=
|
|
145
|
+
placeholder="Type your own answer"
|
|
146
|
+
className="flex-1"
|
|
169
147
|
/>
|
|
170
148
|
) : null}
|
|
171
149
|
</div>
|
|
@@ -174,18 +152,19 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
174
152
|
)
|
|
175
153
|
})}
|
|
176
154
|
<div>
|
|
177
|
-
<Button size=
|
|
155
|
+
<Button size="sm" onClick={submit} disabled={!complete}>
|
|
178
156
|
Answer
|
|
179
157
|
</Button>
|
|
180
158
|
</div>
|
|
181
159
|
</div>
|
|
182
160
|
<Button
|
|
183
|
-
variant=
|
|
184
|
-
size=
|
|
185
|
-
aria-label=
|
|
186
|
-
className=
|
|
187
|
-
onClick={() => onDismiss(request.id, 'Question dismissed by user')}
|
|
188
|
-
|
|
161
|
+
variant="ghost"
|
|
162
|
+
size="icon-sm"
|
|
163
|
+
aria-label="Dismiss question"
|
|
164
|
+
className="shrink-0"
|
|
165
|
+
onClick={() => onDismiss(request.id, 'Question dismissed by user')}
|
|
166
|
+
>
|
|
167
|
+
<X className="size-3.5" />
|
|
189
168
|
</Button>
|
|
190
169
|
</div>
|
|
191
170
|
</div>
|
|
@@ -5,20 +5,19 @@ import { Response } from './Response.tsx'
|
|
|
5
5
|
|
|
6
6
|
export interface ReasoningProps {
|
|
7
7
|
children: string
|
|
8
|
-
/** Auto-opens while true, auto-closes shortly after it flips false. */
|
|
9
8
|
isStreaming?: boolean
|
|
10
9
|
defaultOpen?: boolean
|
|
11
10
|
className?: string
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
/** Collapsible extended-thinking block: open while the model is thinking, tucks itself
|
|
15
|
-
* away once the thought is finished (unless the user toggled it manually). */
|
|
16
13
|
export function Reasoning({ children, isStreaming = false, defaultOpen, className }: ReasoningProps) {
|
|
17
14
|
const [open, setOpen] = useState(defaultOpen ?? isStreaming)
|
|
18
15
|
const userToggled = useRef(false)
|
|
19
16
|
|
|
20
17
|
useEffect(() => {
|
|
21
|
-
if (userToggled.current)
|
|
18
|
+
if (userToggled.current) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
22
21
|
if (isStreaming) {
|
|
23
22
|
setOpen(true)
|
|
24
23
|
} else {
|
|
@@ -27,30 +26,28 @@ export function Reasoning({ children, isStreaming = false, defaultOpen, classNam
|
|
|
27
26
|
}
|
|
28
27
|
}, [isStreaming])
|
|
29
28
|
|
|
30
|
-
// Models with encrypted thinking emit the blocks but never the summary text. Then there is
|
|
31
|
-
// nothing to expand: show a bare "Thinking…" marker live, and nothing at all once it's done.
|
|
32
29
|
const hasText = children.trim() !== ''
|
|
33
|
-
if (!hasText && !isStreaming)
|
|
30
|
+
if (!hasText && !isStreaming) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
34
33
|
|
|
35
34
|
return (
|
|
36
|
-
<div data-slot=
|
|
35
|
+
<div data-slot="reasoning" data-streaming={isStreaming || undefined} className={cn('w-full', className)}>
|
|
37
36
|
<button
|
|
38
|
-
type=
|
|
37
|
+
type="button"
|
|
39
38
|
disabled={!hasText}
|
|
40
39
|
onClick={() => {
|
|
41
40
|
userToggled.current = true
|
|
42
41
|
setOpen((v) => !v)
|
|
43
42
|
}}
|
|
44
|
-
className=
|
|
45
|
-
|
|
43
|
+
className="flex items-center gap-1.5 text-label text-fg-3 transition-colors outline-none disabled:cursor-default hover:text-fg-1 disabled:hover:text-fg-3"
|
|
44
|
+
>
|
|
45
|
+
<Brain className="size-3.5" />
|
|
46
46
|
<span>{isStreaming ? 'Thinking…' : 'Thought process'}</span>
|
|
47
|
-
{hasText ? (
|
|
48
|
-
<ChevronDown className={cn('size-3.5 transition-transform', open && 'rotate-180')} />
|
|
49
|
-
) : null}
|
|
47
|
+
{hasText ? <ChevronDown className={cn('size-3.5 transition-transform', open && 'rotate-180')} /> : null}
|
|
50
48
|
</button>
|
|
51
49
|
{open && hasText ? (
|
|
52
|
-
<div
|
|
53
|
-
className='mt-2 border-l-2 border-border pl-3 text-body-sm text-fg-3 [&_*]:text-fg-3'>
|
|
50
|
+
<div className="mt-2 border-l-2 border-border pl-3 text-body-sm text-fg-3 [&_*]:text-fg-3">
|
|
54
51
|
<Response streaming={isStreaming}>{children}</Response>
|
|
55
52
|
</div>
|
|
56
53
|
) : null}
|