@workerdeck/ui 2.15.0 → 3.0.1

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.
Files changed (45) hide show
  1. package/README.md +6 -5
  2. package/build/{SessionPanel-BVKqPs_3.d.mts → SessionPanel-C1bU4yTM.d.mts} +11 -11
  3. package/build/index.d.mts +46 -9
  4. package/build/index.mjs +70 -43
  5. package/build/index.mjs.map +1 -1
  6. package/build/scoped.css +156 -16
  7. package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-BEWxt0ZM.mjs} +1566 -852
  8. package/build/use-path-links-BEWxt0ZM.mjs.map +1 -0
  9. package/build/workspace.d.mts +3 -3
  10. package/build/workspace.mjs +3 -3
  11. package/build/workspace.mjs.map +1 -1
  12. package/package.json +4 -4
  13. package/src/components/agent/PermissionPrompt.tsx +16 -2
  14. package/src/components/agent/Response.tsx +8 -1
  15. package/src/components/agent/SessionBrowser.tsx +6 -0
  16. package/src/components/agent/SessionItem.tsx +3 -0
  17. package/src/components/agent/SessionPanel.tsx +28 -33
  18. package/src/components/agent/SessionSteps.tsx +46 -5
  19. package/src/components/agent/SessionWorkspace.tsx +3 -3
  20. package/src/components/agent/ShellStrip.tsx +27 -4
  21. package/src/components/agent/ShellTerminal.tsx +1 -1
  22. package/src/components/agent/ToolCallCard.tsx +18 -41
  23. package/src/components/agent/Transcript.tsx +98 -93
  24. package/src/components/agent/TranscriptItemView.tsx +102 -59
  25. package/src/components/agent/TranscriptRows.tsx +4 -2
  26. package/src/components/agent/image-viewer.tsx +326 -0
  27. package/src/components/agent/markdown-image.tsx +91 -0
  28. package/src/components/agent/shell-actions.tsx +2 -0
  29. package/src/components/agent/tool-result-image.tsx +40 -0
  30. package/src/components/agent/transcript-variant.tsx +3 -21
  31. package/src/components/prompt-area/use-prompt-area-keydown.ts +4 -2
  32. package/src/components/terminal/PermissionPrompt.tsx +20 -4
  33. package/src/components/terminal/TerminalTranscript.tsx +38 -21
  34. package/src/components/terminal/affordances.tsx +100 -74
  35. package/src/components/terminal/height.ts +48 -32
  36. package/src/components/terminal/image-box.ts +31 -0
  37. package/src/components/terminal/items.tsx +92 -14
  38. package/src/components/terminal/markdown.tsx +4 -1
  39. package/src/components/terminal/shell-row.ts +17 -3
  40. package/src/index.ts +19 -4
  41. package/src/lib/shell-request.ts +115 -0
  42. package/src/lib/tool-icon.ts +13 -1
  43. package/src/styles/terminal.css +129 -22
  44. package/src/styles/theme.css +4 -0
  45. package/build/use-path-links-BgWXmfAS.mjs.map +0 -1
@@ -3,7 +3,18 @@ import { isAgentRecord, promotedShells, subagentLabel, visibleSubagents } from '
3
3
  import type { SessionInfo, ShellInfo, SubagentDisplay, SubagentInfo } from '@workerdeck/protocol'
4
4
  import { Spinner } from '../ui/Spinner.tsx'
5
5
  import { cn } from '../../lib/utils.ts'
6
- import { SHELL_GLYPH, SHELL_KILL_GLYPH, shellInfoFailed, shellInfoLabel, shellInfoStatusText, shellTitle } from '../terminal/shell-row.ts'
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
19
  export type StepKind = 'agent' | 'shell'
9
20
 
@@ -17,12 +28,15 @@ export type Step = {
17
28
  title: string
18
29
  onSelect: () => void
19
30
  onKill?: () => void
31
+ agentWrite?: { granted: boolean; label: string; toggle: () => void }
20
32
  }
21
33
 
22
34
  export type ShellStepOptions = {
23
35
  now: number
24
36
  onSelect: (shellId: string) => void
25
37
  onKill?: (shellId: string) => void
38
+ // Offered only where the session's agent holds the shell write tools.
39
+ onAgentWrite?: (shellId: string, enabled: boolean) => void
26
40
  }
27
41
 
28
42
  export function sessionSteps(
@@ -46,10 +60,11 @@ export function sessionSteps(
46
60
  if (shells === undefined) {
47
61
  return agents
48
62
  }
49
- return [...agents, ...promotedShells(info, shells.now).map((shell) => shellStep(shell, shells))]
63
+ const grants = info.shellAgentWrite !== undefined
64
+ return [...agents, ...promotedShells(info, shells.now).map((shell) => shellStep(shell, shells, grants))]
50
65
  }
51
66
 
52
- function shellStep(shell: ShellInfo, options: ShellStepOptions): Step {
67
+ function shellStep(shell: ShellInfo, options: ShellStepOptions, grants: boolean): Step {
53
68
  const status = shellInfoStatusText(shell)
54
69
  const running = shell.status === 'running'
55
70
  return {
@@ -58,10 +73,18 @@ function shellStep(shell: ShellInfo, options: ShellStepOptions): Step {
58
73
  label: shellInfoLabel(shell),
59
74
  noun: 'shell',
60
75
  state: shellInfoFailed(shell) ? 'failed' : running ? 'running' : 'done',
61
- detail: running ? undefined : status,
76
+ detail: running ? (shell.agentWrite === true ? SHELL_AGENT_WRITE_NOTE : undefined) : status,
62
77
  title: `${shellTitle(shell)} · ${status}`,
63
78
  onSelect: () => options.onSelect(shell.id),
64
79
  onKill: running && options.onKill ? () => options.onKill?.(shell.id) : undefined,
80
+ agentWrite:
81
+ grants && options.onAgentWrite && shellGrantable(shell)
82
+ ? {
83
+ granted: shell.agentWrite === true,
84
+ label: shellAgentWriteLabel(shell),
85
+ toggle: () => options.onAgentWrite?.(shell.id, shell.agentWrite !== true),
86
+ }
87
+ : undefined,
65
88
  }
66
89
  }
67
90
 
@@ -96,8 +119,26 @@ export function StepRow({ step, active = false, onSelect }: { step: Step; active
96
119
  <StepIcon step={step} />
97
120
  <span className="min-w-0 flex-1 truncate">{step.label}</span>
98
121
  {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" />
122
+ {step.kind === 'shell' ? null : <ArrowRight className="size-3.5 shrink-0 text-fg-4" />}
100
123
  </button>
124
+ {step.agentWrite ? (
125
+ <button
126
+ type="button"
127
+ aria-label={step.agentWrite.label}
128
+ aria-pressed={step.agentWrite.granted}
129
+ title={step.agentWrite.label}
130
+ onClick={(e) => {
131
+ e.stopPropagation()
132
+ step.agentWrite?.toggle()
133
+ }}
134
+ className={cn(
135
+ 'shrink-0 px-1 text-micro leading-none outline-none',
136
+ step.agentWrite.granted ? 'text-warning' : 'text-fg-4 hover:text-fg-1',
137
+ )}
138
+ >
139
+ <AgentWriteIcon granted={step.agentWrite.granted} className="size-3.5" />
140
+ </button>
141
+ ) : null}
101
142
  {step.onKill ? (
102
143
  <button
103
144
  type="button"
@@ -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}
@@ -4,8 +4,8 @@ import { cn } from '../../lib/utils.ts'
4
4
  import { Button } from '../ui/Button.tsx'
5
5
  import { Ink, Row } from '../terminal/row.tsx'
6
6
  import { TerminalSurface } from '../terminal/surface.tsx'
7
- import { shellInfoFailed, shellInfoStatusText, shellTitle } from '../terminal/shell-row.ts'
8
- import { KillShellAction, WithActions } from '../terminal/affordances.tsx'
7
+ import { shellAgentWriteLabel, shellGrantable, shellInfoFailed, shellInfoStatusText, shellTitle } from '../terminal/shell-row.ts'
8
+ import { AgentWriteAction, AgentWriteIcon, KillShellAction, WithActions } from '../terminal/affordances.tsx'
9
9
 
10
10
  export interface ShellStripProps {
11
11
  shell: ShellInfo | undefined
@@ -13,23 +13,34 @@ export interface ShellStripProps {
13
13
  cols?: number
14
14
  onBack: () => void
15
15
  onKill?: () => void
16
+ onAgentWrite?: (enabled: boolean) => void
16
17
  terminal: boolean
17
18
  fontSize?: number
18
19
  lineHeight?: number
19
20
  }
20
21
 
21
- export function ShellStrip({ shell, label, cols, onBack, onKill, terminal, fontSize, lineHeight }: ShellStripProps) {
22
+ export function ShellStrip({ shell, label, cols, onBack, onKill, onAgentWrite, terminal, fontSize, lineHeight }: ShellStripProps) {
22
23
  const running = shell?.status === 'running'
23
24
  const failed = shell ? shellInfoFailed(shell) : false
24
25
  const name = shell ? shellTitle(shell) : label
25
26
  const status = shell ? shellInfoStatusText(shell) : undefined
26
27
  const width = cols ?? shell?.cols
27
28
  const detail = width === undefined ? undefined : `${width} cols`
29
+ const granted = shell?.agentWrite === true
30
+ const grant = shell && onAgentWrite && shellGrantable(shell) ? () => onAgentWrite(!granted) : undefined
28
31
 
29
32
  if (terminal) {
30
33
  return (
31
34
  <TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className="shrink-0">
32
- <WithActions actions={running && onKill ? <KillShellAction onKill={onKill} /> : null}>
35
+ <WithActions
36
+ placement="inline"
37
+ actions={
38
+ <>
39
+ {grant && shell ? <AgentWriteAction granted={granted} label={shellAgentWriteLabel(shell)} onToggle={grant} /> : null}
40
+ {running && onKill ? <KillShellAction onKill={onKill} /> : null}
41
+ </>
42
+ }
43
+ >
33
44
  <Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : 'magenta'}>
34
45
  <button type="button" onClick={onBack} aria-label="Back to the session" className="cursor-pointer text-left">
35
46
  {name}
@@ -55,6 +66,18 @@ export function ShellStrip({ shell, label, cols, onBack, onKill, terminal, fontS
55
66
  </span>
56
67
  ) : null}
57
68
  {detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
69
+ {grant && shell ? (
70
+ <button
71
+ type="button"
72
+ aria-label={shellAgentWriteLabel(shell)}
73
+ aria-pressed={granted}
74
+ title={shellAgentWriteLabel(shell)}
75
+ className={cn('shrink-0 text-label', granted ? 'text-warning' : 'text-fg-3 hover:text-fg-1')}
76
+ onClick={grant}
77
+ >
78
+ <AgentWriteIcon granted={granted} className="size-3.5" />
79
+ </button>
80
+ ) : null}
58
81
  {running && onKill ? (
59
82
  <button
60
83
  type="button"
@@ -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,4 +1,4 @@
1
- import { useEffect, useState } from 'react'
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 { IMAGE_UNAVAILABLE, imagePlaceholder } from '../terminal/image-box.ts'
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?: (path: string) => Promise<string | undefined>
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 = imagePathOf(item)
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 && hostImage ? <HostImage path={imagePath} load={hostImage} /> : null
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
- <img src={src} alt={imagePlaceholder(image)} className="h-full max-w-full object-contain" />
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: (path: string) => Promise<string | undefined> }) {
226
- const [src, setSrc] = useState<string | undefined>()
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
- <img
247
- src={src}
248
- alt={path.split('/').pop() ?? 'Generated image'}
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
  }
@@ -4,7 +4,7 @@ import { cn } from '../../lib/utils.ts'
4
4
  import { Conversation, ConversationContent, ConversationScrollButton } from './Conversation.tsx'
5
5
  import { Loader } from './Loader.tsx'
6
6
  import { SessionEmptyState } from './SessionEmptyState.tsx'
7
- import { type TerminalAffordances } from '../terminal/affordances.tsx'
7
+ import { AffordanceProvider, resolveAffordances, type TerminalAffordances } from '../terminal/affordances.tsx'
8
8
  import { WorkingRow } from '../terminal/items.tsx'
9
9
  import { subagentItems, terminalBlocks, type ToolCallItem } from '../terminal/blocks.ts'
10
10
  import { taskBrief, taskBusy } from '../terminal/tool-run.ts'
@@ -12,7 +12,8 @@ import { type TranscriptRow } from './transcript-rows.ts'
12
12
  import { TranscriptRows } from './TranscriptRows.tsx'
13
13
  import { SessionNamesProvider, SkillNamesProvider } from './PromptTokenText.tsx'
14
14
  import { TerminalSurface } from '../terminal/surface.tsx'
15
- import { ROW_GAP, TranscriptVariantProvider, type TranscriptDensity, type TranscriptVariant } from './transcript-variant.tsx'
15
+ import { ROW_GAP, TranscriptVariantProvider, type TranscriptVariant } from './transcript-variant.tsx'
16
+ import { HostImageProvider } from './tool-result-image.tsx'
16
17
 
17
18
  function useRunStart(status: TranscriptState['status']): number | undefined {
18
19
  const running = status === 'running' || status === 'starting'
@@ -54,7 +55,11 @@ function TerminalShell({
54
55
  children: ReactNode
55
56
  }) {
56
57
  if (!active) {
57
- return <>{children}</>
58
+ return (
59
+ <div data-cards="" className="contents">
60
+ <AffordanceProvider value={resolveAffordances(affordances)}>{children}</AffordanceProvider>
61
+ </div>
62
+ )
58
63
  }
59
64
  return (
60
65
  <TerminalSurface fontSize={fontSize} lineHeight={lineHeight} affordances={affordances} bleed="1ch" className="term-transcript">
@@ -72,7 +77,6 @@ export interface TranscriptProps {
72
77
  canBrowseFiles?: boolean
73
78
  hostImage?: (path: string) => Promise<string | undefined>
74
79
  variant?: TranscriptVariant
75
- density?: TranscriptDensity
76
80
  fontSize?: number
77
81
  lineHeight?: number
78
82
  affordances?: TerminalAffordances | boolean
@@ -99,7 +103,6 @@ export function Transcript({
99
103
  canBrowseFiles,
100
104
  hostImage,
101
105
  variant = 'cards',
102
- density = 'comfortable',
103
106
  fontSize,
104
107
  lineHeight,
105
108
  affordances,
@@ -124,7 +127,7 @@ export function Transcript({
124
127
  frame ? state.items.find((item): item is ToolCallItem => item.kind === 'tool_call' && item.id === frame.parentToolUseId) : undefined,
125
128
  [state.items, frame],
126
129
  )
127
- const gap = ROW_GAP[variant][density]
130
+ const gap = ROW_GAP[variant]
128
131
  const runStartedAt = useRunStart(state.status)
129
132
  const boundary = !frame && catchUp && catchUp.from > 0 && catchUp.from < state.items.length ? catchUp.from : undefined
130
133
  const recap = useMemo(() => (boundary === undefined ? undefined : recapLine(summarizeSince(state, boundary))), [state, boundary])
@@ -143,98 +146,100 @@ export function Transcript({
143
146
  const skillNames = useMemo(() => state.skills?.map((skill) => skill.name), [state.skills])
144
147
  return (
145
148
  <TranscriptVariantProvider value={variant}>
146
- <SkillNamesProvider names={skillNames}>
147
- <SessionNamesProvider names={sessionNames}>
148
- <Conversation className={cn(replaying && 'invisible', className)}>
149
- <ConversationContent className={cn(terminal && 'gap-0 p-0')}>
150
- <TerminalShell active={terminal} fontSize={fontSize} lineHeight={lineHeight} affordances={affordances}>
151
- {frame ? (
152
- frameTask === undefined && !replaying ? (
153
- <div className={cn(terminal ? 'term-row text-fg-4' : 'p-4 text-body-sm text-fg-4')}>
154
- This sub-agent's work is not in this transcript.
155
- </div>
156
- ) : null
157
- ) : items.length === 0 && state.status !== 'starting' ? (
158
- emptyState !== undefined ? (
159
- emptyState
160
- ) : (
161
- <SessionEmptyState
162
- cwd={state.cwd}
163
- hasCommands={!!state.commands?.length}
164
- hasSkills={!!state.skills?.some((s) => s.enabled)}
165
- canBrowseFiles={canBrowseFiles}
166
- />
167
- )
168
- ) : null}
169
- {frame && frameTask === undefined && !replaying ? null : (
170
- <TranscriptRows
171
- rows={rows}
172
- boundary={boundary}
173
- since={catchUp?.since}
174
- terminal={terminal}
175
- replaying={replaying}
176
- stickyPrompt={!frame && stickyPrompt}
177
- gap={gap}
178
- fontSize={fontSize}
179
- lineHeight={lineHeight}
180
- items={items}
181
- pendingApprovals={state.pendingApprovals}
182
- scrubber={scrubber}
183
- bookmarks={frame ? undefined : bookmarks}
184
- affordances={affordances}
185
- fileUrl={fileUrl}
186
- attachmentUrl={attachmentUrl}
187
- hostImage={hostImage}
188
- jumpToRecapRef={jumpToRecapRef}
189
- repinRef={repinRef}
190
- reveal={frame ? undefined : reveal}
191
- frameParentId={frame?.parentToolUseId}
192
- onOpenSubagent={frame ? undefined : onOpenSubagent}
193
- />
194
- )}
195
- {(frame ? frameTask !== undefined && taskBusy(frameTask, items) : showLoader(state)) ? (
196
- terminal ? (
197
- <>
198
- {state.items.length > 0 ? <div className="term-blank" aria-hidden /> : null}
199
- <WorkingRow
200
- label={state.status === 'starting' ? 'Starting…' : 'Working…'}
201
- startedAt={runStartedAt}
202
- tokens={state.contextUsage?.totalTokens}
149
+ <HostImageProvider value={hostImage}>
150
+ <SkillNamesProvider names={skillNames}>
151
+ <SessionNamesProvider names={sessionNames}>
152
+ <Conversation className={cn(replaying && 'invisible', className)}>
153
+ <ConversationContent className={cn(terminal && 'gap-0 p-0')}>
154
+ <TerminalShell active={terminal} fontSize={fontSize} lineHeight={lineHeight} affordances={affordances}>
155
+ {frame ? (
156
+ frameTask === undefined && !replaying ? (
157
+ <div className={cn(terminal ? 'term-row text-fg-4' : 'p-4 text-body-sm text-fg-4')}>
158
+ This sub-agent's work is not in this transcript.
159
+ </div>
160
+ ) : null
161
+ ) : items.length === 0 && state.status !== 'starting' ? (
162
+ emptyState !== undefined ? (
163
+ emptyState
164
+ ) : (
165
+ <SessionEmptyState
166
+ cwd={state.cwd}
167
+ hasCommands={!!state.commands?.length}
168
+ hasSkills={!!state.skills?.some((s) => s.enabled)}
169
+ canBrowseFiles={canBrowseFiles}
203
170
  />
204
- </>
205
- ) : (
206
- <Loader label={state.status === 'starting' ? 'Starting session…' : undefined} />
207
- )
208
- ) : null}
209
- </TerminalShell>
210
- </ConversationContent>
211
- <ConversationScrollButton />
212
- {replaying ? (
213
- <div
214
- data-slot="transcript-hold"
215
- aria-hidden
216
- className="wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden"
217
- >
218
- <div className={cn('mx-auto w-full max-w-[var(--wd-transcript-max-width)]', !terminal && 'px-4 py-4')}>
219
- {terminal ? (
220
- <TerminalSurface
171
+ )
172
+ ) : null}
173
+ {frame && frameTask === undefined && !replaying ? null : (
174
+ <TranscriptRows
175
+ rows={rows}
176
+ boundary={boundary}
177
+ since={catchUp?.since}
178
+ terminal={terminal}
179
+ replaying={replaying}
180
+ stickyPrompt={!frame && stickyPrompt}
181
+ gap={gap}
221
182
  fontSize={fontSize}
222
183
  lineHeight={lineHeight}
223
- affordances={false}
224
- bleed="1ch"
225
- className="term-transcript"
226
- >
227
- <WorkingRow label="Loading…" />
228
- </TerminalSurface>
229
- ) : (
230
- <Loader label="Loading session…" />
184
+ items={items}
185
+ pendingApprovals={state.pendingApprovals}
186
+ scrubber={scrubber}
187
+ bookmarks={frame ? undefined : bookmarks}
188
+ affordances={affordances}
189
+ fileUrl={fileUrl}
190
+ attachmentUrl={attachmentUrl}
191
+ hostImage={hostImage}
192
+ jumpToRecapRef={jumpToRecapRef}
193
+ repinRef={repinRef}
194
+ reveal={frame ? undefined : reveal}
195
+ frameParentId={frame?.parentToolUseId}
196
+ onOpenSubagent={frame ? undefined : onOpenSubagent}
197
+ />
231
198
  )}
199
+ {(frame ? frameTask !== undefined && taskBusy(frameTask, items) : showLoader(state)) ? (
200
+ terminal ? (
201
+ <>
202
+ {state.items.length > 0 ? <div className="term-blank" aria-hidden /> : null}
203
+ <WorkingRow
204
+ label={state.status === 'starting' ? 'Starting…' : 'Working…'}
205
+ startedAt={runStartedAt}
206
+ tokens={state.contextUsage?.totalTokens}
207
+ />
208
+ </>
209
+ ) : (
210
+ <Loader label={state.status === 'starting' ? 'Starting session…' : undefined} />
211
+ )
212
+ ) : null}
213
+ </TerminalShell>
214
+ </ConversationContent>
215
+ <ConversationScrollButton />
216
+ {replaying ? (
217
+ <div
218
+ data-slot="transcript-hold"
219
+ aria-hidden
220
+ className="wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden"
221
+ >
222
+ <div className={cn('mx-auto w-full max-w-[var(--wd-transcript-max-width)]', !terminal && 'px-4 py-4')}>
223
+ {terminal ? (
224
+ <TerminalSurface
225
+ fontSize={fontSize}
226
+ lineHeight={lineHeight}
227
+ affordances={false}
228
+ bleed="1ch"
229
+ className="term-transcript"
230
+ >
231
+ <WorkingRow label="Loading…" />
232
+ </TerminalSurface>
233
+ ) : (
234
+ <Loader label="Loading session…" />
235
+ )}
236
+ </div>
232
237
  </div>
233
- </div>
234
- ) : null}
235
- </Conversation>
236
- </SessionNamesProvider>
237
- </SkillNamesProvider>
238
+ ) : null}
239
+ </Conversation>
240
+ </SessionNamesProvider>
241
+ </SkillNamesProvider>
242
+ </HostImageProvider>
238
243
  </TranscriptVariantProvider>
239
244
  )
240
245
  }