@workerdeck/ui 0.15.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 +53 -0
- package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-B9CHoq8x.d.mts} +158 -21
- package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DII9MmQ8.mjs} +4254 -1661
- 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 +442 -45
- package/build/index.mjs +105 -2
- 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 +14 -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/QuestionPrompt.tsx +0 -122
- package/src/components/agent/Reasoning.tsx +5 -19
- package/src/components/agent/Response.tsx +1 -132
- package/src/components/agent/SessionPanel.tsx +224 -28
- package/src/components/agent/SessionWorkspace.tsx +29 -0
- package/src/components/agent/StatusBar.tsx +20 -4
- 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 +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 +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/index.ts +34 -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 +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
|
@@ -24,7 +24,10 @@ import { Spinner } from '../ui/Spinner.tsx'
|
|
|
24
24
|
import { PromptArea } from '../prompt-area/prompt-area.tsx'
|
|
25
25
|
import { usePromptAreaState } from '../prompt-area/use-prompt-area-state.ts'
|
|
26
26
|
import { commandTrigger, mentionTrigger } from '../prompt-area/trigger-presets.ts'
|
|
27
|
-
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'
|
|
28
31
|
import type { TriggerSuggestion } from '../prompt-area/types.ts'
|
|
29
32
|
import { cn } from '../../lib/utils.ts'
|
|
30
33
|
import { formatBytes } from '../../lib/format.ts'
|
|
@@ -77,6 +80,19 @@ export interface ComposerProps {
|
|
|
77
80
|
* empty composer would otherwise spend two rows saying nothing.
|
|
78
81
|
*/
|
|
79
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
|
|
80
96
|
className?: string
|
|
81
97
|
ref?: Ref<ComposerHandle>
|
|
82
98
|
}
|
|
@@ -145,13 +161,16 @@ export function Composer({
|
|
|
145
161
|
attachments,
|
|
146
162
|
toolbar,
|
|
147
163
|
layout = 'stacked',
|
|
164
|
+
fontSize,
|
|
165
|
+
lineHeight,
|
|
166
|
+
affordances,
|
|
148
167
|
className,
|
|
149
168
|
ref,
|
|
150
169
|
}: ComposerProps) {
|
|
151
170
|
const inline = layout === 'inline'
|
|
152
171
|
// The transcript's variant reaches here through the panel-wide context, so
|
|
153
172
|
// the composer matches the rows above it without a prop chain.
|
|
154
|
-
const
|
|
173
|
+
const terminal = useTranscriptVariant() === 'terminal'
|
|
155
174
|
const { bind, plainText, isEmpty, clear, focus } = usePromptAreaState()
|
|
156
175
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
157
176
|
const [dragging, setDragging] = useState(false)
|
|
@@ -322,36 +341,69 @@ export function Composer({
|
|
|
322
341
|
const attach = canAttach ? (
|
|
323
342
|
<>
|
|
324
343
|
{fileField}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
variant='ghost'
|
|
334
|
-
size='icon-sm'
|
|
335
|
-
aria-label='Attach files'
|
|
336
|
-
disabled={disabled}
|
|
337
|
-
onClick={() => fileInput.current?.click()}>
|
|
338
|
-
<Paperclip className='size-4' />
|
|
339
|
-
</Button>
|
|
340
|
-
)}
|
|
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>
|
|
341
352
|
</>
|
|
342
353
|
) : null
|
|
343
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
|
+
|
|
344
399
|
const interrupting = busy && !canSend
|
|
345
|
-
const submitButton =
|
|
400
|
+
const submitButton = terminal ? (
|
|
346
401
|
// Terminal furniture rather than chat furniture: a glyph that lights up on
|
|
347
|
-
// hover/focus instead of a filled pill. `↵` is
|
|
348
|
-
// — the
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
onClick={interrupting ? onInterrupt : submit}
|
|
353
|
-
className={interrupting ? 'text-warning' : canSend ? 'text-accent' : undefined}>
|
|
354
|
-
{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
|
+
↵
|
|
355
407
|
</GlyphButton>
|
|
356
408
|
) : interrupting ? (
|
|
357
409
|
<Button
|
|
@@ -393,7 +445,6 @@ export function Composer({
|
|
|
393
445
|
<div
|
|
394
446
|
className={cn(
|
|
395
447
|
'mx-auto mt-1 flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-danger',
|
|
396
|
-
lines && 'px-2',
|
|
397
448
|
)}>
|
|
398
449
|
<TriangleAlert className='size-3 shrink-0' />
|
|
399
450
|
<span className='min-w-0 flex-1'>{attachments.error}</span>
|
|
@@ -407,59 +458,72 @@ export function Composer({
|
|
|
407
458
|
</div>
|
|
408
459
|
) : null
|
|
409
460
|
|
|
410
|
-
if (
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
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
|
|
418
469
|
return (
|
|
419
470
|
<div data-slot='composer' className={cn('shrink-0', className)}>
|
|
420
|
-
<
|
|
471
|
+
<TerminalSurface
|
|
421
472
|
{...dropHandlers}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
dragging && 'border-t-accent',
|
|
430
|
-
disabled && 'opacity-60',
|
|
431
|
-
)}>
|
|
432
|
-
<div className='mx-auto w-full max-w-[var(--wd-content-max-w,48rem)] px-2 py-1'>
|
|
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'>
|
|
433
480
|
{staged.length > 0 && attachments ? (
|
|
434
481
|
<AttachmentStrip attachments={attachments} />
|
|
435
482
|
) : null}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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>
|
|
454
515
|
</div>
|
|
455
|
-
{/* Model and mode
|
|
456
|
-
|
|
457
|
-
|
|
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'`. */}
|
|
458
519
|
{toolbar ? (
|
|
459
|
-
<div className='
|
|
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>
|
|
460
524
|
) : null}
|
|
461
525
|
</div>
|
|
462
|
-
</
|
|
526
|
+
</TerminalSurface>
|
|
463
527
|
{errorRow}
|
|
464
528
|
</div>
|
|
465
529
|
)
|
|
@@ -491,6 +555,9 @@ export function Composer({
|
|
|
491
555
|
<PromptArea
|
|
492
556
|
{...bind}
|
|
493
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}
|
|
494
561
|
onSubmit={submit}
|
|
495
562
|
disabled={disabled}
|
|
496
563
|
placeholder={disabled ? 'Session ended' : placeholder}
|
|
@@ -507,6 +574,9 @@ export function Composer({
|
|
|
507
574
|
<PromptArea
|
|
508
575
|
{...bind}
|
|
509
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}
|
|
510
580
|
onSubmit={submit}
|
|
511
581
|
disabled={disabled}
|
|
512
582
|
placeholder={disabled ? 'Session ended' : placeholder}
|
|
@@ -532,22 +602,30 @@ export function Composer({
|
|
|
532
602
|
}
|
|
533
603
|
|
|
534
604
|
/**
|
|
535
|
-
* A composer action as a **character**, for the
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
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.
|
|
540
611
|
*/
|
|
541
612
|
function GlyphButton({
|
|
613
|
+
tone,
|
|
542
614
|
label,
|
|
543
615
|
disabled,
|
|
544
616
|
onClick,
|
|
617
|
+
gutter,
|
|
545
618
|
className,
|
|
546
619
|
children,
|
|
547
620
|
}: {
|
|
621
|
+
tone?: 'blue' | 'yellow'
|
|
548
622
|
label: string
|
|
549
623
|
disabled?: boolean
|
|
550
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
|
|
551
629
|
className?: string
|
|
552
630
|
children: ReactNode
|
|
553
631
|
}) {
|
|
@@ -558,13 +636,9 @@ function GlyphButton({
|
|
|
558
636
|
title={label}
|
|
559
637
|
disabled={disabled}
|
|
560
638
|
onClick={onClick}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
'hover:bg-surface-hover hover:text-fg-1 focus-visible:bg-surface-hover focus-visible:text-fg-1',
|
|
565
|
-
'active:bg-accent-bg disabled:pointer-events-none disabled:opacity-40',
|
|
566
|
-
className,
|
|
567
|
-
)}>
|
|
639
|
+
data-tone={tone}
|
|
640
|
+
data-gutter={gutter || undefined}
|
|
641
|
+
className={cn('term-glyph', className)}>
|
|
568
642
|
{children}
|
|
569
643
|
</button>
|
|
570
644
|
)
|
|
@@ -574,8 +648,16 @@ function GlyphButton({
|
|
|
574
648
|
* the local blob, so nothing here waits on the network; the upload's state rides
|
|
575
649
|
* on top of it and the ✕ takes it back off. */
|
|
576
650
|
function AttachmentStrip({ attachments }: { attachments: UseAttachmentsResult }) {
|
|
651
|
+
const terminal = useTranscriptVariant() === 'terminal'
|
|
577
652
|
return (
|
|
578
|
-
<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
|
+
)}>
|
|
579
661
|
{attachments.items.map((item) => (
|
|
580
662
|
<AttachmentChip
|
|
581
663
|
key={item.key}
|
|
@@ -598,15 +680,23 @@ function AttachmentChip({
|
|
|
598
680
|
onRemove: () => void
|
|
599
681
|
}) {
|
|
600
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'
|
|
601
688
|
return (
|
|
602
689
|
<div
|
|
603
690
|
className='group relative shrink-0'
|
|
604
691
|
title={failed ? `${item.name} — ${item.error}` : `${item.name} · ${formatBytes(item.bytes)}`}>
|
|
605
692
|
<div
|
|
606
693
|
className={cn(
|
|
607
|
-
'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',
|
|
608
697
|
failed && 'border-danger/50',
|
|
609
|
-
)}
|
|
698
|
+
)}
|
|
699
|
+
data-failed={terminal && failed ? '' : undefined}>
|
|
610
700
|
{item.previewUrl ? (
|
|
611
701
|
<img src={item.previewUrl} alt={item.name} className='size-full object-cover' />
|
|
612
702
|
) : (
|
|
@@ -619,7 +709,7 @@ function AttachmentChip({
|
|
|
619
709
|
)}
|
|
620
710
|
</div>
|
|
621
711
|
{item.status === 'uploading' ? (
|
|
622
|
-
<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)}>
|
|
623
713
|
<Spinner className='size-4 text-white' />
|
|
624
714
|
</div>
|
|
625
715
|
) : null}
|
|
@@ -628,7 +718,10 @@ function AttachmentChip({
|
|
|
628
718
|
type='button'
|
|
629
719
|
onClick={onRetry}
|
|
630
720
|
aria-label={`Retry ${item.name}`}
|
|
631
|
-
className=
|
|
721
|
+
className={cn(
|
|
722
|
+
'absolute inset-0 flex items-center justify-center bg-black/45 text-warning',
|
|
723
|
+
round,
|
|
724
|
+
)}>
|
|
632
725
|
<RotateCw className='size-4' />
|
|
633
726
|
</button>
|
|
634
727
|
) : null}
|
|
@@ -636,7 +729,14 @@ function AttachmentChip({
|
|
|
636
729
|
type='button'
|
|
637
730
|
onClick={onRemove}
|
|
638
731
|
aria-label={`Remove ${item.name}`}
|
|
639
|
-
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
|
+
)}>
|
|
640
740
|
<X className='size-2.5' />
|
|
641
741
|
</button>
|
|
642
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
|
}
|
|
@@ -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
|
}
|