@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,131 +1,6 @@
|
|
|
1
1
|
import { memo } from 'react'
|
|
2
2
|
import { Streamdown } from 'streamdown'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
|
-
import { useLines } from './transcript-variant.tsx'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Markdown on a terminal's grid.
|
|
8
|
-
*
|
|
9
|
-
* Prose defaults give every block its own rhythm — a heading is bigger, a list
|
|
10
|
-
* item is looser, an inline `code` chip is padded taller than the line it sits
|
|
11
|
-
* in — and the result is text that visibly breathes at different rates as you
|
|
12
|
-
* scroll. A terminal has exactly one line height and one type size, and that
|
|
13
|
-
* uniformity is most of what makes it readable at a glance.
|
|
14
|
-
*
|
|
15
|
-
* So: every line 1.25rem, every block flush, and one blank line (1.25rem, i.e.
|
|
16
|
-
* exactly one row) between blocks. Headings keep their weight but lose their
|
|
17
|
-
* size. Inline code keeps a background — it still has to be distinguishable —
|
|
18
|
-
* but no vertical padding, because that is what pushes a line off the grid.
|
|
19
|
-
*/
|
|
20
|
-
const TERMINAL_PROSE = [
|
|
21
|
-
'text-body-sm leading-5',
|
|
22
|
-
'[&_*]:leading-5',
|
|
23
|
-
// Block rhythm: nothing has its own margins, and siblings are one row apart.
|
|
24
|
-
// `!` throughout because these fight the renderer's own utilities, and some of
|
|
25
|
-
// those (`space-y-*`, which is a `> * + *` rule) outrank a plain descendant
|
|
26
|
-
// selector — a list would otherwise keep its half-row gaps while every other
|
|
27
|
-
// block snapped to the grid.
|
|
28
|
-
'[&_p]:my-0! [&_pre]:my-0! [&_blockquote]:my-0! [&_table]:my-0!',
|
|
29
|
-
'[&_h1]:my-0! [&_h2]:my-0! [&_h3]:my-0! [&_h4]:my-0! [&_h5]:my-0! [&_h6]:my-0!',
|
|
30
|
-
'[&>*+*]:mt-5',
|
|
31
|
-
// Lists are rows, not paragraphs: no margin anywhere in them, and no
|
|
32
|
-
// inter-item spacing from a `space-y` on the list itself.
|
|
33
|
-
'[&_ul]:my-0! [&_ol]:my-0! [&_li]:my-0! [&_li>p]:my-0!',
|
|
34
|
-
'[&_li+li]:mt-0! [&_ul_ul]:mt-0! [&_ul_ol]:mt-0! [&_ol_ul]:mt-0! [&_ol_ol]:mt-0!',
|
|
35
|
-
'[&_li]:py-0! [&_ul]:py-0! [&_ol]:py-0!',
|
|
36
|
-
// Markers on their own column, at the width the marker actually is — the
|
|
37
|
-
// indent a terminal (and the markdown source) uses, not a typographic one.
|
|
38
|
-
// `ch` is the unit because this variant runs in a monospace face: `• ` is two
|
|
39
|
-
// cells, `1. ` is three, so the text starts exactly where it would in the raw
|
|
40
|
-
// document and a wrapped line hangs under it instead of under the bullet.
|
|
41
|
-
// Nesting then costs one marker width per level, which is the 2-space step.
|
|
42
|
-
// `list-outside` is the load-bearing half: the renderer marks lists `inside`,
|
|
43
|
-
// where the marker joins the text flow — so the indent moves the bullet too
|
|
44
|
-
// (the list sits a marker-width right of the paragraph above it) and a wrapped
|
|
45
|
-
// line runs back under the bullet instead of hanging under its own text.
|
|
46
|
-
'[&_ul]:list-outside! [&_ol]:list-outside!',
|
|
47
|
-
'[&_ul]:pl-[2ch]! [&_ol]:pl-[3ch]! [&_li]:pl-0!',
|
|
48
|
-
// Headings are weight, not size — a terminal has one type size.
|
|
49
|
-
'[&_h1]:text-body-sm [&_h2]:text-body-sm [&_h3]:text-body-sm',
|
|
50
|
-
'[&_h4]:text-body-sm [&_h5]:text-body-sm [&_h6]:text-body-sm',
|
|
51
|
-
'[&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold',
|
|
52
|
-
'[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold',
|
|
53
|
-
'[&_blockquote]:border-l [&_blockquote]:border-border [&_blockquote]:pl-2',
|
|
54
|
-
// Inline code only (`:not(pre) > code`) — fenced blocks keep their own box.
|
|
55
|
-
// Horizontal padding is free; vertical padding is what pushes a line off grid.
|
|
56
|
-
'[&_:not(pre)>code]:rounded-none [&_:not(pre)>code]:px-1! [&_:not(pre)>code]:py-0!',
|
|
57
|
-
'[&_:not(pre)>code]:text-body-sm [&_:not(pre)>code]:leading-5! [&_:not(pre)>code]:text-fg-1',
|
|
58
|
-
'[&_th]:py-0! [&_td]:py-0!',
|
|
59
|
-
// The renderer's fenced-code card: rounded frame, its own header strip, a
|
|
60
|
-
// second rounded frame inside it, and a floating pill for the buttons. Four
|
|
61
|
-
// boxes around what is, in a terminal, a band of dim text. Flattened to
|
|
62
|
-
// exactly that — the band keeps a background so a block still reads as code,
|
|
63
|
-
// and the actions keep their top-right corner but lose the pill.
|
|
64
|
-
'[&_[data-streamdown=code-block]]:my-0! [&_[data-streamdown=code-block]]:gap-0!',
|
|
65
|
-
'[&_[data-streamdown=code-block]]:rounded-none! [&_[data-streamdown=code-block]]:border-0!',
|
|
66
|
-
'[&_[data-streamdown=code-block]]:bg-transparent! [&_[data-streamdown=code-block]]:p-0!',
|
|
67
|
-
// The language strip costs a whole row to say what the code already shows, and
|
|
68
|
-
// the actions cost another. Both go: the block is the band, and the buttons
|
|
69
|
-
// float over its top-right corner, appearing on hover or keyboard focus.
|
|
70
|
-
'[&_[data-streamdown=code-block]]:relative!',
|
|
71
|
-
'[&_[data-streamdown=code-block-header]]:hidden!',
|
|
72
|
-
'[&_div:has(>[data-streamdown=code-block-actions])]:absolute! [&_div:has(>[data-streamdown=code-block-actions])]:top-0!',
|
|
73
|
-
'[&_div:has(>[data-streamdown=code-block-actions])]:right-0! [&_div:has(>[data-streamdown=code-block-actions])]:mt-0!',
|
|
74
|
-
'[&_div:has(>[data-streamdown=code-block-actions])]:h-5! [&_div:has(>[data-streamdown=code-block-actions])]:opacity-0!',
|
|
75
|
-
'[&_div:has(>[data-streamdown=code-block-actions])]:transition-opacity',
|
|
76
|
-
'[&_[data-streamdown=code-block]:hover_div:has(>[data-streamdown=code-block-actions])]:opacity-100!',
|
|
77
|
-
'[&_[data-streamdown=code-block]:focus-within_div:has(>[data-streamdown=code-block-actions])]:opacity-100!',
|
|
78
|
-
'[&_[data-streamdown=code-block-actions]]:rounded-none! [&_[data-streamdown=code-block-actions]]:border-0!',
|
|
79
|
-
'[&_[data-streamdown=code-block-actions]]:bg-code-bg! [&_[data-streamdown=code-block-actions]]:px-1!',
|
|
80
|
-
'[&_[data-streamdown=code-block-actions]]:py-0! [&_[data-streamdown=code-block-actions]]:gap-1!',
|
|
81
|
-
'[&_[data-streamdown=code-block-actions]]:backdrop-blur-none!',
|
|
82
|
-
'[&_[data-streamdown=code-block-body]]:rounded-none! [&_[data-streamdown=code-block-body]]:border-0!',
|
|
83
|
-
'[&_[data-streamdown=code-block-body]]:bg-code-bg! [&_[data-streamdown=code-block-body]]:px-2!',
|
|
84
|
-
'[&_[data-streamdown=code-block-body]]:py-0! [&_[data-streamdown=code-block-body]]:text-body-sm!',
|
|
85
|
-
// The action buttons' hit areas, on the grid like everything else.
|
|
86
|
-
'[&_[data-streamdown=code-block-copy-button]]:p-0! [&_[data-streamdown=code-block-download-button]]:p-0!',
|
|
87
|
-
// Tables come in a frame of their own, plus a controls row above them and a
|
|
88
|
-
// second frame around the scroller — three boxes and a blank line to hold
|
|
89
|
-
// three buttons. Same treatment: the table is the content, the buttons float
|
|
90
|
-
// over its top-right on hover. The controls row is the child that has no
|
|
91
|
-
// `<table>` in it, which is also how it knows to disappear when the renderer
|
|
92
|
-
// draws no controls at all.
|
|
93
|
-
'[&_[data-streamdown=table-wrapper]]:rounded-none! [&_[data-streamdown=table-wrapper]]:border-0!',
|
|
94
|
-
'[&_[data-streamdown=table-wrapper]]:my-0! [&_[data-streamdown=table-wrapper]]:bg-transparent!',
|
|
95
|
-
'[&_[data-streamdown=table-wrapper]]:relative! [&_[data-streamdown=table-wrapper]]:gap-0!',
|
|
96
|
-
'[&_[data-streamdown=table-wrapper]]:p-0!',
|
|
97
|
-
'[&_[data-streamdown=table-wrapper]>div:not(:has(table))]:absolute! [&_[data-streamdown=table-wrapper]>div:not(:has(table))]:top-0!',
|
|
98
|
-
'[&_[data-streamdown=table-wrapper]>div:not(:has(table))]:right-0! [&_[data-streamdown=table-wrapper]>div:not(:has(table))]:z-10!',
|
|
99
|
-
'[&_[data-streamdown=table-wrapper]>div:not(:has(table))]:opacity-0! [&_[data-streamdown=table-wrapper]>div:not(:has(table))]:bg-code-bg!',
|
|
100
|
-
'[&_[data-streamdown=table-wrapper]>div:not(:has(table))]:px-1! [&_[data-streamdown=table-wrapper]>div:not(:has(table))]:transition-opacity',
|
|
101
|
-
'[&_[data-streamdown=table-wrapper]:hover>div:not(:has(table))]:opacity-100!',
|
|
102
|
-
'[&_[data-streamdown=table-wrapper]:focus-within>div:not(:has(table))]:opacity-100!',
|
|
103
|
-
'[&_[data-streamdown=table-wrapper]>div:has(table)]:rounded-none! [&_[data-streamdown=table-wrapper]>div:has(table)]:border-0!',
|
|
104
|
-
'[&_[data-streamdown=table-wrapper]>div:has(table)]:bg-transparent!',
|
|
105
|
-
].join(' ')
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The controls' icons as characters.
|
|
109
|
-
*
|
|
110
|
-
* The renderer draws its copy/download affordances as line-art SVGs, which next
|
|
111
|
-
* to monospace text read as buttons from another application. These are the
|
|
112
|
-
* glyphs a terminal would use — and they inherit the surrounding font, so they
|
|
113
|
-
* sit on the same grid as the code they belong to.
|
|
114
|
-
*/
|
|
115
|
-
const glyphIcon = (glyph: string) =>
|
|
116
|
-
function GlyphIcon({ className }: { className?: string }) {
|
|
117
|
-
return (
|
|
118
|
-
<span aria-hidden className={cn('font-mono text-body-sm leading-5', className)}>
|
|
119
|
-
{glyph}
|
|
120
|
-
</span>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const TERMINAL_ICONS = {
|
|
125
|
-
DownloadIcon: glyphIcon('⤓'),
|
|
126
|
-
CopyIcon: glyphIcon('⧉'),
|
|
127
|
-
CheckIcon: glyphIcon('✓'),
|
|
128
|
-
}
|
|
129
4
|
|
|
130
5
|
export interface ResponseProps {
|
|
131
6
|
children: string
|
|
@@ -138,18 +13,12 @@ export interface ResponseProps {
|
|
|
138
13
|
* highlighted with shiki (dual theme follows [data-theme] through the dark: variant). */
|
|
139
14
|
export const Response = memo(
|
|
140
15
|
function Response({ children, streaming, className }: ResponseProps) {
|
|
141
|
-
const lines = useLines()
|
|
142
16
|
return (
|
|
143
17
|
<Streamdown
|
|
144
18
|
mode={streaming ? 'streaming' : 'static'}
|
|
145
19
|
parseIncompleteMarkdown={streaming}
|
|
146
20
|
shikiTheme={['github-light', 'github-dark']}
|
|
147
|
-
|
|
148
|
-
className={cn(
|
|
149
|
-
'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
|
150
|
-
lines && TERMINAL_PROSE,
|
|
151
|
-
className,
|
|
152
|
-
)}>
|
|
21
|
+
className={cn('size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0', className)}>
|
|
153
22
|
{children}
|
|
154
23
|
</Streamdown>
|
|
155
24
|
)
|
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
BellRing,
|
|
4
4
|
CircleAlert,
|
|
5
5
|
CircleSlash,
|
|
6
|
+
Layers,
|
|
6
7
|
Moon,
|
|
7
8
|
PauseCircle,
|
|
8
9
|
Pencil,
|
|
9
10
|
Search,
|
|
11
|
+
SearchX,
|
|
10
12
|
Trash2,
|
|
11
13
|
X,
|
|
12
14
|
} from 'lucide-react'
|
|
@@ -30,6 +32,7 @@ import type {
|
|
|
30
32
|
WorkspaceScope,
|
|
31
33
|
} from '@workerdeck/protocol'
|
|
32
34
|
import { Button } from '../ui/Button.tsx'
|
|
35
|
+
import { Empty } from '../ui/Empty.tsx'
|
|
33
36
|
import { Input } from '../ui/Input.tsx'
|
|
34
37
|
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
35
38
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
@@ -228,25 +231,23 @@ export function SessionBrowser({
|
|
|
228
231
|
) : null}
|
|
229
232
|
|
|
230
233
|
{rows.length === 0 ? (
|
|
231
|
-
(emptyState ?? <Empty
|
|
234
|
+
(emptyState ?? <Empty icon={<Layers />} title='No sessions yet' />)
|
|
232
235
|
) : visible.length === 0 ? (
|
|
233
236
|
// Two different dead ends, two different ways out. "Nothing matches" is
|
|
234
|
-
// a filter someone set; anything else is the state of the world
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)}
|
|
249
|
-
</Empty>
|
|
237
|
+
// a filter someone set; anything else is the state of the world — and
|
|
238
|
+
// only the first has a button, because an action that does nothing is
|
|
239
|
+
// worse than none.
|
|
240
|
+
hasFacetFilter(config) ? (
|
|
241
|
+
<Empty
|
|
242
|
+
icon={<SearchX />}
|
|
243
|
+
title='No matches'
|
|
244
|
+
description='No session matches the current search and filters.'
|
|
245
|
+
action='Clear filters'
|
|
246
|
+
onAction={() => onConfigChange(clearFilters(config))}
|
|
247
|
+
/>
|
|
248
|
+
) : (
|
|
249
|
+
<Empty icon={<Layers />} title='Nothing here' description='No session to show.' />
|
|
250
|
+
)
|
|
250
251
|
) : (
|
|
251
252
|
<div className='flex flex-col gap-4'>
|
|
252
253
|
{groups.map((group) => (
|
|
@@ -276,10 +277,6 @@ export function SessionBrowser({
|
|
|
276
277
|
)
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
function Empty({ children }: { children: React.ReactNode }) {
|
|
280
|
-
return <div className='px-3 py-6 text-center text-body-sm text-fg-4'>{children}</div>
|
|
281
|
-
}
|
|
282
|
-
|
|
283
280
|
interface SessionRowItemProps {
|
|
284
281
|
row: SessionRow
|
|
285
282
|
active?: boolean
|
|
@@ -315,6 +312,14 @@ function SessionRowItem({
|
|
|
315
312
|
<div
|
|
316
313
|
data-slot='session-row'
|
|
317
314
|
data-active={active || undefined}
|
|
315
|
+
// Selection lives on the whole row, not on the two text buttons inside
|
|
316
|
+
// it: the age, the unread badge and the state glyph sit outside them, and
|
|
317
|
+
// a row where a third of the surface silently does nothing is a row that
|
|
318
|
+
// feels broken. The buttons stay — they are what makes the row reachable
|
|
319
|
+
// by keyboard — but their activation now reaches this handler by
|
|
320
|
+
// bubbling, so there is one code path and no double-fire. Anything that
|
|
321
|
+
// is its own action (rename, close, the name editor) stops the event.
|
|
322
|
+
onClick={() => !editing && onSelect?.(row)}
|
|
318
323
|
className={cn(
|
|
319
324
|
'group flex cursor-pointer flex-col gap-0.5 text-left transition-colors',
|
|
320
325
|
rowShapeClass(active === true),
|
|
@@ -337,7 +342,6 @@ function SessionRowItem({
|
|
|
337
342
|
) : (
|
|
338
343
|
<button
|
|
339
344
|
type='button'
|
|
340
|
-
onClick={() => onSelect?.(row)}
|
|
341
345
|
className={cn(
|
|
342
346
|
'min-w-0 flex-1 truncate text-left text-body-sm outline-none',
|
|
343
347
|
active ? 'font-medium text-fg-1' : 'text-fg-2',
|
|
@@ -365,7 +369,7 @@ function SessionRowItem({
|
|
|
365
369
|
<div className='flex items-center gap-1 text-label text-fg-4'>
|
|
366
370
|
<button
|
|
367
371
|
type='button'
|
|
368
|
-
|
|
372
|
+
tabIndex={-1}
|
|
369
373
|
className='min-w-0 flex-1 truncate text-left font-mono outline-none'>
|
|
370
374
|
{details.join(' · ')}
|
|
371
375
|
</button>
|
|
@@ -375,7 +379,10 @@ function SessionRowItem({
|
|
|
375
379
|
size='icon-sm'
|
|
376
380
|
aria-label='Rename session'
|
|
377
381
|
className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
|
|
378
|
-
onClick={() =>
|
|
382
|
+
onClick={(e) => {
|
|
383
|
+
e.stopPropagation()
|
|
384
|
+
setEditing(true)
|
|
385
|
+
}}>
|
|
379
386
|
<Pencil className='size-3 text-fg-3' />
|
|
380
387
|
</Button>
|
|
381
388
|
) : null}
|
|
@@ -385,7 +392,10 @@ function SessionRowItem({
|
|
|
385
392
|
size='icon-sm'
|
|
386
393
|
aria-label='Close session'
|
|
387
394
|
className='size-5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100'
|
|
388
|
-
onClick={() =>
|
|
395
|
+
onClick={(e) => {
|
|
396
|
+
e.stopPropagation()
|
|
397
|
+
onDelete(row)
|
|
398
|
+
}}>
|
|
389
399
|
<Trash2 className='size-3 text-fg-3' />
|
|
390
400
|
</Button>
|
|
391
401
|
) : null}
|