@workerdeck/ui 2.15.0 → 3.1.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 +6 -5
- package/build/{SessionPanel-BVKqPs_3.d.mts → SessionPanel-BNNc8grK.d.mts} +18 -16
- package/build/index.d.mts +194 -49
- package/build/index.mjs +562 -260
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +170 -21
- package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-C6Fh5zdx.mjs} +3312 -2758
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +3 -3
- package/build/workspace.mjs +3 -3
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/ApprovalPrompts.tsx +87 -0
- package/src/components/agent/Composer.tsx +25 -47
- package/src/components/agent/ContextDialog.tsx +75 -67
- package/src/components/agent/FrameStrip.tsx +74 -0
- package/src/components/agent/HostFilesDialog.tsx +3 -2
- package/src/components/agent/McpDialog.tsx +112 -78
- package/src/components/agent/PermissionPrompt.tsx +22 -17
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Response.tsx +8 -1
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +131 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +29 -6
- package/src/components/agent/SessionPanel.tsx +249 -498
- package/src/components/agent/SessionSteps.tsx +98 -14
- package/src/components/agent/SessionWorkspace.tsx +3 -3
- package/src/components/agent/ShellStrip.tsx +43 -36
- package/src/components/agent/ShellTerminal.tsx +1 -1
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/ToolCallCard.tsx +18 -41
- package/src/components/agent/Transcript.tsx +111 -93
- package/src/components/agent/TranscriptItemView.tsx +80 -67
- package/src/components/agent/TranscriptRows.tsx +4 -2
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/image-viewer.tsx +326 -0
- package/src/components/agent/markdown-image.tsx +139 -0
- package/src/components/agent/scrubber-marks.ts +33 -11
- package/src/components/agent/scrubber-peek.tsx +122 -0
- package/src/components/agent/session-panel-providers.tsx +41 -0
- package/src/components/agent/shell-actions.tsx +38 -1
- package/src/components/agent/tool-result-image.tsx +40 -0
- package/src/components/agent/transcript-variant.tsx +3 -21
- package/src/components/agent/use-catch-up.ts +20 -0
- package/src/components/agent/use-frame.ts +88 -0
- package/src/components/agent/use-host-image.ts +81 -0
- package/src/components/agent/use-shell-frame.ts +12 -70
- package/src/components/agent/use-subagent-frame.ts +19 -75
- package/src/components/prompt-area/prompt-area-engine.ts +21 -20
- package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
- package/src/components/prompt-area/use-prompt-area-keydown.ts +4 -2
- package/src/components/terminal/PermissionPrompt.tsx +31 -28
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +23 -89
- package/src/components/terminal/affordances.tsx +100 -74
- package/src/components/terminal/height.ts +48 -32
- package/src/components/terminal/image-box.ts +31 -0
- package/src/components/terminal/items.tsx +70 -30
- package/src/components/terminal/markdown.tsx +4 -1
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/terminal/shell-row.ts +17 -3
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +65 -12
- package/src/lib/file-link.ts +3 -16
- package/src/lib/permission-prompt.ts +57 -0
- package/src/lib/question-answers.ts +65 -0
- package/src/lib/shell-request.ts +115 -0
- package/src/lib/tool-icon.ts +13 -1
- package/src/styles/terminal.css +129 -22
- package/src/styles/theme.css +4 -0
- package/build/use-path-links-BgWXmfAS.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
|
@@ -1,28 +1,52 @@
|
|
|
1
|
-
import { ArrowRight, Check, CircleAlert } from 'lucide-react'
|
|
2
|
-
import {
|
|
3
|
-
import type { SessionInfo, ShellInfo, SubagentDisplay, SubagentInfo } from '@workerdeck/protocol'
|
|
1
|
+
import { ArrowRight, Check, Circle, CircleAlert } from 'lucide-react'
|
|
2
|
+
import { displayedTasks, isAgentRecord, subagentLabel, visibleShells, visibleSubagents } from '@workerdeck/protocol'
|
|
3
|
+
import type { SessionInfo, SessionTask, ShellInfo, StepDisplay, SubagentDisplay, SubagentInfo } from '@workerdeck/protocol'
|
|
4
4
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
5
5
|
import { cn } from '../../lib/utils.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { AgentWriteIcon } from '../terminal/affordances.tsx'
|
|
7
|
+
import {
|
|
8
|
+
SHELL_AGENT_WRITE_NOTE,
|
|
9
|
+
SHELL_GLYPH,
|
|
10
|
+
SHELL_KILL_GLYPH,
|
|
11
|
+
shellAgentWriteLabel,
|
|
12
|
+
shellGrantable,
|
|
13
|
+
shellInfoFailed,
|
|
14
|
+
shellInfoLabel,
|
|
15
|
+
shellInfoStatusText,
|
|
16
|
+
shellTitle,
|
|
17
|
+
} from '../terminal/shell-row.ts'
|
|
7
18
|
|
|
8
|
-
export type StepKind = 'agent' | 'shell'
|
|
19
|
+
export type StepKind = 'agent' | 'shell' | 'task'
|
|
9
20
|
|
|
10
21
|
export type Step = {
|
|
11
22
|
key: string
|
|
12
23
|
kind: StepKind
|
|
13
24
|
label: string
|
|
14
25
|
noun: string
|
|
15
|
-
state: 'done' | 'running' | 'failed'
|
|
26
|
+
state: 'done' | 'running' | 'failed' | 'pending'
|
|
16
27
|
detail?: string
|
|
17
28
|
title: string
|
|
18
29
|
onSelect: () => void
|
|
19
30
|
onKill?: () => void
|
|
31
|
+
killLabel?: string
|
|
32
|
+
agentWrite?: { granted: boolean; label: string; toggle: () => void }
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
export type ShellStepOptions = {
|
|
23
36
|
now: number
|
|
37
|
+
show?: StepDisplay
|
|
24
38
|
onSelect: (shellId: string) => void
|
|
25
39
|
onKill?: (shellId: string) => void
|
|
40
|
+
// Offered only where the session's agent holds the shell write tools.
|
|
41
|
+
onAgentWrite?: (shellId: string, enabled: boolean) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TaskStepOptions = {
|
|
45
|
+
show: StepDisplay
|
|
46
|
+
// Whether the session is still working: a checklist item left in progress after the turn ended is not running.
|
|
47
|
+
live: boolean
|
|
48
|
+
onSelect: (task: SessionTask) => void
|
|
49
|
+
onStop?: (toolUseId: string) => void
|
|
26
50
|
}
|
|
27
51
|
|
|
28
52
|
export function sessionSteps(
|
|
@@ -30,6 +54,7 @@ export function sessionSteps(
|
|
|
30
54
|
onSelect: (toolUseId: string) => void,
|
|
31
55
|
show: SubagentDisplay = 'all',
|
|
32
56
|
shells?: ShellStepOptions,
|
|
57
|
+
tasks?: TaskStepOptions,
|
|
33
58
|
): Step[] {
|
|
34
59
|
const agents: Step[] = visibleSubagents(info, show)
|
|
35
60
|
.filter(isAgentRecord)
|
|
@@ -43,13 +68,33 @@ export function sessionSteps(
|
|
|
43
68
|
title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,
|
|
44
69
|
onSelect: () => onSelect(sub.toolUseId),
|
|
45
70
|
}))
|
|
71
|
+
const taskSteps = tasks === undefined ? [] : displayedTasks(info, tasks.show).map((task) => taskStep(task, tasks))
|
|
46
72
|
if (shells === undefined) {
|
|
47
|
-
return agents
|
|
73
|
+
return [...agents, ...taskSteps]
|
|
74
|
+
}
|
|
75
|
+
const grants = info.shellAgentWrite !== undefined
|
|
76
|
+
const shellSteps = visibleShells(info, shells.show ?? 'active', shells.now).map((shell) => shellStep(shell, shells, grants))
|
|
77
|
+
return [...agents, ...taskSteps, ...shellSteps]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function taskStep(task: SessionTask, options: TaskStepOptions): Step {
|
|
81
|
+
const stalled = task.source === 'checklist' && task.state === 'running' && !options.live
|
|
82
|
+
const toolUseId = task.toolUseId
|
|
83
|
+
return {
|
|
84
|
+
key: task.key,
|
|
85
|
+
kind: 'task',
|
|
86
|
+
label: task.label,
|
|
87
|
+
noun: 'task',
|
|
88
|
+
state: stalled ? 'pending' : task.state,
|
|
89
|
+
detail: task.detail,
|
|
90
|
+
title: stalled ? `${task.label} · left in progress` : task.label,
|
|
91
|
+
onSelect: () => options.onSelect(task),
|
|
92
|
+
onKill: task.stoppable && toolUseId !== undefined && options.onStop ? () => options.onStop?.(toolUseId) : undefined,
|
|
93
|
+
killLabel: 'Stop this task',
|
|
48
94
|
}
|
|
49
|
-
return [...agents, ...promotedShells(info, shells.now).map((shell) => shellStep(shell, shells))]
|
|
50
95
|
}
|
|
51
96
|
|
|
52
|
-
function shellStep(shell: ShellInfo, options: ShellStepOptions): Step {
|
|
97
|
+
function shellStep(shell: ShellInfo, options: ShellStepOptions, grants: boolean): Step {
|
|
53
98
|
const status = shellInfoStatusText(shell)
|
|
54
99
|
const running = shell.status === 'running'
|
|
55
100
|
return {
|
|
@@ -58,10 +103,19 @@ function shellStep(shell: ShellInfo, options: ShellStepOptions): Step {
|
|
|
58
103
|
label: shellInfoLabel(shell),
|
|
59
104
|
noun: 'shell',
|
|
60
105
|
state: shellInfoFailed(shell) ? 'failed' : running ? 'running' : 'done',
|
|
61
|
-
detail: running ? undefined : status,
|
|
106
|
+
detail: running ? (shell.agentWrite === true ? SHELL_AGENT_WRITE_NOTE : undefined) : status,
|
|
62
107
|
title: `${shellTitle(shell)} · ${status}`,
|
|
63
108
|
onSelect: () => options.onSelect(shell.id),
|
|
64
109
|
onKill: running && options.onKill ? () => options.onKill?.(shell.id) : undefined,
|
|
110
|
+
killLabel: 'Kill this shell',
|
|
111
|
+
agentWrite:
|
|
112
|
+
grants && options.onAgentWrite && shellGrantable(shell)
|
|
113
|
+
? {
|
|
114
|
+
granted: shell.agentWrite === true,
|
|
115
|
+
label: shellAgentWriteLabel(shell),
|
|
116
|
+
toggle: () => options.onAgentWrite?.(shell.id, shell.agentWrite !== true),
|
|
117
|
+
}
|
|
118
|
+
: undefined,
|
|
65
119
|
}
|
|
66
120
|
}
|
|
67
121
|
|
|
@@ -80,7 +134,16 @@ function stepState(status: SubagentInfo['status']): Step['state'] {
|
|
|
80
134
|
}
|
|
81
135
|
|
|
82
136
|
export function StepRow({ step, active = false, onSelect }: { step: Step; active?: boolean; onSelect: () => void }) {
|
|
83
|
-
const body =
|
|
137
|
+
const body =
|
|
138
|
+
step.state === 'failed'
|
|
139
|
+
? 'text-danger'
|
|
140
|
+
: step.kind === 'shell'
|
|
141
|
+
? 'text-[var(--wd-shell-accent)]'
|
|
142
|
+
: step.kind === 'task'
|
|
143
|
+
? step.state === 'pending'
|
|
144
|
+
? 'text-fg-4'
|
|
145
|
+
: 'text-fg-2'
|
|
146
|
+
: 'text-success'
|
|
84
147
|
return (
|
|
85
148
|
<div className={cn('flex w-full items-center rounded-[4px] pr-1.5', active ? 'bg-row-selected' : 'hover:bg-row-active', body)}>
|
|
86
149
|
<button
|
|
@@ -96,13 +159,31 @@ export function StepRow({ step, active = false, onSelect }: { step: Step; active
|
|
|
96
159
|
<StepIcon step={step} />
|
|
97
160
|
<span className="min-w-0 flex-1 truncate">{step.label}</span>
|
|
98
161
|
{step.detail ? <span className="shrink-0 tabular-nums text-fg-4">{step.detail}</span> : null}
|
|
99
|
-
<ArrowRight className="size-3.5 shrink-0 text-fg-4" />
|
|
162
|
+
{step.kind === 'agent' ? <ArrowRight className="size-3.5 shrink-0 text-fg-4" /> : null}
|
|
100
163
|
</button>
|
|
164
|
+
{step.agentWrite ? (
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
aria-label={step.agentWrite.label}
|
|
168
|
+
aria-pressed={step.agentWrite.granted}
|
|
169
|
+
title={step.agentWrite.label}
|
|
170
|
+
onClick={(e) => {
|
|
171
|
+
e.stopPropagation()
|
|
172
|
+
step.agentWrite?.toggle()
|
|
173
|
+
}}
|
|
174
|
+
className={cn(
|
|
175
|
+
'shrink-0 px-1 text-micro leading-none outline-none',
|
|
176
|
+
step.agentWrite.granted ? 'text-warning' : 'text-fg-4 hover:text-fg-1',
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
<AgentWriteIcon granted={step.agentWrite.granted} className="size-3.5" />
|
|
180
|
+
</button>
|
|
181
|
+
) : null}
|
|
101
182
|
{step.onKill ? (
|
|
102
183
|
<button
|
|
103
184
|
type="button"
|
|
104
|
-
aria-label={
|
|
105
|
-
title=
|
|
185
|
+
aria-label={`${step.kind === 'task' ? 'Stop' : 'Kill'} ${step.label}`}
|
|
186
|
+
title={step.killLabel}
|
|
106
187
|
onClick={(e) => {
|
|
107
188
|
e.stopPropagation()
|
|
108
189
|
step.onKill?.()
|
|
@@ -127,6 +208,9 @@ function StepIcon({ step }: { step: Step }) {
|
|
|
127
208
|
case 'failed': {
|
|
128
209
|
return <CircleAlert className="size-[11px] shrink-0" />
|
|
129
210
|
}
|
|
211
|
+
case 'pending': {
|
|
212
|
+
return <Circle className="size-[11px] shrink-0" />
|
|
213
|
+
}
|
|
130
214
|
default: {
|
|
131
215
|
return <Check className="size-[11px] shrink-0" />
|
|
132
216
|
}
|
|
@@ -17,8 +17,8 @@ export interface SessionWorkspaceProps {
|
|
|
17
17
|
sessionId: string | undefined
|
|
18
18
|
header?: SessionPanelProps['header']
|
|
19
19
|
transcriptVariant?: SessionPanelProps['transcriptVariant']
|
|
20
|
-
transcriptDensity?: SessionPanelProps['transcriptDensity']
|
|
21
20
|
transcriptFont?: SessionPanelProps['transcriptFont']
|
|
21
|
+
affordances?: SessionPanelProps['affordances']
|
|
22
22
|
scrubber?: SessionPanelProps['scrubber']
|
|
23
23
|
bookmarks?: SessionPanelProps['bookmarks']
|
|
24
24
|
onToggleBookmark?: SessionPanelProps['onToggleBookmark']
|
|
@@ -54,8 +54,8 @@ export function SessionWorkspace({
|
|
|
54
54
|
sessionId,
|
|
55
55
|
header,
|
|
56
56
|
transcriptVariant,
|
|
57
|
-
transcriptDensity,
|
|
58
57
|
transcriptFont,
|
|
58
|
+
affordances,
|
|
59
59
|
scrubber,
|
|
60
60
|
bookmarks,
|
|
61
61
|
onToggleBookmark,
|
|
@@ -217,8 +217,8 @@ export function SessionWorkspace({
|
|
|
217
217
|
sessionId={sessionId}
|
|
218
218
|
header={hoisted}
|
|
219
219
|
transcriptVariant={transcriptVariant}
|
|
220
|
-
transcriptDensity={transcriptDensity}
|
|
221
220
|
transcriptFont={transcriptFont}
|
|
221
|
+
affordances={affordances}
|
|
222
222
|
scrubber={scrubber}
|
|
223
223
|
bookmarks={bookmarks}
|
|
224
224
|
onToggleBookmark={onToggleBookmark}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { X } from 'lucide-react'
|
|
2
2
|
import type { ShellInfo } from '@workerdeck/protocol'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { shellInfoFailed, shellInfoStatusText, shellTitle } from '../terminal/shell-row.ts'
|
|
8
|
-
import { KillShellAction, WithActions } from '../terminal/affordances.tsx'
|
|
4
|
+
import { shellAgentWriteLabel, shellGrantable, shellInfoFailed, shellInfoStatusText, shellTitle } from '../terminal/shell-row.ts'
|
|
5
|
+
import { AgentWriteAction, AgentWriteIcon, KillShellAction } from '../terminal/affordances.tsx'
|
|
6
|
+
import { FrameStrip } from './FrameStrip.tsx'
|
|
9
7
|
|
|
10
8
|
export interface ShellStripProps {
|
|
11
9
|
shell: ShellInfo | undefined
|
|
@@ -13,48 +11,41 @@ export interface ShellStripProps {
|
|
|
13
11
|
cols?: number
|
|
14
12
|
onBack: () => void
|
|
15
13
|
onKill?: () => void
|
|
14
|
+
onAgentWrite?: (enabled: boolean) => void
|
|
16
15
|
terminal: boolean
|
|
17
16
|
fontSize?: number
|
|
18
17
|
lineHeight?: number
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
export function ShellStrip({ shell, label, cols, onBack, onKill, terminal, fontSize, lineHeight }: ShellStripProps) {
|
|
20
|
+
export function ShellStrip({ shell, label, cols, onBack, onKill, onAgentWrite, terminal, fontSize, lineHeight }: ShellStripProps) {
|
|
22
21
|
const running = shell?.status === 'running'
|
|
23
22
|
const failed = shell ? shellInfoFailed(shell) : false
|
|
24
23
|
const name = shell ? shellTitle(shell) : label
|
|
25
24
|
const status = shell ? shellInfoStatusText(shell) : undefined
|
|
26
25
|
const width = cols ?? shell?.cols
|
|
27
26
|
const detail = width === undefined ? undefined : `${width} cols`
|
|
27
|
+
const granted = shell?.agentWrite === true
|
|
28
|
+
const grant = shell && onAgentWrite && shellGrantable(shell) ? () => onAgentWrite(!granted) : undefined
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<Button variant="ghost" size="sm" onClick={onBack} className="h-6 gap-1 px-1.5">
|
|
48
|
-
<ArrowLeft className="size-3.5" />
|
|
49
|
-
Back
|
|
50
|
-
</Button>
|
|
51
|
-
<span className={cn('min-w-0 flex-1 truncate font-mono text-body-sm', failed ? 'text-danger' : 'text-fg-2')}>{name}</span>
|
|
52
|
-
{status ? (
|
|
53
|
-
<span className={cn('shrink-0 text-label', failed ? 'text-danger' : running ? 'text-[var(--wd-shell-accent)]' : 'text-fg-3')}>
|
|
54
|
-
{status}
|
|
55
|
-
</span>
|
|
30
|
+
const actions = terminal ? (
|
|
31
|
+
<>
|
|
32
|
+
{grant && shell ? <AgentWriteAction granted={granted} label={shellAgentWriteLabel(shell)} onToggle={grant} /> : null}
|
|
33
|
+
{running && onKill ? <KillShellAction onKill={onKill} /> : null}
|
|
34
|
+
</>
|
|
35
|
+
) : (
|
|
36
|
+
<>
|
|
37
|
+
{grant && shell ? (
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
aria-label={shellAgentWriteLabel(shell)}
|
|
41
|
+
aria-pressed={granted}
|
|
42
|
+
title={shellAgentWriteLabel(shell)}
|
|
43
|
+
className={cn('shrink-0 text-label', granted ? 'text-warning' : 'text-fg-3 hover:text-fg-1')}
|
|
44
|
+
onClick={grant}
|
|
45
|
+
>
|
|
46
|
+
<AgentWriteIcon granted={granted} className="size-3.5" />
|
|
47
|
+
</button>
|
|
56
48
|
) : null}
|
|
57
|
-
{detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
|
|
58
49
|
{running && onKill ? (
|
|
59
50
|
<button
|
|
60
51
|
type="button"
|
|
@@ -66,6 +57,22 @@ export function ShellStrip({ shell, label, cols, onBack, onKill, terminal, fontS
|
|
|
66
57
|
<X className="size-3.5" />
|
|
67
58
|
</button>
|
|
68
59
|
) : null}
|
|
69
|
-
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<FrameStrip
|
|
65
|
+
kind="shell"
|
|
66
|
+
name={name}
|
|
67
|
+
status={status}
|
|
68
|
+
detail={detail}
|
|
69
|
+
failed={failed}
|
|
70
|
+
live={running}
|
|
71
|
+
onBack={onBack}
|
|
72
|
+
actions={actions}
|
|
73
|
+
terminal={terminal}
|
|
74
|
+
fontSize={fontSize}
|
|
75
|
+
lineHeight={lineHeight}
|
|
76
|
+
/>
|
|
70
77
|
)
|
|
71
78
|
}
|
|
@@ -126,7 +126,7 @@ export function ShellTerminal({ handle, shellId, onEnded, onShell, fontSize, cla
|
|
|
126
126
|
}, [ready])
|
|
127
127
|
|
|
128
128
|
return (
|
|
129
|
-
<div className={cn('relative min-h-0 flex-1 overflow-hidden px-2 py-1', className)}>
|
|
129
|
+
<div className={cn('wd-shell-term relative min-h-0 flex-1 overflow-hidden px-2 py-1', className)}>
|
|
130
130
|
<div ref={host} className="h-full w-full" onClick={() => term.current?.focus()} />
|
|
131
131
|
{ready && shell.status !== 'idle' ? null : (
|
|
132
132
|
<div className="absolute inset-0 grid place-items-center">
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { ArrowLeft } from 'lucide-react'
|
|
2
1
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
|
-
import { cn } from '../../lib/utils.ts'
|
|
4
2
|
import { formatDuration } from '../../lib/format.ts'
|
|
5
|
-
import { Button } from '../ui/Button.tsx'
|
|
6
|
-
import { Ink, Row } from '../terminal/row.tsx'
|
|
7
|
-
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
8
3
|
import { taskBusy, taskFailed, taskIdentity } from '../terminal/tool-run.ts'
|
|
9
4
|
import type { ToolCallItem } from '../terminal/blocks.ts'
|
|
10
5
|
import { usePulse } from './pulse.tsx'
|
|
11
6
|
import { useTicker } from '../terminal/items.tsx'
|
|
7
|
+
import { FrameStrip } from './FrameStrip.tsx'
|
|
12
8
|
|
|
13
9
|
export function SubagentStrip({
|
|
14
10
|
task,
|
|
@@ -39,31 +35,18 @@ export function SubagentStrip({
|
|
|
39
35
|
const status = !task ? undefined : failed ? 'failed' : busy ? `${pulse} working…` : 'done'
|
|
40
36
|
const detail = [tools > 0 ? `${tools} tool${tools === 1 ? '' : 's'}` : undefined, elapsed].filter(Boolean).join(' · ')
|
|
41
37
|
|
|
42
|
-
if (terminal) {
|
|
43
|
-
return (
|
|
44
|
-
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className="shrink-0">
|
|
45
|
-
<button type="button" onClick={onBack} aria-label="Back to the session" className="block w-full cursor-pointer text-left">
|
|
46
|
-
<Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : 'green'}>
|
|
47
|
-
{name}
|
|
48
|
-
{status ? <Ink tone={failed ? 'red' : busy ? 'mark' : 'dim'}> · {status}</Ink> : null}
|
|
49
|
-
{detail ? <Ink tone="faint"> · {detail}</Ink> : null}
|
|
50
|
-
</Row>
|
|
51
|
-
</button>
|
|
52
|
-
</TerminalSurface>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
38
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
39
|
+
<FrameStrip
|
|
40
|
+
kind="subagent"
|
|
41
|
+
name={name}
|
|
42
|
+
status={status}
|
|
43
|
+
detail={detail}
|
|
44
|
+
failed={failed}
|
|
45
|
+
live={busy}
|
|
46
|
+
onBack={onBack}
|
|
47
|
+
terminal={terminal}
|
|
48
|
+
fontSize={fontSize}
|
|
49
|
+
lineHeight={lineHeight}
|
|
50
|
+
/>
|
|
68
51
|
)
|
|
69
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react'
|
|
2
2
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
3
3
|
import { ChevronDown, Clock } from 'lucide-react'
|
|
4
4
|
import { Badge } from '../ui/Badge.tsx'
|
|
@@ -9,27 +9,17 @@ import { toolInputPreview } from '../../lib/format.ts'
|
|
|
9
9
|
import { toolIcon } from '../../lib/tool-icon.ts'
|
|
10
10
|
import { useToolResultFetcher } from './tool-result-fetch.tsx'
|
|
11
11
|
import { useToolTitle } from './tool-titles.tsx'
|
|
12
|
-
import { useToolResultImageSrc } from './tool-result-image.tsx'
|
|
13
|
-
import {
|
|
12
|
+
import { useHostImageSrc, useToolResultImageSrc, type HostImageLoader } from './tool-result-image.tsx'
|
|
13
|
+
import { ViewableImage } from './image-viewer.tsx'
|
|
14
|
+
import { IMAGE_UNAVAILABLE, baseName, hostImagePathOf, imagePlaceholder, resultImageName } from '../terminal/image-box.ts'
|
|
14
15
|
|
|
15
16
|
export type ToolCallItem = Extract<TranscriptItem, { kind: 'tool_call' }>
|
|
16
17
|
|
|
17
18
|
const RESULT_PREVIEW_CHARS = 2000
|
|
18
19
|
|
|
19
|
-
const IMAGE_TOOLS = new Set(['CodexImageGeneration', 'CodexImageView'])
|
|
20
|
-
|
|
21
|
-
function imagePathOf(item: ToolCallItem): string | undefined {
|
|
22
|
-
if (!IMAGE_TOOLS.has(item.name)) {
|
|
23
|
-
return undefined
|
|
24
|
-
}
|
|
25
|
-
const input = item.input as { savedPath?: unknown; path?: unknown } | null
|
|
26
|
-
const path = input?.savedPath ?? input?.path
|
|
27
|
-
return typeof path === 'string' ? path : undefined
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
export interface ToolCallCardProps {
|
|
31
21
|
item: ToolCallItem
|
|
32
|
-
hostImage?:
|
|
22
|
+
hostImage?: HostImageLoader
|
|
33
23
|
className?: string
|
|
34
24
|
}
|
|
35
25
|
|
|
@@ -106,7 +96,7 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
|
|
|
106
96
|
const [fullResult, setFullResult] = useState(false)
|
|
107
97
|
const [fetching, setFetching] = useState(false)
|
|
108
98
|
const fetchResult = useToolResultFetcher()
|
|
109
|
-
const imagePath =
|
|
99
|
+
const imagePath = hostImagePathOf(item)
|
|
110
100
|
const status: Status = item.status ?? (item.result === undefined ? 'running' : 'settled')
|
|
111
101
|
const badge = STATE_BADGE[status]
|
|
112
102
|
const isError = status === 'failed' || item.result?.isError === true
|
|
@@ -155,12 +145,12 @@ export function ToolCallCard({ item, hostImage, className }: ToolCallCardProps)
|
|
|
155
145
|
</div>
|
|
156
146
|
) : null
|
|
157
147
|
|
|
158
|
-
const image = imagePath
|
|
148
|
+
const image = imagePath ? <HostImage path={imagePath} load={hostImage} /> : null
|
|
159
149
|
|
|
160
150
|
const resultImages = item.result?.images?.length ? (
|
|
161
151
|
<div className="flex flex-col gap-2 border-t border-border p-2.5">
|
|
162
152
|
{item.result.images.map((ref) => (
|
|
163
|
-
<ResultImage key={ref.partIndex} toolUseId={item.id} image={ref} />
|
|
153
|
+
<ResultImage key={ref.partIndex} toolUseId={item.id} name={resultImageName(item.name, ref)} image={ref} />
|
|
164
154
|
))}
|
|
165
155
|
</div>
|
|
166
156
|
) : null
|
|
@@ -209,12 +199,14 @@ function PlainPayload({ code, label, className }: { code: string; label: string;
|
|
|
209
199
|
|
|
210
200
|
type ToolResultImage = NonNullable<NonNullable<ToolCallItem['result']>['images']>[number]
|
|
211
201
|
|
|
212
|
-
function ResultImage({ toolUseId, image }: { toolUseId: string; image: ToolResultImage }) {
|
|
202
|
+
function ResultImage({ toolUseId, name, image }: { toolUseId: string; name: string; image: ToolResultImage }) {
|
|
213
203
|
const { src, failed } = useToolResultImageSrc({ toolUseId, ...image })
|
|
214
204
|
return (
|
|
215
205
|
<div className="flex h-60 items-start overflow-hidden rounded-md border border-border bg-surface-hover">
|
|
216
206
|
{src ? (
|
|
217
|
-
<
|
|
207
|
+
<ViewableImage image={{ src, name }} className="h-full max-w-full">
|
|
208
|
+
<img src={src} alt={name} className="h-full max-w-full object-contain" />
|
|
209
|
+
</ViewableImage>
|
|
218
210
|
) : (
|
|
219
211
|
<span className="p-2 text-label text-fg-4">{failed ? IMAGE_UNAVAILABLE : imagePlaceholder(image)}</span>
|
|
220
212
|
)}
|
|
@@ -222,32 +214,17 @@ function ResultImage({ toolUseId, image }: { toolUseId: string; image: ToolResul
|
|
|
222
214
|
)
|
|
223
215
|
}
|
|
224
216
|
|
|
225
|
-
function HostImage({ path, load }: { path: string; load
|
|
226
|
-
const
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
let cancelled = false
|
|
229
|
-
setSrc(undefined)
|
|
230
|
-
load(path)
|
|
231
|
-
.then((url) => {
|
|
232
|
-
if (!cancelled) {
|
|
233
|
-
setSrc(url)
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
.catch(() => {})
|
|
237
|
-
return () => {
|
|
238
|
-
cancelled = true
|
|
239
|
-
}
|
|
240
|
-
}, [path, load])
|
|
217
|
+
function HostImage({ path, load }: { path: string; load?: HostImageLoader }) {
|
|
218
|
+
const { src } = useHostImageSrc(path, load)
|
|
241
219
|
if (!src) {
|
|
242
220
|
return null
|
|
243
221
|
}
|
|
222
|
+
const name = baseName(path)
|
|
244
223
|
return (
|
|
245
224
|
<div className="border-t border-border p-2.5">
|
|
246
|
-
<
|
|
247
|
-
src={src}
|
|
248
|
-
|
|
249
|
-
className="max-h-96 w-auto max-w-full rounded-md border border-border"
|
|
250
|
-
/>
|
|
225
|
+
<ViewableImage image={{ src, name }} className="block max-w-full">
|
|
226
|
+
<img src={src} alt={name} className="max-h-96 w-auto max-w-full rounded-md border border-border" />
|
|
227
|
+
</ViewableImage>
|
|
251
228
|
</div>
|
|
252
229
|
)
|
|
253
230
|
}
|