@workerdeck/ui 0.23.0 → 1.0.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/build/{SessionPanel-DUt2VzXG.mjs → SessionPanel-DDgBkHsi.mjs} +2151 -3357
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -23
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +5 -126
- package/build/workspace.mjs +11 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +3 -9
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +315 -996
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +104 -219
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +96 -32
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-BobynUo4.d.mts +0 -669
- package/build/SessionPanel-DUt2VzXG.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -3,21 +3,8 @@ import type { TranscriptItem, TranscriptState } from '@workerdeck/react'
|
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
import type { TerminalAffordances } from './affordances.tsx'
|
|
5
5
|
import { OpenSubagentAction, WithActions } from './affordances.tsx'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
FileRow,
|
|
9
|
-
NoticeRow,
|
|
10
|
-
ToolRunRow,
|
|
11
|
-
ThinkingRow,
|
|
12
|
-
ToolRow,
|
|
13
|
-
TurnResultRow,
|
|
14
|
-
UserRow,
|
|
15
|
-
WorkingRow,
|
|
16
|
-
blockNeedsBlank,
|
|
17
|
-
taskChildItems,
|
|
18
|
-
terminalBlocks,
|
|
19
|
-
type TaskBlock,
|
|
20
|
-
} from './items.tsx'
|
|
6
|
+
import { AssistantRow, FileRow, NoticeRow, ToolRunRow, ThinkingRow, ToolRow, TurnResultRow, UserRow, WorkingRow } from './items.tsx'
|
|
7
|
+
import { blockNeedsBlank, taskChildItems, terminalBlocks, type TaskBlock } from './blocks.ts'
|
|
21
8
|
import { usePulse } from '../agent/pulse.tsx'
|
|
22
9
|
import { Pressable, useRevealOnOpen } from './press.tsx'
|
|
23
10
|
import { taskBrief, taskBusy, taskFailed, taskSummary } from './tool-run.ts'
|
|
@@ -25,122 +12,61 @@ import { BRIEF_LINES } from './height.ts'
|
|
|
25
12
|
import { Blank, Row } from './row.tsx'
|
|
26
13
|
import { TerminalSurface } from './surface.tsx'
|
|
27
14
|
|
|
28
|
-
/**
|
|
29
|
-
* The transcript, drawn as a terminal.
|
|
30
|
-
*
|
|
31
|
-
* The whole of the layout is here and it is four lines long, which is the point:
|
|
32
|
-
* items become row blocks, a blank line goes between blocks that do not belong
|
|
33
|
-
* together, and the working line is one more row at the end. There is no gap
|
|
34
|
-
* scale, no density knob and no variant branch — a terminal has one line height
|
|
35
|
-
* and one type size, and everything the theme can express is expressed in which
|
|
36
|
-
* rows exist and what marker each carries.
|
|
37
|
-
*
|
|
38
|
-
* Not virtualized yet: the row blocks are the part that has to be right first,
|
|
39
|
-
* and the existing `Transcript` already owns a hard-won virtualizer plus the
|
|
40
|
-
* scroll-regime rules it needs. This renders into that shell when the rows are
|
|
41
|
-
* settled, rather than growing a second copy of that logic.
|
|
42
|
-
*/
|
|
43
15
|
export interface TerminalTranscriptProps {
|
|
44
16
|
state: TranscriptState
|
|
45
|
-
/** Builds the download URL for a delivered file. */
|
|
46
17
|
fileUrl?: (path: string) => string
|
|
47
|
-
/** Cell metrics, in whole pixels. See {@link TerminalSurface}. */
|
|
48
18
|
fontSize?: number
|
|
49
19
|
lineHeight?: number
|
|
50
|
-
/** The pointer affordances a real terminal has no way to offer — hover fill,
|
|
51
|
-
* hover-revealed copy. `false` for none. See {@link TerminalAffordances}. */
|
|
52
20
|
affordances?: TerminalAffordances | boolean
|
|
53
21
|
className?: string
|
|
54
22
|
}
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
* One transcript item as terminal rows. Exported because the virtualized shell
|
|
58
|
-
* in `agent/Transcript.tsx` renders rows through it: the theme owns how a row
|
|
59
|
-
* looks, that component owns which rows are mounted, and neither needs a copy
|
|
60
|
-
* of the other.
|
|
61
|
-
*/
|
|
62
|
-
export function TerminalItemView({
|
|
63
|
-
item,
|
|
64
|
-
fileUrl,
|
|
65
|
-
}: {
|
|
66
|
-
item: TranscriptItem
|
|
67
|
-
fileUrl?: (path: string) => string
|
|
68
|
-
}) {
|
|
24
|
+
export function TerminalItemView({ item, fileUrl }: { item: TranscriptItem; fileUrl?: (path: string) => string }) {
|
|
69
25
|
switch (item.kind) {
|
|
70
|
-
case 'user':
|
|
26
|
+
case 'user': {
|
|
71
27
|
return <UserRow item={item} />
|
|
72
|
-
|
|
28
|
+
}
|
|
29
|
+
case 'assistant_text': {
|
|
73
30
|
return <AssistantRow item={item} />
|
|
74
|
-
|
|
31
|
+
}
|
|
32
|
+
case 'thinking': {
|
|
75
33
|
return <ThinkingRow item={item} />
|
|
76
|
-
|
|
34
|
+
}
|
|
35
|
+
case 'tool_call': {
|
|
77
36
|
return <ToolRow item={item} />
|
|
78
|
-
|
|
37
|
+
}
|
|
38
|
+
case 'turn_result': {
|
|
79
39
|
return <TurnResultRow item={item} />
|
|
80
|
-
|
|
40
|
+
}
|
|
41
|
+
case 'notice': {
|
|
81
42
|
return <NoticeRow item={item} />
|
|
82
|
-
|
|
43
|
+
}
|
|
44
|
+
case 'file_delivered': {
|
|
83
45
|
return <FileRow item={item} href={fileUrl?.(item.path)} />
|
|
84
|
-
|
|
46
|
+
}
|
|
47
|
+
default: {
|
|
85
48
|
return null
|
|
49
|
+
}
|
|
86
50
|
}
|
|
87
51
|
}
|
|
88
52
|
|
|
89
|
-
/**
|
|
90
|
-
* A `Task` and everything the subagent it spawned produced, as one row.
|
|
91
|
-
*
|
|
92
|
-
* The same claim the tool-run fold makes, and a stronger one: a subagent is
|
|
93
|
-
* *sixty* rows of somebody else's working — a brief, a dozen greps, its own
|
|
94
|
-
* thinking — and none of it is what you came back to read. What you came back
|
|
95
|
-
* to read is the report, and the report is the model's next sentence. So the
|
|
96
|
-
* whole frame collapses to one line saying what was asked and how big the
|
|
97
|
-
* answer was, and opens in full the moment it is the thing you want.
|
|
98
|
-
*
|
|
99
|
-
* **Always collapsed when unmounted**, and that is load-bearing rather than
|
|
100
|
-
* tidy: `height.ts` predicts this row as exactly one wrapped `taskSummary`, and
|
|
101
|
-
* expansion is component-local state that dies with the row. A row auto-opening
|
|
102
|
-
* because its subagent happens to be running would make its own height
|
|
103
|
-
* unpredictable — which is why the live signal is *in* the collapsed line (the
|
|
104
|
-
* pulse, and a count that climbs) rather than in an open block.
|
|
105
|
-
*
|
|
106
|
-
* The children are the theme's ordinary rows, stepped in behind a rule, and
|
|
107
|
-
* they fold among themselves: a subagent's consecutive tool calls are as much
|
|
108
|
-
* an aside inside its frame as they are in the main thread.
|
|
109
|
-
*/
|
|
110
|
-
/**
|
|
111
|
-
* **What the agent was asked** — the sub-agent's brief, clipped to
|
|
112
|
-
* {@link BRIEF_LINES} and pressable for the whole of it.
|
|
113
|
-
*
|
|
114
|
-
* It leads the takeover's frame and the inline task expansion alike, because
|
|
115
|
-
* both are answering the same question and an answer without its question is
|
|
116
|
-
* half a transcript. This is the one row here built from something other than
|
|
117
|
-
* the stream, and it exists for the case the stream does not cover: a
|
|
118
|
-
* **background** agent forwards no brief, where a foreground `Task` forwards a
|
|
119
|
-
* real nested user item that renders as an ordinary prompt row. The callers
|
|
120
|
-
* splice this in only when that row is absent. Codex draws nothing either way —
|
|
121
|
-
* its spawn message is encrypted on the wire, so there is no brief to give.
|
|
122
|
-
*
|
|
123
|
-
* `>` and blue, the prompt's own marker and colour, because that is what this
|
|
124
|
-
* is: somebody's instruction, one level in. The clip is `line-clamp`, which
|
|
125
|
-
* cuts on the same wrapped lines `briefPx` counts — one rule, so the reserved
|
|
126
|
-
* height and the drawn height cannot disagree.
|
|
127
|
-
*/
|
|
128
53
|
export function BriefRow({ text, terminal }: { text: string; terminal?: boolean }) {
|
|
129
54
|
const [open, setOpen] = useState(false)
|
|
130
55
|
if (!terminal) {
|
|
131
56
|
return (
|
|
132
|
-
<div data-slot=
|
|
57
|
+
<div data-slot="brief" className="px-4 py-2 text-body-sm whitespace-pre-wrap text-fg-2">
|
|
133
58
|
{text}
|
|
134
59
|
</div>
|
|
135
60
|
)
|
|
136
61
|
}
|
|
137
62
|
return (
|
|
138
|
-
<div data-slot=
|
|
63
|
+
<div data-slot="brief">
|
|
139
64
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
140
|
-
<Row glyph=
|
|
65
|
+
<Row glyph=">" glyphTone="blue" tone="dim">
|
|
141
66
|
<span
|
|
142
67
|
className={cn('whitespace-pre-wrap', !open && 'term-brief-clip')}
|
|
143
|
-
style={open ? undefined : { WebkitLineClamp: BRIEF_LINES }}
|
|
68
|
+
style={open ? undefined : { WebkitLineClamp: BRIEF_LINES }}
|
|
69
|
+
>
|
|
144
70
|
{text}
|
|
145
71
|
</span>
|
|
146
72
|
</Row>
|
|
@@ -156,17 +82,11 @@ export function TaskRow({
|
|
|
156
82
|
}: {
|
|
157
83
|
block: TaskBlock
|
|
158
84
|
fileUrl?: (path: string) => string
|
|
159
|
-
/** Take over the panel with this sub-agent's own frame. Absent draws no
|
|
160
|
-
* affordance at all — the plain renderer has no surface to take over, and an
|
|
161
|
-
* action that opens nothing is worse than no action. */
|
|
162
85
|
onOpenSubagent?: (toolUseId: string) => void
|
|
163
86
|
}) {
|
|
164
87
|
const [open, setOpen] = useState(false)
|
|
165
88
|
const reveal = useRevealOnOpen(open)
|
|
166
89
|
const children = useMemo(() => taskChildItems(block), [block])
|
|
167
|
-
// Only when the sub-agent's own stream carries no brief — a foreground Task
|
|
168
|
-
// forwards one as a real user item, and two spellings of one instruction is
|
|
169
|
-
// worse than none. See `taskBrief`.
|
|
170
90
|
const brief = children.some((item) => item.kind === 'user') ? undefined : taskBrief(block.task)
|
|
171
91
|
const busy = taskBusy(block.task, children)
|
|
172
92
|
const failed = taskFailed(block.task)
|
|
@@ -175,64 +95,30 @@ export function TaskRow({
|
|
|
175
95
|
const row = (
|
|
176
96
|
<div ref={reveal} className={open ? 'term-open' : undefined}>
|
|
177
97
|
<Pressable onPress={() => setOpen((v) => !v)} expanded={open}>
|
|
178
|
-
{
|
|
179
|
-
but delegating a piece of the work is something the model *did*, and
|
|
180
|
-
the row stands for the whole of it. The body is `taskSummary`
|
|
181
|
-
verbatim — it is the string `height.ts` wraps to size this row, and
|
|
182
|
-
a second spelling here would be a second height. */}
|
|
183
|
-
{/* Green means sub-agent, and it means it here for the same reason it
|
|
184
|
-
means it on the rail: every other colour is spoken for — blue is
|
|
185
|
-
you, white is the answer, red is an alarm, magenta is your bookmark,
|
|
186
|
-
yellow is the session waiting on you (see `terminal.css`). The
|
|
187
|
-
*body* is green and the marker is not: a green glyph already means
|
|
188
|
-
"wrote to the workspace" a few rows down, and one colour cannot mean
|
|
189
|
-
two things in the same gutter. Failure still outranks it — an alarm
|
|
190
|
-
is not a category. */}
|
|
191
|
-
<Row
|
|
192
|
-
glyph={busy ? pulse : '●'}
|
|
193
|
-
glyphTone={failed ? 'red' : busy ? 'mark' : 'dim'}
|
|
194
|
-
tone={failed ? 'red' : 'green'}>
|
|
98
|
+
<Row glyph={busy ? pulse : '●'} glyphTone={failed ? 'red' : busy ? 'mark' : 'dim'} tone={failed ? 'red' : 'green'}>
|
|
195
99
|
{taskSummary(block.task, children)}
|
|
196
100
|
</Row>
|
|
197
101
|
</Pressable>
|
|
198
102
|
{open ? (
|
|
199
|
-
|
|
200
|
-
// sits on the open block's wash, where that border token is invisible,
|
|
201
|
-
// and its 14px would take every nested marker off the cell grid.
|
|
202
|
-
<div className='term-nested'>
|
|
203
|
-
{/* The brief leads the children for the same reason it leads the
|
|
204
|
-
frame: the instruction, then the work. */}
|
|
103
|
+
<div className="term-nested">
|
|
205
104
|
{brief ? <BriefRow text={brief} terminal /> : null}
|
|
206
105
|
{block.children.map((leaf, index) => (
|
|
207
106
|
<Fragment key={leaf.key}>
|
|
208
107
|
{index > 0 && blockNeedsBlank(block.children[index - 1]!, leaf) ? <Blank /> : null}
|
|
209
|
-
{'run' in leaf ?
|
|
210
|
-
<ToolRunRow items={leaf.run} />
|
|
211
|
-
) : (
|
|
212
|
-
<TerminalItemView item={leaf.item} fileUrl={fileUrl} />
|
|
213
|
-
)}
|
|
108
|
+
{'run' in leaf ? <ToolRunRow items={leaf.run} /> : <TerminalItemView item={leaf.item} fileUrl={fileUrl} />}
|
|
214
109
|
</Fragment>
|
|
215
110
|
))}
|
|
216
111
|
</div>
|
|
217
112
|
) : null}
|
|
218
113
|
</div>
|
|
219
114
|
)
|
|
220
|
-
// Wrapped only when there is somewhere to go. `WithActions` is a no-op when
|
|
221
|
-
// affordances are off, but wrapping unconditionally would still put an
|
|
222
|
-
// "open" glyph on a renderer that cannot honour it.
|
|
223
115
|
return onOpenSubagent === undefined ? (
|
|
224
116
|
row
|
|
225
117
|
) : (
|
|
226
|
-
<WithActions actions={<OpenSubagentAction onOpen={() => onOpenSubagent(block.task.id)} />}>
|
|
227
|
-
{row}
|
|
228
|
-
</WithActions>
|
|
118
|
+
<WithActions actions={<OpenSubagentAction onOpen={() => onOpenSubagent(block.task.id)} />}>{row}</WithActions>
|
|
229
119
|
)
|
|
230
120
|
}
|
|
231
121
|
|
|
232
|
-
/** When the current run began — the clock the working line counts from. Held
|
|
233
|
-
* here, not in the row, because the row comes and goes within a single turn (it
|
|
234
|
-
* hides the moment text streams) and a clock restarting at every tool call
|
|
235
|
-
* would be measuring the wrong thing. */
|
|
236
122
|
function useRunStart(status: TranscriptState['status']): number | undefined {
|
|
237
123
|
const running = status === 'running' || status === 'starting'
|
|
238
124
|
const [startedAt, setStartedAt] = useState<number | undefined>(undefined)
|
|
@@ -242,25 +128,24 @@ function useRunStart(status: TranscriptState['status']): number | undefined {
|
|
|
242
128
|
return running ? startedAt : undefined
|
|
243
129
|
}
|
|
244
130
|
|
|
245
|
-
/** Is the model between outputs? Only then does the working line show — while
|
|
246
|
-
* text is streaming, the text itself is the evidence. */
|
|
247
131
|
function working(state: TranscriptState): boolean {
|
|
248
|
-
if (state.status !== 'running' && state.status !== 'starting')
|
|
132
|
+
if (state.status !== 'running' && state.status !== 'starting') {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
249
135
|
const last = state.items.at(-1)
|
|
250
|
-
if (!last)
|
|
251
|
-
|
|
252
|
-
|
|
136
|
+
if (!last) {
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
if (last.kind === 'assistant_text' && last.streaming) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
if (last.kind === 'thinking' && last.id === 'streaming-thinking') {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
253
145
|
return true
|
|
254
146
|
}
|
|
255
147
|
|
|
256
|
-
export function TerminalTranscript({
|
|
257
|
-
state,
|
|
258
|
-
fileUrl,
|
|
259
|
-
fontSize,
|
|
260
|
-
lineHeight,
|
|
261
|
-
affordances,
|
|
262
|
-
className,
|
|
263
|
-
}: TerminalTranscriptProps) {
|
|
148
|
+
export function TerminalTranscript({ state, fileUrl, fontSize, lineHeight, affordances, className }: TerminalTranscriptProps) {
|
|
264
149
|
const runStartedAt = useRunStart(state.status)
|
|
265
150
|
const blocks = useMemo(() => terminalBlocks(state.items), [state.items])
|
|
266
151
|
return (
|
|
@@ -268,10 +153,9 @@ export function TerminalTranscript({
|
|
|
268
153
|
fontSize={fontSize}
|
|
269
154
|
lineHeight={lineHeight}
|
|
270
155
|
affordances={affordances}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
className={cn('term-transcript', className)}>
|
|
156
|
+
bleed="1ch"
|
|
157
|
+
className={cn('term-transcript', className)}
|
|
158
|
+
>
|
|
275
159
|
{blocks.map((block, index) => (
|
|
276
160
|
<Fragment key={block.key}>
|
|
277
161
|
{index > 0 && blockNeedsBlank(blocks[index - 1]!, block) ? <Blank /> : null}
|
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
import { createContext, useContext, useState, type ReactNode } from 'react'
|
|
2
|
+
import { copyText } from '../../lib/clipboard.ts'
|
|
2
3
|
import { cn } from '../../lib/utils.ts'
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* The things a terminal cannot do, and this can.
|
|
6
|
-
*
|
|
7
|
-
* The theme's whole discipline is that it renders like a CLI — but it is not a
|
|
8
|
-
* CLI, and refusing every affordance a pointer makes possible would be cosplay
|
|
9
|
-
* rather than design. A terminal cannot highlight the row under your cursor and
|
|
10
|
-
* cannot put a copy button on a block of output; a web view can do both for free
|
|
11
|
-
* and they are genuinely useful.
|
|
12
|
-
*
|
|
13
|
-
* The rule that keeps this honest is that **each one costs no layout**. A hover
|
|
14
|
-
* fill is a background. An action button is an overlay at the row's right edge,
|
|
15
|
-
* one line tall, absolutely positioned so it displaces nothing. Turn them all
|
|
16
|
-
* off and every glyph is on exactly the same cell it was on — which is what
|
|
17
|
-
* makes `off` a real option rather than a degraded mode, and why it is the mode
|
|
18
|
-
* a host projecting to a real terminal would choose.
|
|
19
|
-
*
|
|
20
|
-
* Off by nothing in particular: both default **on**, because the surface these
|
|
21
|
-
* render on is a browser and pretending otherwise helps nobody. A host that
|
|
22
|
-
* wants the pure article passes `affordances={false}`.
|
|
23
|
-
*/
|
|
24
5
|
export type TerminalAffordances = {
|
|
25
|
-
/** Fill the row under the pointer, on anything pressable. */
|
|
26
6
|
hover?: boolean
|
|
27
|
-
/** Reveal a row's actions (copy, and whatever a row adds) on hover or focus. */
|
|
28
7
|
actions?: boolean
|
|
29
8
|
}
|
|
30
9
|
|
|
@@ -36,88 +15,83 @@ export function useAffordances(): Required<TerminalAffordances> {
|
|
|
36
15
|
return useContext(AffordanceContext)
|
|
37
16
|
}
|
|
38
17
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (value ===
|
|
44
|
-
|
|
18
|
+
export function resolveAffordances(value: TerminalAffordances | boolean | undefined): Required<TerminalAffordances> {
|
|
19
|
+
if (value === false) {
|
|
20
|
+
return { hover: false, actions: false }
|
|
21
|
+
}
|
|
22
|
+
if (value === true || value === undefined) {
|
|
23
|
+
return DEFAULTS
|
|
24
|
+
}
|
|
45
25
|
return { ...DEFAULTS, ...value }
|
|
46
26
|
}
|
|
47
27
|
|
|
48
|
-
export function AffordanceProvider({
|
|
49
|
-
value,
|
|
50
|
-
children,
|
|
51
|
-
}: {
|
|
52
|
-
value: Required<TerminalAffordances>
|
|
53
|
-
children: ReactNode
|
|
54
|
-
}) {
|
|
28
|
+
export function AffordanceProvider({ value, children }: { value: Required<TerminalAffordances>; children: ReactNode }) {
|
|
55
29
|
return <AffordanceContext.Provider value={value}>{children}</AffordanceContext.Provider>
|
|
56
30
|
}
|
|
57
31
|
|
|
58
|
-
|
|
59
|
-
* A block that reveals its actions on hover.
|
|
60
|
-
*
|
|
61
|
-
* Wraps rather than decorates because the actions belong to the *block* — a
|
|
62
|
-
* message is many rows and its copy button belongs at the top right of all of
|
|
63
|
-
* them, not on whichever row the pointer happens to be over.
|
|
64
|
-
*/
|
|
65
|
-
export function WithActions({
|
|
66
|
-
actions,
|
|
67
|
-
children,
|
|
68
|
-
className,
|
|
69
|
-
}: {
|
|
70
|
-
actions: ReactNode
|
|
71
|
-
children: ReactNode
|
|
72
|
-
className?: string
|
|
73
|
-
}) {
|
|
32
|
+
export function WithActions({ actions, children, className }: { actions: ReactNode; children: ReactNode; className?: string }) {
|
|
74
33
|
const { actions: enabled } = useAffordances()
|
|
75
|
-
if (!enabled)
|
|
34
|
+
if (!enabled) {
|
|
35
|
+
return <>{children}</>
|
|
36
|
+
}
|
|
76
37
|
return (
|
|
77
38
|
<div className={cn('term-hoverable', className)}>
|
|
78
39
|
{children}
|
|
79
|
-
<div className=
|
|
40
|
+
<div className="term-actions">{actions}</div>
|
|
80
41
|
</div>
|
|
81
42
|
)
|
|
82
43
|
}
|
|
83
44
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
45
|
+
// Bookmarks are a host concern (which items, where they persist); the transcript only needs
|
|
46
|
+
// membership and a toggle. A missing provider renders no action at all, so embeddings that
|
|
47
|
+
// never wire bookmarks pay nothing — the same contract AffordanceContext has.
|
|
48
|
+
export type BookmarkHandle = {
|
|
49
|
+
has: (itemId: string) => boolean
|
|
50
|
+
toggle: (itemId: string) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BookmarkContext = createContext<BookmarkHandle | undefined>(undefined)
|
|
54
|
+
|
|
55
|
+
export function BookmarkProvider({ value, children }: { value: BookmarkHandle | undefined; children: ReactNode }) {
|
|
56
|
+
return <BookmarkContext.Provider value={value}>{children}</BookmarkContext.Provider>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function BookmarkAction({ id }: { id: string }) {
|
|
60
|
+
const handle = useContext(BookmarkContext)
|
|
61
|
+
if (!handle) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
const active = handle.has(id)
|
|
65
|
+
const label = active ? 'Remove bookmark' : 'Bookmark'
|
|
66
|
+
return (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className="term-action"
|
|
70
|
+
title={label}
|
|
71
|
+
aria-label={label}
|
|
72
|
+
aria-pressed={active}
|
|
73
|
+
onClick={(event) => {
|
|
74
|
+
event.stopPropagation()
|
|
75
|
+
handle.toggle(id)
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{active ? '★' : '☆'}
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function OpenSubagentAction({ onOpen, label = 'Open sub-agent' }: { onOpen: () => void; label?: string }) {
|
|
110
84
|
return (
|
|
111
85
|
<button
|
|
112
|
-
type=
|
|
113
|
-
className=
|
|
86
|
+
type="button"
|
|
87
|
+
className="term-action"
|
|
114
88
|
title={label}
|
|
115
89
|
aria-label={label}
|
|
116
90
|
onClick={(event) => {
|
|
117
|
-
// The row underneath expands; opening is not expanding.
|
|
118
91
|
event.stopPropagation()
|
|
119
92
|
onOpen()
|
|
120
|
-
}}
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
121
95
|
⤢
|
|
122
96
|
</button>
|
|
123
97
|
)
|
|
@@ -127,25 +101,21 @@ export function CopyAction({ text, label = 'Copy' }: { text: string; label?: str
|
|
|
127
101
|
const [copied, setCopied] = useState(false)
|
|
128
102
|
return (
|
|
129
103
|
<button
|
|
130
|
-
type=
|
|
131
|
-
className=
|
|
104
|
+
type="button"
|
|
105
|
+
className="term-action"
|
|
132
106
|
title={label}
|
|
133
107
|
aria-label={label}
|
|
134
108
|
onClick={(event) => {
|
|
135
|
-
// The row underneath is usually pressable (a tool call expands); copying
|
|
136
|
-
// is not expanding.
|
|
137
109
|
event.stopPropagation()
|
|
138
|
-
void
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.catch(() => {})
|
|
148
|
-
}}>
|
|
110
|
+
void copyText(text).then((ok) => {
|
|
111
|
+
if (!ok) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
setCopied(true)
|
|
115
|
+
setTimeout(() => setCopied(false), 1200)
|
|
116
|
+
})
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
149
119
|
{copied ? '✓' : '⧉'}
|
|
150
120
|
</button>
|
|
151
121
|
)
|