@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,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useImperativeHandle,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type DragEvent,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type Ref,
|
|
9
|
+
} from 'react'
|
|
2
10
|
import type { SkillInfo, SlashCommandInfo } from '@workerdeck/protocol'
|
|
3
11
|
import type { StagedAttachment, UseAttachmentsResult } from '@workerdeck/react'
|
|
4
12
|
import {
|
|
@@ -16,7 +24,10 @@ import { Spinner } from '../ui/Spinner.tsx'
|
|
|
16
24
|
import { PromptArea } from '../prompt-area/prompt-area.tsx'
|
|
17
25
|
import { usePromptAreaState } from '../prompt-area/use-prompt-area-state.ts'
|
|
18
26
|
import { commandTrigger, mentionTrigger } from '../prompt-area/trigger-presets.ts'
|
|
19
|
-
import {
|
|
27
|
+
import { useTranscriptVariant } from './transcript-variant.tsx'
|
|
28
|
+
import type { TerminalAffordances } from '../terminal/affordances.tsx'
|
|
29
|
+
import { PROMPT_GLYPH } from '../terminal/items.tsx'
|
|
30
|
+
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
20
31
|
import type { TriggerSuggestion } from '../prompt-area/types.ts'
|
|
21
32
|
import { cn } from '../../lib/utils.ts'
|
|
22
33
|
import { formatBytes } from '../../lib/format.ts'
|
|
@@ -69,6 +80,19 @@ export interface ComposerProps {
|
|
|
69
80
|
* empty composer would otherwise spend two rows saying nothing.
|
|
70
81
|
*/
|
|
71
82
|
layout?: 'stacked' | 'inline'
|
|
83
|
+
/**
|
|
84
|
+
* Terminal theme only: the cell the composer draws on. Passed straight
|
|
85
|
+
* through to its own {@link TerminalSurface}, and it has to be passed — the
|
|
86
|
+
* composer sits outside the transcript's scroller, so it establishes a second
|
|
87
|
+
* surface, and a surface handed no metrics falls back to the 13/18 default. A
|
|
88
|
+
* host running the transcript at its editor's size and the composer at the
|
|
89
|
+
* default would have the caret land on a different column from the text above
|
|
90
|
+
* it, which is the one thing this theme exists to prevent.
|
|
91
|
+
*/
|
|
92
|
+
fontSize?: number
|
|
93
|
+
lineHeight?: number
|
|
94
|
+
/** Terminal theme only; see {@link TerminalAffordances}. */
|
|
95
|
+
affordances?: TerminalAffordances | boolean
|
|
72
96
|
className?: string
|
|
73
97
|
ref?: Ref<ComposerHandle>
|
|
74
98
|
}
|
|
@@ -137,13 +161,16 @@ export function Composer({
|
|
|
137
161
|
attachments,
|
|
138
162
|
toolbar,
|
|
139
163
|
layout = 'stacked',
|
|
164
|
+
fontSize,
|
|
165
|
+
lineHeight,
|
|
166
|
+
affordances,
|
|
140
167
|
className,
|
|
141
168
|
ref,
|
|
142
169
|
}: ComposerProps) {
|
|
143
170
|
const inline = layout === 'inline'
|
|
144
171
|
// The transcript's variant reaches here through the panel-wide context, so
|
|
145
172
|
// the composer matches the rows above it without a prop chain.
|
|
146
|
-
const
|
|
173
|
+
const terminal = useTranscriptVariant() === 'terminal'
|
|
147
174
|
const { bind, plainText, isEmpty, clear, focus } = usePromptAreaState()
|
|
148
175
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
149
176
|
const [dragging, setDragging] = useState(false)
|
|
@@ -314,36 +341,69 @@ export function Composer({
|
|
|
314
341
|
const attach = canAttach ? (
|
|
315
342
|
<>
|
|
316
343
|
{fileField}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
variant='ghost'
|
|
326
|
-
size='icon-sm'
|
|
327
|
-
aria-label='Attach files'
|
|
328
|
-
disabled={disabled}
|
|
329
|
-
onClick={() => fileInput.current?.click()}>
|
|
330
|
-
<Paperclip className='size-4' />
|
|
331
|
-
</Button>
|
|
332
|
-
)}
|
|
344
|
+
<Button
|
|
345
|
+
variant='ghost'
|
|
346
|
+
size='icon-sm'
|
|
347
|
+
aria-label='Attach files'
|
|
348
|
+
disabled={disabled}
|
|
349
|
+
onClick={() => fileInput.current?.click()}>
|
|
350
|
+
<Paperclip className='size-4' />
|
|
351
|
+
</Button>
|
|
333
352
|
</>
|
|
334
353
|
) : null
|
|
335
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The terminal composer's **gutter cell** — the one column every transcript
|
|
357
|
+
* row's marker sits in, so whatever stands here cannot move the text beside
|
|
358
|
+
* it. It holds one of three things, in this order:
|
|
359
|
+
*
|
|
360
|
+
* `✕` **while the session is working**, because the gutter is where the eye
|
|
361
|
+
* already is (it is the column the pulse and every marker share) and stop is
|
|
362
|
+
* the only action that matters mid-run. Note the condition is `busy` alone,
|
|
363
|
+
* not the old `busy && !canSend`: with send living on the other side of the
|
|
364
|
+
* field there is no longer a slot to compete for, so typing a follow-up while
|
|
365
|
+
* a turn runs no longer hides the way to stop it.
|
|
366
|
+
*
|
|
367
|
+
* A cross rather than the `■` this started as: the square reads as a *state*
|
|
368
|
+
* ("stopped") in a column where `●` and `◆` really are states, so it looked
|
|
369
|
+
* like a status marker rather than something to press. A cross reads as an
|
|
370
|
+
* action and collides with nothing else on the column. It is also one of the
|
|
371
|
+
* few candidates that measures exactly 1ch in JetBrains Mono — `⏹`, `⏸` and
|
|
372
|
+
* `⏻`, the obvious picks, are 1.05–1.31 cells and would break the grid.
|
|
373
|
+
*
|
|
374
|
+
* `+` **otherwise**, when there is anything to attach — the composer's own
|
|
375
|
+
* affordance, in the composer's own gutter.
|
|
376
|
+
*
|
|
377
|
+
* `❯` when neither applies, so the column is never empty and the typed line
|
|
378
|
+
* never shifts. Blue, not the brand's coral: coral is the *working* mark, and
|
|
379
|
+
* a prompt waiting for you is not the session working.
|
|
380
|
+
*/
|
|
381
|
+
const gutter = busy ? (
|
|
382
|
+
<GlyphButton gutter label='Interrupt' tone='yellow' onClick={onInterrupt}>
|
|
383
|
+
✕
|
|
384
|
+
</GlyphButton>
|
|
385
|
+
) : canAttach ? (
|
|
386
|
+
<GlyphButton
|
|
387
|
+
gutter
|
|
388
|
+
label='Attach files'
|
|
389
|
+
disabled={disabled}
|
|
390
|
+
onClick={() => fileInput.current?.click()}>
|
|
391
|
+
+
|
|
392
|
+
</GlyphButton>
|
|
393
|
+
) : (
|
|
394
|
+
<span aria-hidden className='term-gutter' data-tone='blue'>
|
|
395
|
+
{PROMPT_GLYPH}
|
|
396
|
+
</span>
|
|
397
|
+
)
|
|
398
|
+
|
|
336
399
|
const interrupting = busy && !canSend
|
|
337
|
-
const submitButton =
|
|
400
|
+
const submitButton = terminal ? (
|
|
338
401
|
// Terminal furniture rather than chat furniture: a glyph that lights up on
|
|
339
|
-
// hover/focus instead of a filled pill. `↵` is
|
|
340
|
-
// — the
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
onClick={interrupting ? onInterrupt : submit}
|
|
345
|
-
className={interrupting ? 'text-warning' : canSend ? 'text-accent' : undefined}>
|
|
346
|
-
{interrupting ? '■' : '↵'}
|
|
402
|
+
// hover/focus instead of a filled pill. `↵` is the only thing on this side
|
|
403
|
+
// now — stop moved to the gutter — so it means one thing at all times, and
|
|
404
|
+
// a reader never has to check which symbol is currently under their cursor.
|
|
405
|
+
<GlyphButton label='Send' disabled={!canSend} onClick={submit} tone={canSend ? 'blue' : undefined}>
|
|
406
|
+
↵
|
|
347
407
|
</GlyphButton>
|
|
348
408
|
) : interrupting ? (
|
|
349
409
|
<Button
|
|
@@ -365,36 +425,119 @@ export function Composer({
|
|
|
365
425
|
</Button>
|
|
366
426
|
)
|
|
367
427
|
|
|
428
|
+
const dropHandlers = {
|
|
429
|
+
onDragOver: (e: DragEvent) => {
|
|
430
|
+
if (attachments && !attachments.disabled) {
|
|
431
|
+
e.preventDefault()
|
|
432
|
+
setDragging(true)
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
onDragLeave: () => setDragging(false),
|
|
436
|
+
onDrop: (e: DragEvent) => {
|
|
437
|
+
if (!attachments || attachments.disabled) return
|
|
438
|
+
e.preventDefault()
|
|
439
|
+
setDragging(false)
|
|
440
|
+
pick(e.dataTransfer.files)
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const errorRow = attachments?.error ? (
|
|
445
|
+
<div
|
|
446
|
+
className={cn(
|
|
447
|
+
'mx-auto mt-1 flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-danger',
|
|
448
|
+
)}>
|
|
449
|
+
<TriangleAlert className='size-3 shrink-0' />
|
|
450
|
+
<span className='min-w-0 flex-1'>{attachments.error}</span>
|
|
451
|
+
<button
|
|
452
|
+
type='button'
|
|
453
|
+
onClick={attachments.dismissError}
|
|
454
|
+
aria-label='Dismiss'
|
|
455
|
+
className='shrink-0 opacity-70 hover:opacity-100'>
|
|
456
|
+
<X className='size-3' />
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
459
|
+
) : null
|
|
460
|
+
|
|
461
|
+
if (terminal) {
|
|
462
|
+
// The CLI's own prompt, on the CLI's own grid: `>` sits in the gutter cell
|
|
463
|
+
// every transcript row's marker sits in, so what you type starts on the
|
|
464
|
+
// column what you are reading starts on. That alignment is the entire
|
|
465
|
+
// reason this is its own branch rather than the docked chrome restyled —
|
|
466
|
+
// the composer is outside the transcript's scroller, so it needs a surface
|
|
467
|
+
// of its own, at the same metrics.
|
|
468
|
+
const line = lineHeight ?? 18
|
|
469
|
+
return (
|
|
470
|
+
<div data-slot='composer' className={cn('shrink-0', className)}>
|
|
471
|
+
<TerminalSurface
|
|
472
|
+
{...dropHandlers}
|
|
473
|
+
fontSize={fontSize}
|
|
474
|
+
lineHeight={lineHeight}
|
|
475
|
+
affordances={affordances}
|
|
476
|
+
bleed='1ch'
|
|
477
|
+
data-dragging={dragging || undefined}
|
|
478
|
+
className={cn('term-composer', disabled && 'opacity-60')}>
|
|
479
|
+
<div className='term-composer-body'>
|
|
480
|
+
{staged.length > 0 && attachments ? (
|
|
481
|
+
<AttachmentStrip attachments={attachments} />
|
|
482
|
+
) : null}
|
|
483
|
+
<div className='term-row'>
|
|
484
|
+
{/* The hidden input only — its trigger lives in the gutter. */}
|
|
485
|
+
{canAttach ? fileField : null}
|
|
486
|
+
{gutter}
|
|
487
|
+
<div className='flex min-w-0 items-start'>
|
|
488
|
+
<PromptArea
|
|
489
|
+
{...bind}
|
|
490
|
+
triggers={triggers}
|
|
491
|
+
// What the agent receives is markdown, so the marker the user
|
|
492
|
+
// typed is the marker that has to survive. Left on (the
|
|
493
|
+
// default), the editor rewrites `- ` to `• ` in the *model*,
|
|
494
|
+
// not just on screen — so a bulleted message reached the agent
|
|
495
|
+
// as `• item`, which is not a list in any markdown parser, and
|
|
496
|
+
// drew a glyph the character grid has no cell for. Off, the
|
|
497
|
+
// convenience stays and only the rewrite goes:
|
|
498
|
+
// `insertListContinuation` keys on `[•\-*] ` and reuses the
|
|
499
|
+
// line's own marker, so Enter after `- item` still inserts
|
|
500
|
+
// `\n- `, and Enter on an empty item still leaves the list.
|
|
501
|
+
normalizeBullets={false}
|
|
502
|
+
onSubmit={submit}
|
|
503
|
+
disabled={disabled}
|
|
504
|
+
placeholder={disabled ? 'Session ended' : placeholder}
|
|
505
|
+
// The field's own metrics are the cell's: one row is one line,
|
|
506
|
+
// and it grows in whole lines from there.
|
|
507
|
+
minHeight={line}
|
|
508
|
+
maxHeight={line * 10}
|
|
509
|
+
aria-label='Message the agent'
|
|
510
|
+
className='term-composer-field min-w-0 flex-1'
|
|
511
|
+
onImagePaste={(file) => attachments?.add([file])}
|
|
512
|
+
/>
|
|
513
|
+
{submitButton}
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
{/* Model and mode below the prompt, on the body column so they read
|
|
517
|
+
as a continuation of the line rather than new chrome. Absent
|
|
518
|
+
under `controlsSurface: 'external'`. */}
|
|
519
|
+
{toolbar ? (
|
|
520
|
+
<div className='term-row'>
|
|
521
|
+
<span aria-hidden className='term-gutter' />
|
|
522
|
+
<div className='flex min-w-0 items-center gap-[1ch]'>{toolbar}</div>
|
|
523
|
+
</div>
|
|
524
|
+
) : null}
|
|
525
|
+
</div>
|
|
526
|
+
</TerminalSurface>
|
|
527
|
+
{errorRow}
|
|
528
|
+
</div>
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
368
532
|
return (
|
|
369
533
|
<div data-slot='composer' className={cn('px-3 pb-3', className)}>
|
|
370
534
|
<div
|
|
371
|
-
|
|
372
|
-
if (attachments && !attachments.disabled) {
|
|
373
|
-
e.preventDefault()
|
|
374
|
-
setDragging(true)
|
|
375
|
-
}
|
|
376
|
-
}}
|
|
377
|
-
onDragLeave={() => setDragging(false)}
|
|
378
|
-
onDrop={(e) => {
|
|
379
|
-
if (!attachments || attachments.disabled) return
|
|
380
|
-
e.preventDefault()
|
|
381
|
-
setDragging(false)
|
|
382
|
-
pick(e.dataTransfer.files)
|
|
383
|
-
}}
|
|
535
|
+
{...dropHandlers}
|
|
384
536
|
className={cn(
|
|
385
537
|
'mx-auto w-full max-w-[var(--wd-content-max-w,48rem)] overflow-hidden border border-border bg-bg',
|
|
386
|
-
'transition-colors',
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// an editor's chrome sits *in* the surface rather than floating above
|
|
390
|
-
// it, and the `lines` transcript above it has no rounded box either.
|
|
391
|
-
lines
|
|
392
|
-
? 'rounded-sm focus-within:border-accent'
|
|
393
|
-
: cn(
|
|
394
|
-
'rounded-lg shadow-(--shadow-xs)',
|
|
395
|
-
'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30',
|
|
396
|
-
),
|
|
397
|
-
dragging && (lines ? 'border-accent' : 'border-ring ring-2 ring-ring/30'),
|
|
538
|
+
'transition-colors rounded-lg shadow-(--shadow-xs)',
|
|
539
|
+
'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30',
|
|
540
|
+
dragging && 'border-ring ring-2 ring-ring/30',
|
|
398
541
|
disabled && 'opacity-60',
|
|
399
542
|
)}>
|
|
400
543
|
{/* Above the field, like the picture you are talking about should be. */}
|
|
@@ -412,6 +555,9 @@ export function Composer({
|
|
|
412
555
|
<PromptArea
|
|
413
556
|
{...bind}
|
|
414
557
|
triggers={triggers}
|
|
558
|
+
// See the terminal composer above: `•` is not a markdown list
|
|
559
|
+
// marker, and this rewrite reaches the sent message.
|
|
560
|
+
normalizeBullets={false}
|
|
415
561
|
onSubmit={submit}
|
|
416
562
|
disabled={disabled}
|
|
417
563
|
placeholder={disabled ? 'Session ended' : placeholder}
|
|
@@ -428,6 +574,9 @@ export function Composer({
|
|
|
428
574
|
<PromptArea
|
|
429
575
|
{...bind}
|
|
430
576
|
triggers={triggers}
|
|
577
|
+
// See the terminal composer above: `•` is not a markdown list
|
|
578
|
+
// marker, and this rewrite reaches the sent message.
|
|
579
|
+
normalizeBullets={false}
|
|
431
580
|
onSubmit={submit}
|
|
432
581
|
disabled={disabled}
|
|
433
582
|
placeholder={disabled ? 'Session ended' : placeholder}
|
|
@@ -447,40 +596,36 @@ export function Composer({
|
|
|
447
596
|
</>
|
|
448
597
|
)}
|
|
449
598
|
</div>
|
|
450
|
-
{
|
|
451
|
-
<div className='mx-auto mt-1 flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-danger'>
|
|
452
|
-
<TriangleAlert className='size-3 shrink-0' />
|
|
453
|
-
<span className='min-w-0 flex-1'>{attachments.error}</span>
|
|
454
|
-
<button
|
|
455
|
-
type='button'
|
|
456
|
-
onClick={attachments.dismissError}
|
|
457
|
-
aria-label='Dismiss'
|
|
458
|
-
className='shrink-0 opacity-70 hover:opacity-100'>
|
|
459
|
-
<X className='size-3' />
|
|
460
|
-
</button>
|
|
461
|
-
</div>
|
|
462
|
-
) : null}
|
|
599
|
+
{errorRow}
|
|
463
600
|
</div>
|
|
464
601
|
)
|
|
465
602
|
}
|
|
466
603
|
|
|
467
604
|
/**
|
|
468
|
-
* A composer action as a **character**, for the
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
605
|
+
* A composer action as a **character**, for the terminal composer: no pill, no
|
|
606
|
+
* border, nothing drawn until you reach for it — the surface only appears on
|
|
607
|
+
* hover/focus, which is how a terminal's own affordances behave.
|
|
608
|
+
*
|
|
609
|
+
* One cell wide and one line tall (`term-glyph` in `terminal.css`), so it sits
|
|
610
|
+
* on the same grid as the row it shares and removing it would move nothing.
|
|
473
611
|
*/
|
|
474
612
|
function GlyphButton({
|
|
613
|
+
tone,
|
|
475
614
|
label,
|
|
476
615
|
disabled,
|
|
477
616
|
onClick,
|
|
617
|
+
gutter,
|
|
478
618
|
className,
|
|
479
619
|
children,
|
|
480
620
|
}: {
|
|
621
|
+
tone?: 'blue' | 'yellow'
|
|
481
622
|
label: string
|
|
482
623
|
disabled?: boolean
|
|
483
624
|
onClick: () => void
|
|
625
|
+
/** Standing in the gutter cell rather than at the row's trailing edge, so the
|
|
626
|
+
* glyph aligns to the column's start like every transcript marker instead of
|
|
627
|
+
* centring in the 2ch cell — see `.term-glyph[data-gutter]`. */
|
|
628
|
+
gutter?: boolean
|
|
484
629
|
className?: string
|
|
485
630
|
children: ReactNode
|
|
486
631
|
}) {
|
|
@@ -491,13 +636,9 @@ function GlyphButton({
|
|
|
491
636
|
title={label}
|
|
492
637
|
disabled={disabled}
|
|
493
638
|
onClick={onClick}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
'hover:bg-surface-hover hover:text-fg-1 focus-visible:bg-surface-hover focus-visible:text-fg-1',
|
|
498
|
-
'active:bg-accent-bg disabled:pointer-events-none disabled:opacity-40',
|
|
499
|
-
className,
|
|
500
|
-
)}>
|
|
639
|
+
data-tone={tone}
|
|
640
|
+
data-gutter={gutter || undefined}
|
|
641
|
+
className={cn('term-glyph', className)}>
|
|
501
642
|
{children}
|
|
502
643
|
</button>
|
|
503
644
|
)
|
|
@@ -507,8 +648,16 @@ function GlyphButton({
|
|
|
507
648
|
* the local blob, so nothing here waits on the network; the upload's state rides
|
|
508
649
|
* on top of it and the ✕ takes it back off. */
|
|
509
650
|
function AttachmentStrip({ attachments }: { attachments: UseAttachmentsResult }) {
|
|
651
|
+
const terminal = useTranscriptVariant() === 'terminal'
|
|
510
652
|
return (
|
|
511
|
-
<div
|
|
653
|
+
<div
|
|
654
|
+
className={cn(
|
|
655
|
+
'flex gap-2 overflow-x-auto',
|
|
656
|
+
// The terminal form draws no rule of its own: the composer's frame is
|
|
657
|
+
// already directly below this strip, and two rules a few pixels apart
|
|
658
|
+
// is the box the theme has none of. Geometry in `terminal.css`.
|
|
659
|
+
terminal ? 'term-attachments' : 'border-b border-border px-2 py-2',
|
|
660
|
+
)}>
|
|
512
661
|
{attachments.items.map((item) => (
|
|
513
662
|
<AttachmentChip
|
|
514
663
|
key={item.key}
|
|
@@ -531,15 +680,23 @@ function AttachmentChip({
|
|
|
531
680
|
onRemove: () => void
|
|
532
681
|
}) {
|
|
533
682
|
const failed = item.status === 'failed'
|
|
683
|
+
const terminal = useTranscriptVariant() === 'terminal'
|
|
684
|
+
// Corners are the whole difference: rounded and floating in `cards`, square
|
|
685
|
+
// and inside the frame here. Kept as one flag rather than four so a thumbnail
|
|
686
|
+
// and the overlays stacked on it cannot disagree about their own shape.
|
|
687
|
+
const round = terminal ? '' : 'rounded-md'
|
|
534
688
|
return (
|
|
535
689
|
<div
|
|
536
690
|
className='group relative shrink-0'
|
|
537
691
|
title={failed ? `${item.name} — ${item.error}` : `${item.name} · ${formatBytes(item.bytes)}`}>
|
|
538
692
|
<div
|
|
539
693
|
className={cn(
|
|
540
|
-
'flex size-14 items-center justify-center overflow-hidden
|
|
694
|
+
'flex size-14 items-center justify-center overflow-hidden bg-surface',
|
|
695
|
+
round,
|
|
696
|
+
terminal ? 'term-attachment' : 'border border-border',
|
|
541
697
|
failed && 'border-danger/50',
|
|
542
|
-
)}
|
|
698
|
+
)}
|
|
699
|
+
data-failed={terminal && failed ? '' : undefined}>
|
|
543
700
|
{item.previewUrl ? (
|
|
544
701
|
<img src={item.previewUrl} alt={item.name} className='size-full object-cover' />
|
|
545
702
|
) : (
|
|
@@ -552,7 +709,7 @@ function AttachmentChip({
|
|
|
552
709
|
)}
|
|
553
710
|
</div>
|
|
554
711
|
{item.status === 'uploading' ? (
|
|
555
|
-
<div className='absolute inset-0 flex items-center justify-center
|
|
712
|
+
<div className={cn('absolute inset-0 flex items-center justify-center bg-black/35', round)}>
|
|
556
713
|
<Spinner className='size-4 text-white' />
|
|
557
714
|
</div>
|
|
558
715
|
) : null}
|
|
@@ -561,7 +718,10 @@ function AttachmentChip({
|
|
|
561
718
|
type='button'
|
|
562
719
|
onClick={onRetry}
|
|
563
720
|
aria-label={`Retry ${item.name}`}
|
|
564
|
-
className=
|
|
721
|
+
className={cn(
|
|
722
|
+
'absolute inset-0 flex items-center justify-center bg-black/45 text-warning',
|
|
723
|
+
round,
|
|
724
|
+
)}>
|
|
565
725
|
<RotateCw className='size-4' />
|
|
566
726
|
</button>
|
|
567
727
|
) : null}
|
|
@@ -569,7 +729,14 @@ function AttachmentChip({
|
|
|
569
729
|
type='button'
|
|
570
730
|
onClick={onRemove}
|
|
571
731
|
aria-label={`Remove ${item.name}`}
|
|
572
|
-
className=
|
|
732
|
+
className={cn(
|
|
733
|
+
'absolute flex size-4 items-center justify-center bg-surface text-fg-3 hover:text-fg-1',
|
|
734
|
+
// Cards hang it off the corner as a round badge; the terminal tucks it
|
|
735
|
+
// inside, squared, with two rules for its corner (`terminal.css`).
|
|
736
|
+
terminal
|
|
737
|
+
? 'term-attachment-remove'
|
|
738
|
+
: '-top-1 -right-1 rounded-full border border-border shadow-(--shadow-xs)',
|
|
739
|
+
)}>
|
|
573
740
|
<X className='size-2.5' />
|
|
574
741
|
</button>
|
|
575
742
|
</div>
|
|
@@ -6,25 +6,25 @@ import { cn } from '../../lib/utils.ts'
|
|
|
6
6
|
|
|
7
7
|
export interface ConversationProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
8
8
|
children: ReactNode
|
|
9
|
-
/**
|
|
10
|
-
* How the view follows content that grows. `'smooth'` is right for a live
|
|
11
|
-
* turn — the text scrolls as it is written. `'instant'` is right while a
|
|
12
|
-
* transcript is *filling* (an attach replaying a whole session), where
|
|
13
|
-
* animating each arrival makes opening a session a several-second journey
|
|
14
|
-
* from its first row to its last.
|
|
15
|
-
*/
|
|
16
|
-
resize?: 'smooth' | 'instant'
|
|
17
9
|
}
|
|
18
10
|
|
|
19
11
|
/** Scroll container that stays pinned to the bottom while streaming, unless the user
|
|
20
|
-
* scrolls up — plus a floating scroll-to-bottom button.
|
|
21
|
-
|
|
12
|
+
* scrolls up — plus a floating scroll-to-bottom button.
|
|
13
|
+
*
|
|
14
|
+
* **Nothing here animates its scroll position.** `initial` and `resize` are both
|
|
15
|
+
* `'instant'` and are no longer configurable: the follow spring's job is to
|
|
16
|
+
* *stay* at the bottom, not to travel there, and every travel a reader has ever
|
|
17
|
+
* complained about on this surface was an animation we asked for. The `resize`
|
|
18
|
+
* prop used to exist so a caller could pick, which required the caller to know
|
|
19
|
+
* whether the transcript was streaming or replaying — a latch's worth of
|
|
20
|
+
* machinery to decide something that has one right answer. */
|
|
21
|
+
export function Conversation({ className, children, ...props }: ConversationProps) {
|
|
22
22
|
return (
|
|
23
23
|
<StickToBottom
|
|
24
24
|
data-slot='conversation'
|
|
25
25
|
className={cn('relative flex-1 overflow-y-auto', className)}
|
|
26
26
|
initial='instant'
|
|
27
|
-
resize=
|
|
27
|
+
resize='instant'
|
|
28
28
|
role='log'
|
|
29
29
|
{...props}>
|
|
30
30
|
{children}
|
|
@@ -60,7 +60,7 @@ export function ConversationScrollButton({ className }: { className?: string })
|
|
|
60
60
|
'absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-(--shadow-md)',
|
|
61
61
|
className,
|
|
62
62
|
)}
|
|
63
|
-
onClick={() => void scrollToBottom()}>
|
|
63
|
+
onClick={() => void scrollToBottom('instant')}>
|
|
64
64
|
<ArrowDown className='size-4' />
|
|
65
65
|
</Button>
|
|
66
66
|
)
|
|
@@ -2,7 +2,6 @@ import type { TranscriptItem } from '@workerdeck/react'
|
|
|
2
2
|
import { Download, FileText } from 'lucide-react'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
import { formatBytes } from '../../lib/format.ts'
|
|
5
|
-
import { LineGlyph, useLines } from './transcript-variant.tsx'
|
|
6
5
|
|
|
7
6
|
export type FileDeliveredItem = Extract<TranscriptItem, { kind: 'file_delivered' }>
|
|
8
7
|
|
|
@@ -18,32 +17,7 @@ export interface FileCardProps {
|
|
|
18
17
|
* The file lives in the session's in-memory VFS — the link works while the
|
|
19
18
|
* session lives. */
|
|
20
19
|
export function FileCard({ item, href, className }: FileCardProps) {
|
|
21
|
-
const lines = useLines()
|
|
22
20
|
const name = item.path.split('/').pop() || item.path
|
|
23
|
-
if (lines) {
|
|
24
|
-
// One row: what arrived, how big, and the link — no box around it.
|
|
25
|
-
return (
|
|
26
|
-
<div data-slot='file-delivered' className={cn('flex w-full items-baseline gap-2', className)}>
|
|
27
|
-
<LineGlyph className='text-fg-4'>◇</LineGlyph>
|
|
28
|
-
<span className='min-w-0 flex-1 truncate text-body-sm leading-5'>
|
|
29
|
-
<span className='font-medium text-fg-1'>{name}</span>
|
|
30
|
-
<span className='text-fg-4'>
|
|
31
|
-
{' · '}
|
|
32
|
-
{formatBytes(item.bytes)}
|
|
33
|
-
{item.description ? ` · ${item.description}` : ''}
|
|
34
|
-
</span>
|
|
35
|
-
</span>
|
|
36
|
-
{href ? (
|
|
37
|
-
<a
|
|
38
|
-
href={href}
|
|
39
|
-
download={name}
|
|
40
|
-
className='shrink-0 text-label text-fg-3 underline-offset-2 hover:text-fg-1 hover:underline'>
|
|
41
|
-
download
|
|
42
|
-
</a>
|
|
43
|
-
) : null}
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
21
|
return (
|
|
48
22
|
<div
|
|
49
23
|
data-slot='file-delivered'
|
|
@@ -108,9 +108,7 @@ export function FileTree({
|
|
|
108
108
|
) : null}
|
|
109
109
|
</div>
|
|
110
110
|
) : (
|
|
111
|
-
<span className='min-w-0 flex-1 truncate px-1
|
|
112
|
-
{tree.root}
|
|
113
|
-
</span>
|
|
111
|
+
<span className='min-w-0 flex-1 truncate px-1 text-label text-fg-4'>{tree.root}</span>
|
|
114
112
|
)}
|
|
115
113
|
<Button
|
|
116
114
|
variant='ghost'
|
|
@@ -259,11 +257,14 @@ function Row({
|
|
|
259
257
|
<span className='flex size-3 shrink-0 items-center justify-center'>{chevron}</span>
|
|
260
258
|
{icon}
|
|
261
259
|
{/* `shrink-0` on the name and `min-w-0` on the detail: when the row runs
|
|
262
|
-
out of room the directory gives way and the filename stays whole.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
out of room the directory gives way and the filename stays whole.
|
|
261
|
+
The rail reads in the **UI font**, never mono: it is workbench chrome
|
|
262
|
+
— a list you scan — and the editors it sits beside (VS Code, Finder)
|
|
263
|
+
all set filenames in their UI face. Mono is for content on a grid, and
|
|
264
|
+
nothing here is on one. It is also independent of `transcriptFont`,
|
|
265
|
+
which scopes to the session panel alone. */}
|
|
266
|
+
<span className='shrink-0 truncate text-label'>{label}</span>
|
|
267
|
+
{detail ? <span className='min-w-0 flex-1 truncate text-label text-fg-4'>{detail}</span> : null}
|
|
267
268
|
</button>
|
|
268
269
|
)
|
|
269
270
|
}
|