@workerdeck/ui 0.15.0 → 0.17.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/README.md +60 -0
- package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-CnNYEX80.d.mts} +170 -21
- package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DMPhsNlW.mjs} +4759 -1483
- package/build/SessionPanel-DMPhsNlW.mjs.map +1 -0
- package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
- package/build/format.d.mts +39 -4
- package/build/format.mjs +2 -118
- package/build/index.d.mts +493 -45
- package/build/index.mjs +343 -17
- package/build/index.mjs.map +1 -1
- package/build/status-Ydzi7n6j.mjs +143 -0
- package/build/status-Ydzi7n6j.mjs.map +1 -0
- package/build/workspace.d.mts +9 -1
- package/build/workspace.mjs +108 -5
- package/build/workspace.mjs.map +1 -1
- package/package.json +16 -7
- package/src/components/agent/Composer.tsx +189 -89
- package/src/components/agent/Conversation.tsx +12 -12
- package/src/components/agent/FileCard.tsx +0 -26
- package/src/components/agent/FileTree.tsx +9 -8
- package/src/components/agent/Loader.tsx +22 -72
- package/src/components/agent/Message.tsx +11 -46
- package/src/components/agent/PermissionPrompt.tsx +0 -92
- package/src/components/agent/ProjectIcon.tsx +119 -0
- package/src/components/agent/QuestionPrompt.tsx +0 -122
- package/src/components/agent/Reasoning.tsx +5 -19
- package/src/components/agent/Response.tsx +1 -132
- package/src/components/agent/SessionBrowser.tsx +84 -3
- package/src/components/agent/SessionPanel.tsx +249 -28
- package/src/components/agent/SessionWorkspace.tsx +29 -0
- package/src/components/agent/StatusBar.tsx +20 -4
- package/src/components/agent/ToolCallCard.tsx +85 -112
- package/src/components/agent/Transcript.tsx +780 -203
- package/src/components/agent/UsageDialog.tsx +20 -106
- package/src/components/agent/UsageMeters.tsx +133 -0
- package/src/components/agent/pulse.tsx +3 -2
- package/src/components/agent/tool-result-fetch.tsx +36 -0
- package/src/components/agent/tool-result-image.tsx +209 -0
- package/src/components/agent/transcript-rows.ts +173 -0
- package/src/components/agent/transcript-variant.tsx +29 -51
- package/src/components/agent/use-height-epoch.ts +60 -0
- package/src/components/agent/use-path-links.ts +147 -0
- package/src/components/agent/use-transcript-jumps.ts +190 -0
- package/src/components/prompt-area/cursor-helpers.ts +65 -0
- package/src/components/prompt-area/use-prompt-area.ts +16 -10
- package/src/components/terminal/PermissionPrompt.tsx +119 -0
- package/src/components/terminal/QuestionPrompt.tsx +322 -0
- package/src/components/terminal/StatusLine.tsx +159 -0
- package/src/components/terminal/TerminalTranscript.tsx +225 -0
- package/src/components/terminal/affordances.tsx +118 -0
- package/src/components/terminal/blocks.ts +232 -0
- package/src/components/terminal/diff.tsx +130 -0
- package/src/components/terminal/height.ts +770 -0
- package/src/components/terminal/image-box.ts +53 -0
- package/src/components/terminal/items.tsx +486 -0
- package/src/components/terminal/markdown.tsx +191 -0
- package/src/components/terminal/press.tsx +120 -0
- package/src/components/terminal/prompt.tsx +343 -0
- package/src/components/terminal/result-preview.ts +86 -0
- package/src/components/terminal/row.tsx +132 -0
- package/src/components/terminal/scrubber.tsx +784 -0
- package/src/components/terminal/surface.tsx +80 -0
- package/src/components/terminal/tool-run.ts +224 -0
- package/src/index.ts +36 -1
- package/src/lib/status.ts +59 -3
- package/src/lib/tool-icon.ts +14 -0
- package/src/styles/terminal.css +1081 -0
- package/src/styles/theme.css +41 -0
- package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
- package/build/format.mjs.map +0 -1
- package/src/components/agent/line-prompt.tsx +0 -249
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'react'
|
|
2
2
|
import { cn } from '../../lib/utils.ts'
|
|
3
|
-
import { LINE_TEXT, LineGlyph, useLines } from './transcript-variant.tsx'
|
|
4
3
|
|
|
5
4
|
export interface MessageProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
5
|
from: 'user' | 'assistant'
|
|
@@ -16,67 +15,33 @@ export interface MessageProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
16
15
|
* column separates a prompt from the reply it produced. The bubble alone is
|
|
17
16
|
* enough to say who spoke.
|
|
18
17
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* already distinguishable by its marker, and the bubble's padding is vertical
|
|
22
|
-
* space the terminal treatment refuses to spend.
|
|
18
|
+
* The terminal theme draws none of this: it is its own renderer
|
|
19
|
+
* (`components/terminal/`) and mounts instead of these rows.
|
|
23
20
|
*/
|
|
24
21
|
export function Message({ from, className, children, ...props }: MessageProps) {
|
|
25
|
-
const lines = useLines()
|
|
26
22
|
return (
|
|
27
23
|
<div
|
|
28
24
|
data-slot='message'
|
|
29
25
|
data-from={from}
|
|
30
|
-
className={cn(
|
|
31
|
-
'flex w-full',
|
|
32
|
-
lines
|
|
33
|
-
? cn(
|
|
34
|
-
'flex-row gap-2',
|
|
35
|
-
// What YOU said, on a band of its own — the CLI's own treatment.
|
|
36
|
-
// Full-bleed (the negative margin cancels the row's padding) so
|
|
37
|
-
// the band lines up with the hover highlight rather than sitting
|
|
38
|
-
// inside it, and square-ish so it reads as a strip, not a bubble.
|
|
39
|
-
from === 'user' && '-mx-1 rounded-sm bg-surface px-1',
|
|
40
|
-
)
|
|
41
|
-
: 'flex-col items-start gap-1',
|
|
42
|
-
className,
|
|
43
|
-
)}
|
|
26
|
+
className={cn('flex w-full flex-col items-start gap-1', className)}
|
|
44
27
|
{...props}>
|
|
45
|
-
{
|
|
46
|
-
<>
|
|
47
|
-
{/* The prompt marker is accent, the same blue VS Code's own chat and
|
|
48
|
-
the iOS app give it: in a column of grey rows it is the one thing
|
|
49
|
-
that says "you said this", and it is what the eye scrolls back to. */}
|
|
50
|
-
<LineGlyph className={from === 'user' ? 'text-accent' : 'text-fg-3'}>
|
|
51
|
-
{from === 'user' ? '❯' : '●'}
|
|
52
|
-
</LineGlyph>
|
|
53
|
-
<div className='flex min-w-0 flex-1 flex-col gap-1'>{children}</div>
|
|
54
|
-
</>
|
|
55
|
-
) : (
|
|
56
|
-
children
|
|
57
|
-
)}
|
|
28
|
+
{children}
|
|
58
29
|
</div>
|
|
59
30
|
)
|
|
60
31
|
}
|
|
61
32
|
|
|
62
33
|
export function MessageContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
63
|
-
const lines = useLines()
|
|
64
34
|
return (
|
|
65
35
|
<div
|
|
66
36
|
data-slot='message-content'
|
|
67
37
|
className={cn(
|
|
68
|
-
'min-w-0',
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'in-data-[from=user]:max-w-[85%] in-data-[from=user]:rounded-lg in-data-[from=user]:rounded-bl-sm',
|
|
76
|
-
'in-data-[from=user]:bg-accent-bg in-data-[from=user]:px-3 in-data-[from=user]:py-2',
|
|
77
|
-
'in-data-[from=user]:whitespace-pre-wrap',
|
|
78
|
-
'in-data-[from=assistant]:w-full',
|
|
79
|
-
),
|
|
38
|
+
'min-w-0 text-body-sm leading-6 text-fg-1',
|
|
39
|
+
// Bubble treatment only within a user message row.
|
|
40
|
+
// The tail points bottom-*left* now that the bubble is left-aligned.
|
|
41
|
+
'in-data-[from=user]:max-w-[85%] in-data-[from=user]:rounded-lg in-data-[from=user]:rounded-bl-sm',
|
|
42
|
+
'in-data-[from=user]:bg-accent-bg in-data-[from=user]:px-3 in-data-[from=user]:py-2',
|
|
43
|
+
'in-data-[from=user]:whitespace-pre-wrap',
|
|
44
|
+
'in-data-[from=assistant]:w-full',
|
|
80
45
|
className,
|
|
81
46
|
)}
|
|
82
47
|
{...props}
|
|
@@ -6,14 +6,6 @@ import { Input } from '../ui/Input.tsx'
|
|
|
6
6
|
import { cn } from '../../lib/utils.ts'
|
|
7
7
|
import { toolInputPreview } from '../../lib/format.ts'
|
|
8
8
|
import { toolIcon } from '../../lib/tool-icon.ts'
|
|
9
|
-
import {
|
|
10
|
-
LINE_INDENT,
|
|
11
|
-
LineHint,
|
|
12
|
-
LineInput,
|
|
13
|
-
LineOptionList,
|
|
14
|
-
LinePayload,
|
|
15
|
-
} from './line-prompt.tsx'
|
|
16
|
-
import { LineGlyph, useLines } from './transcript-variant.tsx'
|
|
17
9
|
|
|
18
10
|
export interface PermissionPromptProps {
|
|
19
11
|
request: PermissionRequest
|
|
@@ -37,11 +29,9 @@ export interface PermissionPromptProps {
|
|
|
37
29
|
* already written the sentence that says so.
|
|
38
30
|
*/
|
|
39
31
|
export function PermissionPrompt({ request, onApprove, onDeny, className }: PermissionPromptProps) {
|
|
40
|
-
const lines = useLines()
|
|
41
32
|
const [showInput, setShowInput] = useState(false)
|
|
42
33
|
const [denying, setDenying] = useState(false)
|
|
43
34
|
const [reason, setReason] = useState('')
|
|
44
|
-
const [focused, setFocused] = useState(0)
|
|
45
35
|
|
|
46
36
|
const deny = (interrupt: boolean) => {
|
|
47
37
|
const message = reason.trim()
|
|
@@ -52,88 +42,6 @@ export function PermissionPrompt({ request, onApprove, onDeny, className }: Perm
|
|
|
52
42
|
|
|
53
43
|
const summary = toolInputPreview(request.input)
|
|
54
44
|
const ToolIcon = toolIcon(request.toolName)
|
|
55
|
-
const heading = request.title ?? request.displayName ?? 'Permission needed'
|
|
56
|
-
|
|
57
|
-
if (lines) {
|
|
58
|
-
// The same three outcomes as the card, as rows. "Deny" opens the reason
|
|
59
|
-
// field rather than acting, because a denial the agent can learn from is the
|
|
60
|
-
// one worth defaulting to — an empty field still just denies.
|
|
61
|
-
const options = [
|
|
62
|
-
{ key: 'allow', label: 'Allow' },
|
|
63
|
-
{
|
|
64
|
-
key: 'deny',
|
|
65
|
-
label: 'Deny',
|
|
66
|
-
description: 'the turn continues',
|
|
67
|
-
detail: denying ? (
|
|
68
|
-
<LineInput
|
|
69
|
-
value={reason}
|
|
70
|
-
onChange={setReason}
|
|
71
|
-
onSubmit={() => deny(false)}
|
|
72
|
-
onCancel={() => setDenying(false)}
|
|
73
|
-
placeholder='Reason (optional) — the agent reads this and can try something else'
|
|
74
|
-
/>
|
|
75
|
-
) : undefined,
|
|
76
|
-
},
|
|
77
|
-
{ key: 'stop', label: 'Deny & stop', description: 'interrupt the turn', danger: true },
|
|
78
|
-
]
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div
|
|
82
|
-
data-slot='permission-prompt'
|
|
83
|
-
className={cn('w-full', className)}
|
|
84
|
-
onKeyDown={(event) => {
|
|
85
|
-
if (event.key === 'Escape') {
|
|
86
|
-
event.preventDefault()
|
|
87
|
-
deny(false)
|
|
88
|
-
}
|
|
89
|
-
}}>
|
|
90
|
-
<div className='flex items-baseline gap-2'>
|
|
91
|
-
<LineGlyph className='text-warning'>!</LineGlyph>
|
|
92
|
-
<span className='min-w-0 flex-1 text-body-sm leading-5 font-medium text-fg-1'>
|
|
93
|
-
{heading}
|
|
94
|
-
</span>
|
|
95
|
-
</div>
|
|
96
|
-
{request.description ? (
|
|
97
|
-
<div className={cn(LINE_INDENT, 'text-label leading-5 text-fg-3')}>
|
|
98
|
-
{request.description}
|
|
99
|
-
</div>
|
|
100
|
-
) : null}
|
|
101
|
-
{request.decisionReason ? (
|
|
102
|
-
<div className={cn(LINE_INDENT, 'text-label leading-5 text-fg-4')}>
|
|
103
|
-
{request.decisionReason}
|
|
104
|
-
</div>
|
|
105
|
-
) : null}
|
|
106
|
-
<button
|
|
107
|
-
type='button'
|
|
108
|
-
onClick={() => setShowInput((v) => !v)}
|
|
109
|
-
className='flex w-full items-baseline gap-2 text-left outline-none hover:bg-surface-hover/60'>
|
|
110
|
-
<LineGlyph className='text-fg-4'>⎿</LineGlyph>
|
|
111
|
-
<span className='min-w-0 flex-1 truncate text-label leading-5 text-fg-4'>
|
|
112
|
-
<span className='font-medium text-fg-2'>{request.toolName}</span>
|
|
113
|
-
{summary ? `(${summary})` : null}
|
|
114
|
-
</span>
|
|
115
|
-
</button>
|
|
116
|
-
{showInput ? (
|
|
117
|
-
<div className={LINE_INDENT}>
|
|
118
|
-
<LinePayload code={JSON.stringify(request.input, null, 2)} language='json' label='Input' />
|
|
119
|
-
</div>
|
|
120
|
-
) : null}
|
|
121
|
-
<LineOptionList
|
|
122
|
-
label={heading}
|
|
123
|
-
options={options}
|
|
124
|
-
focused={focused}
|
|
125
|
-
onFocus={setFocused}
|
|
126
|
-
active={!denying}
|
|
127
|
-
onChoose={(index) => {
|
|
128
|
-
if (index === 0) onApprove(request.id)
|
|
129
|
-
else if (index === 1) setDenying(true)
|
|
130
|
-
else deny(true)
|
|
131
|
-
}}
|
|
132
|
-
/>
|
|
133
|
-
<LineHint>↑↓ move · 1–3 choose · esc deny</LineHint>
|
|
134
|
-
</div>
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
45
|
|
|
138
46
|
return (
|
|
139
47
|
<div
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ProjectIcon as ProjectIconSpec } from '@workerdeck/protocol'
|
|
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,
|
|
14
|
+
} from 'lucide-react'
|
|
15
|
+
import type { ComponentType } from 'react'
|
|
16
|
+
import { cn } from '../../lib/utils.ts'
|
|
17
|
+
|
|
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
|
+
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,
|
|
85
|
+
zap: Zap,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function ProjectIcon({
|
|
89
|
+
icon,
|
|
90
|
+
src,
|
|
91
|
+
name,
|
|
92
|
+
className,
|
|
93
|
+
}: {
|
|
94
|
+
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
|
+
src?: string
|
|
98
|
+
/** The project's name, for the alt text. */
|
|
99
|
+
name?: string
|
|
100
|
+
className?: string
|
|
101
|
+
}) {
|
|
102
|
+
if (!icon) return null
|
|
103
|
+
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
|
+
)
|
|
116
|
+
}
|
|
117
|
+
const Glyph = GLYPHS[icon.name] ?? Folder
|
|
118
|
+
return <Glyph className={cn('inline-block size-3 shrink-0', className)} />
|
|
119
|
+
}
|
|
@@ -10,8 +10,6 @@ import { Badge } from '../ui/Badge.tsx'
|
|
|
10
10
|
import { Button } from '../ui/Button.tsx'
|
|
11
11
|
import { Input } from '../ui/Input.tsx'
|
|
12
12
|
import { cn } from '../../lib/utils.ts'
|
|
13
|
-
import { LINE_INDENT, LineHint, LineInput, LineOptionList, LinePayload } from './line-prompt.tsx'
|
|
14
|
-
import { LineGlyph, useLines } from './transcript-variant.tsx'
|
|
15
13
|
|
|
16
14
|
export type QuestionBehaviorMeta = {
|
|
17
15
|
value: QuestionBehavior
|
|
@@ -49,9 +47,6 @@ export function parseUserQuestions(input: Record<string, unknown>): UserQuestion
|
|
|
49
47
|
|
|
50
48
|
type Selection = { labels: string[]; other: string; otherActive: boolean }
|
|
51
49
|
|
|
52
|
-
/** Whether the key hint should say "toggle" rather than "choose". */
|
|
53
|
-
const multiSelectAnywhere = (questions: UserQuestion[]) => questions.some((q) => q.multiSelect === true)
|
|
54
|
-
|
|
55
50
|
const EMPTY_SELECTION: Selection = { labels: [], other: '', otherActive: false }
|
|
56
51
|
|
|
57
52
|
/** A question's answer string: chosen label(s) (multi-select comma-joined), with any
|
|
@@ -77,16 +72,10 @@ export interface QuestionPromptProps {
|
|
|
77
72
|
* and the focused option's preview. Falls back to nothing renderable → the caller
|
|
78
73
|
* should show a generic PermissionPrompt if `parseUserQuestions` finds no questions. */
|
|
79
74
|
export function QuestionPrompt({ request, onAnswer, onDismiss, className }: QuestionPromptProps) {
|
|
80
|
-
const lines = useLines()
|
|
81
75
|
const questions = parseUserQuestions(request.input)
|
|
82
76
|
const [selections, setSelections] = useState<Selection[]>(() =>
|
|
83
77
|
questions.map(() => EMPTY_SELECTION),
|
|
84
78
|
)
|
|
85
|
-
// `lines` only: the roving cursor per question, and which question's list owns
|
|
86
|
-
// the DOM focus — two lists both chasing their own index would tear the caret
|
|
87
|
-
// between them on every render.
|
|
88
|
-
const [cursors, setCursors] = useState<number[]>(() => questions.map(() => 0))
|
|
89
|
-
const [activeQuestion, setActiveQuestion] = useState(0)
|
|
90
79
|
|
|
91
80
|
const update = (index: number, patch: Partial<Selection>) => {
|
|
92
81
|
setSelections((prev) => prev.map((s, i) => (i === index ? { ...s, ...patch } : s)))
|
|
@@ -115,117 +104,6 @@ export function QuestionPrompt({ request, onAnswer, onDismiss, className }: Ques
|
|
|
115
104
|
onAnswer(request.id, { ...request.input, answers })
|
|
116
105
|
}
|
|
117
106
|
|
|
118
|
-
if (lines) {
|
|
119
|
-
const dismiss = () => onDismiss(request.id, 'Question dismissed by user')
|
|
120
|
-
return (
|
|
121
|
-
<div
|
|
122
|
-
data-slot='question-prompt'
|
|
123
|
-
className={cn('flex w-full flex-col gap-2', className)}
|
|
124
|
-
onKeyDown={(event) => {
|
|
125
|
-
if (event.key === 'Escape') {
|
|
126
|
-
event.preventDefault()
|
|
127
|
-
dismiss()
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
// Enter belongs to the focused row (it is a button), so committing the
|
|
131
|
-
// whole form needs the modifier — the composer's own convention.
|
|
132
|
-
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey) && complete) {
|
|
133
|
-
event.preventDefault()
|
|
134
|
-
submit()
|
|
135
|
-
}
|
|
136
|
-
}}>
|
|
137
|
-
{questions.map((q, index) => {
|
|
138
|
-
const selection = selections[index] ?? EMPTY_SELECTION
|
|
139
|
-
const cursor = cursors[index] ?? 0
|
|
140
|
-
const multiSelect = q.multiSelect === true
|
|
141
|
-
const options = [
|
|
142
|
-
...q.options.map((option, optionIndex) => {
|
|
143
|
-
const selected = selection.labels.includes(option.label)
|
|
144
|
-
return {
|
|
145
|
-
key: option.label,
|
|
146
|
-
label: option.label,
|
|
147
|
-
description: option.description,
|
|
148
|
-
// A preview shows on focus, not only on selection: in a list you
|
|
149
|
-
// walk with the arrow keys, "what does this one look like" is the
|
|
150
|
-
// question the cursor is asking.
|
|
151
|
-
detail:
|
|
152
|
-
option.preview && (selected || cursor === optionIndex) ? (
|
|
153
|
-
<LinePayload code={option.preview} label='Preview' />
|
|
154
|
-
) : undefined,
|
|
155
|
-
// Both kinds draw their state: without a marker a chosen
|
|
156
|
-
// one-of is invisible the moment the cursor moves off it.
|
|
157
|
-
checked: selected,
|
|
158
|
-
marker: multiSelect ? ('check' as const) : ('radio' as const),
|
|
159
|
-
}
|
|
160
|
-
}),
|
|
161
|
-
{
|
|
162
|
-
key: '__other',
|
|
163
|
-
label: 'Other…',
|
|
164
|
-
checked: selection.otherActive,
|
|
165
|
-
marker: multiSelect ? ('check' as const) : ('radio' as const),
|
|
166
|
-
detail: selection.otherActive ? (
|
|
167
|
-
<LineInput
|
|
168
|
-
value={selection.other}
|
|
169
|
-
onChange={(value) => update(index, { other: value })}
|
|
170
|
-
onSubmit={() => {
|
|
171
|
-
// The typed text is already in state, so `complete` is
|
|
172
|
-
// current: Enter finishes the form when this was the last
|
|
173
|
-
// question outstanding, and is a no-op when it wasn't.
|
|
174
|
-
if (complete) submit()
|
|
175
|
-
}}
|
|
176
|
-
onCancel={() => update(index, { otherActive: false, other: '' })}
|
|
177
|
-
placeholder='Type your own answer'
|
|
178
|
-
/>
|
|
179
|
-
) : undefined,
|
|
180
|
-
},
|
|
181
|
-
]
|
|
182
|
-
return (
|
|
183
|
-
<div key={index} className='flex flex-col'>
|
|
184
|
-
<div className='flex items-baseline gap-2'>
|
|
185
|
-
<LineGlyph className='text-fg-3'>?</LineGlyph>
|
|
186
|
-
<span className='min-w-0 flex-1 text-body-sm leading-5 text-fg-1'>
|
|
187
|
-
{q.header ? <span className='text-fg-4'>{q.header} · </span> : null}
|
|
188
|
-
<span className='font-medium'>{q.question}</span>
|
|
189
|
-
</span>
|
|
190
|
-
</div>
|
|
191
|
-
<LineOptionList
|
|
192
|
-
label={q.question}
|
|
193
|
-
options={options}
|
|
194
|
-
focused={cursor}
|
|
195
|
-
active={activeQuestion === index && !selection.otherActive}
|
|
196
|
-
onFocus={(next) => {
|
|
197
|
-
setActiveQuestion(index)
|
|
198
|
-
setCursors((prev) => prev.map((c, i) => (i === index ? next : c)))
|
|
199
|
-
}}
|
|
200
|
-
onChoose={(next) => {
|
|
201
|
-
if (next === q.options.length) {
|
|
202
|
-
update(index, { otherActive: !selection.otherActive })
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
const option = q.options[next]
|
|
206
|
-
if (option) toggle(index, option.label, multiSelect)
|
|
207
|
-
}}
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
210
|
-
)
|
|
211
|
-
})}
|
|
212
|
-
<div className={cn(LINE_INDENT, 'flex flex-col')}>
|
|
213
|
-
<button
|
|
214
|
-
type='button'
|
|
215
|
-
disabled={!complete}
|
|
216
|
-
onClick={submit}
|
|
217
|
-
className='w-fit text-body-sm leading-5 text-accent underline-offset-2 outline-none hover:underline disabled:text-fg-4 disabled:no-underline'>
|
|
218
|
-
⏎ Answer
|
|
219
|
-
</button>
|
|
220
|
-
</div>
|
|
221
|
-
<LineHint>
|
|
222
|
-
↑↓ move · {multiSelectAnywhere(questions) ? 'space/1–9 toggle' : '1–9 choose'} · ⌘⏎ answer
|
|
223
|
-
· esc dismiss
|
|
224
|
-
</LineHint>
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
107
|
return (
|
|
230
108
|
<div
|
|
231
109
|
data-slot='question-prompt'
|
|
@@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import { Brain, ChevronDown } from 'lucide-react'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
import { Response } from './Response.tsx'
|
|
5
|
-
import { LineGlyph, useLines } from './transcript-variant.tsx'
|
|
6
5
|
|
|
7
6
|
export interface ReasoningProps {
|
|
8
7
|
children: string
|
|
@@ -15,7 +14,6 @@ export interface ReasoningProps {
|
|
|
15
14
|
/** Collapsible extended-thinking block: open while the model is thinking, tucks itself
|
|
16
15
|
* away once the thought is finished (unless the user toggled it manually). */
|
|
17
16
|
export function Reasoning({ children, isStreaming = false, defaultOpen, className }: ReasoningProps) {
|
|
18
|
-
const lines = useLines()
|
|
19
17
|
const [open, setOpen] = useState(defaultOpen ?? isStreaming)
|
|
20
18
|
const userToggled = useRef(false)
|
|
21
19
|
|
|
@@ -43,28 +41,16 @@ export function Reasoning({ children, isStreaming = false, defaultOpen, classNam
|
|
|
43
41
|
userToggled.current = true
|
|
44
42
|
setOpen((v) => !v)
|
|
45
43
|
}}
|
|
46
|
-
className=
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{lines ? (
|
|
51
|
-
<LineGlyph className='text-fg-4'>✻</LineGlyph>
|
|
52
|
-
) : (
|
|
53
|
-
<Brain className='size-3.5' />
|
|
54
|
-
)}
|
|
55
|
-
<span className={cn(lines && 'leading-5')}>
|
|
56
|
-
{isStreaming ? 'Thinking…' : 'Thought process'}
|
|
57
|
-
</span>
|
|
58
|
-
{hasText && !lines ? (
|
|
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' />
|
|
46
|
+
<span>{isStreaming ? 'Thinking…' : 'Thought process'}</span>
|
|
47
|
+
{hasText ? (
|
|
59
48
|
<ChevronDown className={cn('size-3.5 transition-transform', open && 'rotate-180')} />
|
|
60
49
|
) : null}
|
|
61
50
|
</button>
|
|
62
51
|
{open && hasText ? (
|
|
63
52
|
<div
|
|
64
|
-
className=
|
|
65
|
-
'border-l border-border text-body-sm text-fg-3 [&_*]:text-fg-3',
|
|
66
|
-
lines ? 'ml-[0.4375rem] mt-0.5 pl-2 leading-5' : 'mt-2 border-l-2 pl-3',
|
|
67
|
-
)}>
|
|
53
|
+
className='mt-2 border-l-2 border-border pl-3 text-body-sm text-fg-3 [&_*]:text-fg-3'>
|
|
68
54
|
<Response streaming={isStreaming}>{children}</Response>
|
|
69
55
|
</div>
|
|
70
56
|
) : null}
|