@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.
Files changed (69) hide show
  1. package/README.md +72 -0
  2. package/build/{SessionPanel-CZMA44NM.d.mts → SessionPanel-B9CHoq8x.d.mts} +213 -27
  3. package/build/{SessionPanel-CKQa4i0Y.mjs → SessionPanel-DII9MmQ8.mjs} +5192 -2542
  4. package/build/SessionPanel-DII9MmQ8.mjs.map +1 -0
  5. package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
  6. package/build/format.d.mts +39 -4
  7. package/build/format.mjs +2 -118
  8. package/build/index.d.mts +494 -45
  9. package/build/index.mjs +182 -24
  10. package/build/index.mjs.map +1 -1
  11. package/build/status-Ydzi7n6j.mjs +143 -0
  12. package/build/status-Ydzi7n6j.mjs.map +1 -0
  13. package/build/workspace.d.mts +13 -1
  14. package/build/workspace.mjs +111 -5
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +14 -7
  17. package/src/components/agent/Composer.tsx +251 -84
  18. package/src/components/agent/Conversation.tsx +12 -12
  19. package/src/components/agent/FileCard.tsx +0 -26
  20. package/src/components/agent/FileTree.tsx +9 -8
  21. package/src/components/agent/Loader.tsx +22 -72
  22. package/src/components/agent/Message.tsx +11 -43
  23. package/src/components/agent/PermissionPrompt.tsx +0 -92
  24. package/src/components/agent/QuestionPrompt.tsx +0 -122
  25. package/src/components/agent/Reasoning.tsx +5 -19
  26. package/src/components/agent/Response.tsx +1 -132
  27. package/src/components/agent/SessionBrowser.tsx +35 -25
  28. package/src/components/agent/SessionPanel.tsx +312 -63
  29. package/src/components/agent/SessionWorkspace.tsx +36 -0
  30. package/src/components/agent/StatusBar.tsx +70 -15
  31. package/src/components/agent/ToolCallCard.tsx +17 -111
  32. package/src/components/agent/Transcript.tsx +710 -203
  33. package/src/components/agent/UsageDialog.tsx +20 -106
  34. package/src/components/agent/UsageMeters.tsx +133 -0
  35. package/src/components/agent/pulse.tsx +3 -2
  36. package/src/components/agent/transcript-rows.ts +82 -0
  37. package/src/components/agent/transcript-variant.tsx +43 -51
  38. package/src/components/agent/use-height-epoch.ts +60 -0
  39. package/src/components/agent/use-path-links.ts +147 -0
  40. package/src/components/agent/use-transcript-jumps.ts +190 -0
  41. package/src/components/prompt-area/cursor-helpers.ts +65 -0
  42. package/src/components/prompt-area/use-prompt-area.ts +16 -10
  43. package/src/components/terminal/PermissionPrompt.tsx +119 -0
  44. package/src/components/terminal/QuestionPrompt.tsx +322 -0
  45. package/src/components/terminal/StatusLine.tsx +159 -0
  46. package/src/components/terminal/TerminalTranscript.tsx +147 -0
  47. package/src/components/terminal/affordances.tsx +118 -0
  48. package/src/components/terminal/diff.tsx +130 -0
  49. package/src/components/terminal/height.ts +727 -0
  50. package/src/components/terminal/items.tsx +449 -0
  51. package/src/components/terminal/markdown.tsx +191 -0
  52. package/src/components/terminal/press.tsx +120 -0
  53. package/src/components/terminal/prompt.tsx +343 -0
  54. package/src/components/terminal/result-preview.ts +72 -0
  55. package/src/components/terminal/row.tsx +132 -0
  56. package/src/components/terminal/scrubber.tsx +663 -0
  57. package/src/components/terminal/surface.tsx +80 -0
  58. package/src/components/terminal/tool-run.ts +91 -0
  59. package/src/components/ui/Badge.tsx +6 -1
  60. package/src/components/ui/Empty.tsx +56 -0
  61. package/src/components/ui/Splitter.tsx +14 -0
  62. package/src/index.ts +36 -0
  63. package/src/lib/status.ts +59 -3
  64. package/src/lib/tool-icon.ts +14 -0
  65. package/src/styles/terminal.css +1011 -0
  66. package/src/styles/theme.css +73 -0
  67. package/build/SessionPanel-CKQa4i0Y.mjs.map +0 -1
  68. package/build/format.mjs.map +0 -1
  69. 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, shown beside the clock. */
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
- * `lines`: a terminal working line — the mark's own pulse in the gutter (see
42
- * `pulse.tsx`), a verb, and the readings that answer "should I still be
43
- * waiting?" in one parenthesis. `cards`: the three-dot pulse, unchanged the
44
- * dashboard's loader is not a gutter glyph and has no column to pulse in.
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, startedAt, tokens, className }: LoaderProps) {
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 data-slot='loader' className={cn('flex items-baseline gap-2', className)}>
82
- {/* `lines` is monochrome: the terminal treatment earns its calm by
83
- spending no colour on decoration, and a pulsing glyph is already the
84
- loudest thing on a still page. `cards` keeps the brand accent. */}
85
- <LineGlyph className={lines ? 'text-fg-3' : 'text-accent'}>{pulse}</LineGlyph>
86
- <span className='min-w-0 flex-1 text-body-sm leading-5 text-fg-3'>
87
- {verb}…{' '}
88
- {readings.length ? <span className='text-label text-fg-4'>({readings.join(' · ')})</span> : null}
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
- * `lines`: both are left-aligned full-width line items behind a gutter glyph
20
- * `❯` for what was typed, `●` for what the model said. No bubble: a prompt is
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
- {lines ? (
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
- lines
67
- ? cn('w-full text-fg-1', LINE_TEXT, 'in-data-[from=user]:whitespace-pre-wrap')
68
- : cn(
69
- 'text-body-sm leading-6 text-fg-1',
70
- // Bubble treatment only within a user message row.
71
- // The tail points bottom-*left* now that the bubble is left-aligned.
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={cn(
47
- 'flex items-baseline text-label text-fg-3 transition-colors outline-none disabled:cursor-default hover:text-fg-1 disabled:hover:text-fg-3',
48
- lines ? 'w-full gap-2' : 'items-center gap-1.5',
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={cn(
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}