@workerdeck/ui 0.13.0 → 0.16.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 +72 -0
- package/build/{SessionPanel-CZMA44NM.d.mts → SessionPanel-B9CHoq8x.d.mts} +213 -27
- package/build/{SessionPanel-CKQa4i0Y.mjs → SessionPanel-DII9MmQ8.mjs} +5192 -2542
- package/build/SessionPanel-DII9MmQ8.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 +494 -45
- package/build/index.mjs +182 -24
- 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 +13 -1
- package/build/workspace.mjs +111 -5
- package/build/workspace.mjs.map +1 -1
- package/package.json +14 -7
- package/src/components/agent/Composer.tsx +251 -84
- 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 -43
- package/src/components/agent/PermissionPrompt.tsx +0 -92
- 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 +35 -25
- package/src/components/agent/SessionPanel.tsx +312 -63
- package/src/components/agent/SessionWorkspace.tsx +36 -0
- package/src/components/agent/StatusBar.tsx +70 -15
- package/src/components/agent/ToolCallCard.tsx +17 -111
- package/src/components/agent/Transcript.tsx +710 -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/transcript-rows.ts +82 -0
- package/src/components/agent/transcript-variant.tsx +43 -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 +147 -0
- package/src/components/terminal/affordances.tsx +118 -0
- package/src/components/terminal/diff.tsx +130 -0
- package/src/components/terminal/height.ts +727 -0
- package/src/components/terminal/items.tsx +449 -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 +72 -0
- package/src/components/terminal/row.tsx +132 -0
- package/src/components/terminal/scrubber.tsx +663 -0
- package/src/components/terminal/surface.tsx +80 -0
- package/src/components/terminal/tool-run.ts +91 -0
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/Empty.tsx +56 -0
- package/src/components/ui/Splitter.tsx +14 -0
- package/src/index.ts +36 -0
- package/src/lib/status.ts +59 -3
- package/src/lib/tool-icon.ts +14 -0
- package/src/styles/terminal.css +1011 -0
- package/src/styles/theme.css +73 -0
- package/build/SessionPanel-CKQa4i0Y.mjs.map +0 -1
- package/build/format.mjs.map +0 -1
- package/src/components/agent/line-prompt.tsx +0 -249
|
@@ -1,92 +1,42 @@
|
|
|
1
1
|
import { cn } from '../../lib/utils.ts'
|
|
2
|
-
import { formatDuration, formatTokens } from '../../lib/format.ts'
|
|
3
|
-
import { usePulse } from './pulse.tsx'
|
|
4
|
-
import { LineGlyph, useLines } from './transcript-variant.tsx'
|
|
5
2
|
|
|
6
3
|
export interface LoaderProps {
|
|
7
4
|
/** Overrides the cycling verb — for a state that has one true name
|
|
8
5
|
* ("Starting session…"). */
|
|
9
6
|
label?: string
|
|
10
|
-
/** When the current run began, for the elapsed clock. Absent = no clock.
|
|
7
|
+
/** When the current run began, for the elapsed clock. Absent = no clock.
|
|
8
|
+
* Unused by the card treatment; kept so the `cards` and `terminal` loaders
|
|
9
|
+
* take the same call site in `Transcript`. */
|
|
11
10
|
startedAt?: number
|
|
12
|
-
/** Context tokens in play
|
|
11
|
+
/** Context tokens in play. Same story as `startedAt`. */
|
|
13
12
|
tokens?: number
|
|
14
13
|
className?: string
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
* What it says it is doing while it hasn't said anything yet. Cycled on a slow
|
|
19
|
-
* clock so a long turn doesn't sit under one frozen word — a still label reads
|
|
20
|
-
* as a hung process, which is exactly what this is meant to disprove.
|
|
21
|
-
*/
|
|
22
|
-
const VERBS = [
|
|
23
|
-
'Working',
|
|
24
|
-
'Thinking',
|
|
25
|
-
'Churning',
|
|
26
|
-
'Pondering',
|
|
27
|
-
'Whirring',
|
|
28
|
-
'Computing',
|
|
29
|
-
'Percolating',
|
|
30
|
-
'Tinkering',
|
|
31
|
-
'Deliberating',
|
|
32
|
-
'Simmering',
|
|
33
|
-
'Crunching',
|
|
34
|
-
'Noodling',
|
|
35
|
-
]
|
|
36
|
-
const VERB_MS = 4000
|
|
37
|
-
|
|
38
16
|
/**
|
|
39
17
|
* "The agent is working and hasn't produced output yet."
|
|
40
18
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
19
|
+
* Three dots pulsing in CSS — the chat convention, for the `cards` variant. The
|
|
20
|
+
* terminal theme has its own working line (`terminal/items.tsx`'s `WorkingRow`,
|
|
21
|
+
* with the mark's pulse in the gutter and the run's readings beside the verb),
|
|
22
|
+
* because there it is a *row of the transcript* rather than a spinner floating
|
|
23
|
+
* over one.
|
|
45
24
|
*/
|
|
46
|
-
export function Loader({ label,
|
|
47
|
-
const lines = useLines()
|
|
48
|
-
// Only the line variant animates a glyph; cards pulse three dots in CSS.
|
|
49
|
-
const pulse = usePulse(lines)
|
|
50
|
-
|
|
51
|
-
if (!lines) {
|
|
52
|
-
return (
|
|
53
|
-
<div
|
|
54
|
-
data-slot='loader'
|
|
55
|
-
className={cn('flex items-center gap-2 py-1 text-body-sm text-fg-4', className)}>
|
|
56
|
-
<span className='flex items-center gap-1'>
|
|
57
|
-
{[0, 1, 2].map((i) => (
|
|
58
|
-
<span
|
|
59
|
-
key={i}
|
|
60
|
-
className='size-1.5 animate-pulse rounded-full bg-fg-4'
|
|
61
|
-
style={{ animationDelay: `${i * 160}ms` }}
|
|
62
|
-
/>
|
|
63
|
-
))}
|
|
64
|
-
</span>
|
|
65
|
-
{label ? <span>{label}</span> : null}
|
|
66
|
-
</div>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Derived from the clock rather than kept in state: the frame tick is already
|
|
71
|
-
// re-rendering, so the seconds and the verb come along for free.
|
|
72
|
-
const elapsed = startedAt === undefined ? undefined : Date.now() - startedAt
|
|
73
|
-
const verb =
|
|
74
|
-
label ?? VERBS[Math.floor((elapsed ?? 0) / VERB_MS) % VERBS.length] ?? VERBS[0]
|
|
75
|
-
const readings = [
|
|
76
|
-
elapsed !== undefined ? formatDuration(elapsed) : undefined,
|
|
77
|
-
tokens !== undefined ? `↓ ${formatTokens(tokens)}` : undefined,
|
|
78
|
-
].filter(Boolean)
|
|
79
|
-
|
|
25
|
+
export function Loader({ label, className }: LoaderProps) {
|
|
80
26
|
return (
|
|
81
|
-
<div
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
27
|
+
<div
|
|
28
|
+
data-slot='loader'
|
|
29
|
+
className={cn('flex items-center gap-2 py-1 text-body-sm text-fg-4', className)}>
|
|
30
|
+
<span className='flex items-center gap-1'>
|
|
31
|
+
{[0, 1, 2].map((i) => (
|
|
32
|
+
<span
|
|
33
|
+
key={i}
|
|
34
|
+
className='size-1.5 animate-pulse rounded-full bg-fg-4'
|
|
35
|
+
style={{ animationDelay: `${i * 160}ms` }}
|
|
36
|
+
/>
|
|
37
|
+
))}
|
|
89
38
|
</span>
|
|
39
|
+
{label ? <span>{label}</span> : null}
|
|
90
40
|
</div>
|
|
91
41
|
)
|
|
92
42
|
}
|
|
@@ -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,64 +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
|
-
<LineGlyph className={from === 'user' ? 'text-fg-2' : 'text-fg-3'}>
|
|
48
|
-
{from === 'user' ? '❯' : '●'}
|
|
49
|
-
</LineGlyph>
|
|
50
|
-
<div className='flex min-w-0 flex-1 flex-col gap-1'>{children}</div>
|
|
51
|
-
</>
|
|
52
|
-
) : (
|
|
53
|
-
children
|
|
54
|
-
)}
|
|
28
|
+
{children}
|
|
55
29
|
</div>
|
|
56
30
|
)
|
|
57
31
|
}
|
|
58
32
|
|
|
59
33
|
export function MessageContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
60
|
-
const lines = useLines()
|
|
61
34
|
return (
|
|
62
35
|
<div
|
|
63
36
|
data-slot='message-content'
|
|
64
37
|
className={cn(
|
|
65
|
-
'min-w-0',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'in-data-[from=user]:max-w-[85%] in-data-[from=user]:rounded-lg in-data-[from=user]:rounded-bl-sm',
|
|
73
|
-
'in-data-[from=user]:bg-accent-bg in-data-[from=user]:px-3 in-data-[from=user]:py-2',
|
|
74
|
-
'in-data-[from=user]:whitespace-pre-wrap',
|
|
75
|
-
'in-data-[from=assistant]:w-full',
|
|
76
|
-
),
|
|
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',
|
|
77
45
|
className,
|
|
78
46
|
)}
|
|
79
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
|
|
@@ -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}
|