@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.
Files changed (145) hide show
  1. package/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
  2. package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
  3. package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
  4. package/build/format-B8ZM9LPy.d.mts +16 -0
  5. package/build/format.d.mts +8 -75
  6. package/build/format.mjs +2 -2
  7. package/build/index.d.mts +63 -1138
  8. package/build/index.mjs +4 -300
  9. package/build/index.mjs.map +1 -1
  10. package/build/scoped.css +15 -23
  11. package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
  12. package/build/status-C0HSLZas.mjs.map +1 -0
  13. package/build/workspace.d.mts +5 -126
  14. package/build/workspace.mjs +11 -143
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +4 -4
  17. package/src/components/agent/CodeEditor.tsx +31 -103
  18. package/src/components/agent/Composer.tsx +114 -333
  19. package/src/components/agent/ContextDialog.tsx +20 -41
  20. package/src/components/agent/ContextRing.tsx +6 -28
  21. package/src/components/agent/Conversation.tsx +21 -33
  22. package/src/components/agent/EditorTabs.tsx +39 -67
  23. package/src/components/agent/EngineIcon.tsx +29 -72
  24. package/src/components/agent/EntryIcon.tsx +16 -0
  25. package/src/components/agent/FileCard.tsx +10 -16
  26. package/src/components/agent/FileTree.tsx +52 -125
  27. package/src/components/agent/FileViewer.tsx +30 -60
  28. package/src/components/agent/HostFilesDialog.tsx +40 -69
  29. package/src/components/agent/Loader.tsx +3 -24
  30. package/src/components/agent/McpDialog.tsx +64 -137
  31. package/src/components/agent/Message.tsx +2 -21
  32. package/src/components/agent/ModelSelect.tsx +33 -62
  33. package/src/components/agent/PermissionModeSelect.tsx +21 -79
  34. package/src/components/agent/PermissionPrompt.tsx +59 -63
  35. package/src/components/agent/ProjectIcon.tsx +228 -93
  36. package/src/components/agent/PromptTokenText.tsx +10 -16
  37. package/src/components/agent/QuestionPrompt.tsx +43 -64
  38. package/src/components/agent/Reasoning.tsx +13 -16
  39. package/src/components/agent/Response.tsx +3 -9
  40. package/src/components/agent/Scrubber.tsx +31 -80
  41. package/src/components/agent/SessionBrowser.tsx +64 -243
  42. package/src/components/agent/SessionEmptyState.tsx +10 -40
  43. package/src/components/agent/SessionInfoDialog.tsx +46 -78
  44. package/src/components/agent/SessionItem.tsx +53 -320
  45. package/src/components/agent/SessionList.tsx +23 -39
  46. package/src/components/agent/SessionPanel.tsx +315 -996
  47. package/src/components/agent/SessionStatusIcon.tsx +14 -28
  48. package/src/components/agent/SessionSteps.tsx +34 -187
  49. package/src/components/agent/SessionWorkspace.tsx +104 -219
  50. package/src/components/agent/SkillsDialog.tsx +35 -80
  51. package/src/components/agent/StatusBar.tsx +51 -136
  52. package/src/components/agent/SubagentStrip.tsx +12 -77
  53. package/src/components/agent/ToolCallCard.tsx +94 -135
  54. package/src/components/agent/Transcript.tsx +103 -1288
  55. package/src/components/agent/TranscriptItemView.tsx +155 -0
  56. package/src/components/agent/TranscriptRows.tsx +418 -0
  57. package/src/components/agent/UsageDialog.tsx +7 -29
  58. package/src/components/agent/UsageMeters.tsx +18 -73
  59. package/src/components/agent/pulse.tsx +9 -32
  60. package/src/components/agent/scrubber-marks.ts +33 -103
  61. package/src/components/agent/status.ts +1 -5
  62. package/src/components/agent/tool-result-fetch.tsx +3 -25
  63. package/src/components/agent/tool-result-image.tsx +32 -111
  64. package/src/components/agent/transcript-rows.ts +41 -118
  65. package/src/components/agent/transcript-variant.tsx +3 -80
  66. package/src/components/agent/use-height-epoch.ts +10 -24
  67. package/src/components/agent/use-path-links.ts +45 -63
  68. package/src/components/agent/use-subagent-frame.ts +106 -0
  69. package/src/components/agent/use-transcript-jumps.ts +49 -107
  70. package/src/components/prompt-area/animated-placeholder.tsx +6 -5
  71. package/src/components/prompt-area/clipboard-helpers.ts +26 -21
  72. package/src/components/prompt-area/cursor-helpers.ts +79 -37
  73. package/src/components/prompt-area/dom-helpers.ts +97 -69
  74. package/src/components/prompt-area/file-strip.tsx +90 -80
  75. package/src/components/prompt-area/html-to-markdown.ts +67 -46
  76. package/src/components/prompt-area/image-strip.tsx +8 -18
  77. package/src/components/prompt-area/index.ts +2 -15
  78. package/src/components/prompt-area/prompt-area-engine.ts +100 -115
  79. package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
  80. package/src/components/prompt-area/prompt-area.tsx +31 -38
  81. package/src/components/prompt-area/remove-button.tsx +3 -9
  82. package/src/components/prompt-area/segment-helpers.ts +4 -12
  83. package/src/components/prompt-area/trigger-popover.tsx +15 -27
  84. package/src/components/prompt-area/trigger-presets.ts +0 -10
  85. package/src/components/prompt-area/types.ts +3 -8
  86. package/src/components/prompt-area/use-chip-editing.ts +305 -0
  87. package/src/components/prompt-area/use-markdown-mode.ts +7 -17
  88. package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
  89. package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
  90. package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
  91. package/src/components/prompt-area/use-prompt-area.ts +170 -896
  92. package/src/components/prompt-area/use-trigger-search.ts +21 -19
  93. package/src/components/terminal/PermissionPrompt.tsx +56 -59
  94. package/src/components/terminal/QuestionPrompt.tsx +28 -79
  95. package/src/components/terminal/StatusLine.tsx +13 -63
  96. package/src/components/terminal/TerminalTranscript.tsx +44 -160
  97. package/src/components/terminal/affordances.tsx +68 -98
  98. package/src/components/terminal/blocks.ts +20 -156
  99. package/src/components/terminal/diff.tsx +19 -67
  100. package/src/components/terminal/height.ts +163 -313
  101. package/src/components/terminal/image-box.ts +0 -44
  102. package/src/components/terminal/items.tsx +150 -305
  103. package/src/components/terminal/markdown.tsx +36 -95
  104. package/src/components/terminal/press.tsx +26 -53
  105. package/src/components/terminal/prompt.tsx +40 -170
  106. package/src/components/terminal/result-preview.ts +8 -56
  107. package/src/components/terminal/row.tsx +11 -98
  108. package/src/components/terminal/scrubber.tsx +108 -401
  109. package/src/components/terminal/surface.tsx +6 -54
  110. package/src/components/terminal/todos.ts +66 -0
  111. package/src/components/terminal/tool-run.ts +21 -192
  112. package/src/components/ui/AlertDialog.tsx +8 -23
  113. package/src/components/ui/Badge.tsx +18 -29
  114. package/src/components/ui/Button.tsx +7 -15
  115. package/src/components/ui/Card.tsx +2 -13
  116. package/src/components/ui/CodeBlock.tsx +10 -37
  117. package/src/components/ui/CopyButton.tsx +10 -23
  118. package/src/components/ui/Dialog.tsx +23 -36
  119. package/src/components/ui/Empty.tsx +6 -29
  120. package/src/components/ui/Input.tsx +14 -16
  121. package/src/components/ui/Menu.tsx +6 -18
  122. package/src/components/ui/PortalScope.tsx +1 -21
  123. package/src/components/ui/ProgressRing.tsx +8 -19
  124. package/src/components/ui/Select.tsx +18 -31
  125. package/src/components/ui/Sonner.tsx +1 -2
  126. package/src/components/ui/Spinner.tsx +1 -1
  127. package/src/components/ui/Splitter.tsx +30 -64
  128. package/src/components/ui/Textarea.tsx +2 -5
  129. package/src/components/ui/Tooltip.tsx +3 -13
  130. package/src/format.ts +0 -9
  131. package/src/index.ts +16 -113
  132. package/src/lib/clipboard.ts +8 -28
  133. package/src/lib/css.ts +8 -0
  134. package/src/lib/format.ts +101 -86
  135. package/src/lib/plan-request.ts +16 -0
  136. package/src/lib/status.ts +45 -95
  137. package/src/lib/tool-icon.ts +39 -47
  138. package/src/lib/utils.ts +1 -3
  139. package/src/styles/terminal.css +81 -49
  140. package/src/styles/theme.css +96 -32
  141. package/src/workspace.ts +1 -24
  142. package/build/SessionPanel-BobynUo4.d.mts +0 -669
  143. package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
  144. package/build/format-ljc3lKpA.d.mts +0 -59
  145. 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, Atom, Beaker, Bike, Binary, Blocks, Bot, Box, Brain, Briefcase, Brush, Bug,
4
- Building2, Cable, Camera, Car, Cat, CircuitBoard, Cloud, Code, Coffee, Cog, Compass,
5
- Container, Cpu, CreditCard, Database, Diamond, Dog, Feather, Film, Flame, FlaskConical,
6
- Folder, Gamepad2, Gauge, Gem, Ghost, Gift, GitBranch, Globe, GraduationCap, Hammer,
7
- Hexagon, House, Image, Key, Landmark, Laptop, Layers, Leaf, Library, Lightbulb, Lock,
8
- Mail, Map, MessageCircle, Mic, Monitor, Moon, Mountain, Music, Network, Newspaper,
9
- Notebook, Orbit, Package, Palette, PenTool, Phone, PiggyBank, Plane, Plug, Puzzle,
10
- Radar, Radio, Receipt, Rocket, Ruler, Satellite, Scale, Scissors, Server, Shield, Ship,
11
- ShoppingCart, Signal, Smartphone, Sparkles, Sprout, Square, Star, Sun, Table, Tag,
12
- Target, Telescope, Terminal, TestTube, TreePine, Trophy, Truck, Umbrella, Users, Wallet,
13
- Wand, Waves, Webhook, Wifi, Wrench, Zap,
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, atom: Atom, beaker: Beaker, bike: Bike, binary: Binary, blocks: Blocks,
64
- bot: Bot, box: Box, brain: Brain, briefcase: Briefcase, brush: Brush, bug: Bug,
65
- 'building-2': Building2, cable: Cable, camera: Camera, car: Car, cat: Cat,
66
- 'circuit-board': CircuitBoard, cloud: Cloud, code: Code, coffee: Coffee, cog: Cog,
67
- compass: Compass, container: Container, cpu: Cpu, 'credit-card': CreditCard,
68
- database: Database, diamond: Diamond, dog: Dog, feather: Feather, film: Film,
69
- flame: Flame, 'flask-conical': FlaskConical, folder: Folder, 'gamepad-2': Gamepad2,
70
- gauge: Gauge, gem: Gem, ghost: Ghost, gift: Gift, 'git-branch': GitBranch, globe: Globe,
71
- 'graduation-cap': GraduationCap, hammer: Hammer, hexagon: Hexagon, house: House,
72
- image: Image, key: Key, landmark: Landmark, laptop: Laptop, layers: Layers, leaf: Leaf,
73
- library: Library, lightbulb: Lightbulb, lock: Lock, mail: Mail, map: Map,
74
- 'message-circle': MessageCircle, mic: Mic, monitor: Monitor, moon: Moon,
75
- mountain: Mountain, music: Music, network: Network, newspaper: Newspaper,
76
- notebook: Notebook, orbit: Orbit, package: Package, palette: Palette, 'pen-tool': PenTool,
77
- phone: Phone, 'piggy-bank': PiggyBank, plane: Plane, plug: Plug, puzzle: Puzzle,
78
- radar: Radar, radio: Radio, receipt: Receipt, rocket: Rocket, ruler: Ruler,
79
- satellite: Satellite, scale: Scale, scissors: Scissors, server: Server, shield: Shield,
80
- ship: Ship, 'shopping-cart': ShoppingCart, signal: Signal, smartphone: Smartphone,
81
- sparkles: Sparkles, sprout: Sprout, square: Square, star: Star, sun: Sun, table: Table,
82
- tag: Tag, target: Target, telescope: Telescope, terminal: Terminal, 'test-tube': TestTube,
83
- 'tree-pine': TreePine, trophy: Trophy, truck: Truck, umbrella: Umbrella, users: Users,
84
- wallet: Wallet, wand: Wand, waves: Waves, webhook: Webhook, wifi: Wifi, wrench: Wrench,
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) return null
243
+ if (!icon) {
244
+ return null
245
+ }
103
246
  if (icon.type === 'image') {
104
- if (!src) return null
105
- return (
106
- <img
107
- src={src}
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) return <span className={className}>{text}</span>
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) parts.push(text.slice(cursor, token.start))
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) parts.push(text.slice(cursor))
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)) return []
33
- const options = q.options.filter(
34
- (o): o is UserQuestionOption => typeof (o as UserQuestionOption | undefined)?.label === 'string',
35
- )
36
- if (options.length === 0) return []
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()) parts.push(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
- data-slot='question-prompt'
110
- className={cn('rounded-lg border border-info/40 bg-info-bg p-3', className)}>
111
- <div className='flex items-start gap-2.5'>
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='flex flex-col gap-1.5'>
118
- <div className='flex items-center gap-2'>
119
- {q.header ? <Badge variant='info'>{q.header}</Badge> : null}
120
- <span className='text-body-sm font-medium text-fg-1'>{q.question}</span>
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='flex flex-col gap-1'>
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='button'
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
- ? 'border-info bg-bg'
134
- : 'border-border bg-bg/50 hover:border-border-strong hover:bg-bg',
135
- )}>
136
- <span className='block text-body-sm font-medium text-fg-1'>
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='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'>
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='flex items-center gap-2'>
127
+ <div className="flex items-center gap-2">
151
128
  <button
152
- type='button'
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='Type your own answer'
168
- className='flex-1'
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='sm' onClick={submit} disabled={!complete}>
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='ghost'
184
- size='icon-sm'
185
- aria-label='Dismiss question'
186
- className='shrink-0'
187
- onClick={() => onDismiss(request.id, 'Question dismissed by user')}>
188
- <X className='size-3.5' />
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) return
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) return null
30
+ if (!hasText && !isStreaming) {
31
+ return null
32
+ }
34
33
 
35
34
  return (
36
- <div data-slot='reasoning' data-streaming={isStreaming || undefined} className={cn('w-full', className)}>
35
+ <div data-slot="reasoning" data-streaming={isStreaming || undefined} className={cn('w-full', className)}>
37
36
  <button
38
- type='button'
37
+ type="button"
39
38
  disabled={!hasText}
40
39
  onClick={() => {
41
40
  userToggled.current = true
42
41
  setOpen((v) => !v)
43
42
  }}
44
- 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'>
45
- <Brain className='size-3.5' />
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}