@workerdeck/ui 3.0.1 → 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/build/{SessionPanel-C1bU4yTM.d.mts → SessionPanel-BNNc8grK.d.mts} +9 -7
- package/build/index.d.mts +153 -45
- package/build/index.mjs +501 -226
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -6
- package/build/{use-path-links-BEWxt0ZM.mjs → use-path-links-C6Fh5zdx.mjs} +1926 -2086
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +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 +13 -22
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +125 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +27 -7
- package/src/components/agent/SessionPanel.tsx +244 -488
- package/src/components/agent/SessionSteps.tsx +54 -11
- package/src/components/agent/ShellStrip.tsx +27 -43
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/Transcript.tsx +13 -0
- package/src/components/agent/TranscriptItemView.tsx +6 -36
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/markdown-image.tsx +49 -1
- 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 +36 -1
- 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/terminal/PermissionPrompt.tsx +19 -32
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +3 -86
- package/src/components/terminal/items.tsx +5 -43
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +46 -8
- 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/build/use-path-links-BEWxt0ZM.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
usageInfos,
|
|
18
18
|
type ModelOption,
|
|
19
19
|
type PermissionMode,
|
|
20
|
-
type PermissionRequest,
|
|
21
20
|
type RateLimitInfo,
|
|
22
21
|
type SessionTask,
|
|
23
22
|
type ShellInfo,
|
|
@@ -34,90 +33,50 @@ import {
|
|
|
34
33
|
useToolCallHost,
|
|
35
34
|
type ClientToolHandler,
|
|
36
35
|
type ConnectionState,
|
|
37
|
-
type ProducedFileRef,
|
|
38
36
|
type TranscriptState,
|
|
39
37
|
type UseToolCallHostOptions,
|
|
40
38
|
} from '@workerdeck/react'
|
|
41
|
-
import { ChartPie, FolderTree, Gauge, Info, MoreHorizontal, Plug, Sparkles, TriangleAlert, X } from 'lucide-react'
|
|
39
|
+
import { ChartPie, FolderTree, Gauge, Info, MoreHorizontal, Plug, Sparkles, TriangleAlert, X, type LucideIcon } from 'lucide-react'
|
|
42
40
|
import { cn } from '../../lib/utils.ts'
|
|
43
41
|
import { Button } from '../ui/Button.tsx'
|
|
44
42
|
import { Menu, MenuContent, MenuItem, MenuTrigger } from '../ui/Menu.tsx'
|
|
45
43
|
import { Composer, type ComposerHandle } from './Composer.tsx'
|
|
46
44
|
import {
|
|
45
|
+
buildClientCommands,
|
|
47
46
|
composerCommandRows,
|
|
48
47
|
matchClientCommand,
|
|
49
48
|
mergeComposerRows,
|
|
50
49
|
skillPrompt,
|
|
51
|
-
type ClientCommand,
|
|
52
50
|
type ComposerCommandRow,
|
|
53
51
|
} from './composer-commands.ts'
|
|
54
52
|
import { ContextDialog } from './ContextDialog.tsx'
|
|
55
|
-
import { TasksDialog } from './TasksDialog.tsx'
|
|
56
53
|
import { HostFilesDialog } from './HostFilesDialog.tsx'
|
|
57
54
|
import { McpDialog } from './McpDialog.tsx'
|
|
58
55
|
import { SkillsDialog } from './SkillsDialog.tsx'
|
|
59
56
|
import { ModelSelect } from './ModelSelect.tsx'
|
|
60
57
|
import { PermissionModeSelect, permissionModeChoices, type PermissionModeChoice } from './PermissionModeSelect.tsx'
|
|
61
|
-
import { PermissionPrompt } from './PermissionPrompt.tsx'
|
|
62
58
|
import { SubagentStrip } from './SubagentStrip.tsx'
|
|
63
59
|
import { useSubagentFrame } from './use-subagent-frame.ts'
|
|
64
60
|
import { ShellStrip } from './ShellStrip.tsx'
|
|
65
61
|
import { ShellTerminal } from './ShellTerminal.tsx'
|
|
66
62
|
import { useShellFrame } from './use-shell-frame.ts'
|
|
67
|
-
import { QuestionPrompt, parseUserQuestions } from './QuestionPrompt.tsx'
|
|
68
|
-
import { TerminalPermissionPrompt } from '../terminal/PermissionPrompt.tsx'
|
|
69
|
-
import { TerminalQuestionPrompt } from '../terminal/QuestionPrompt.tsx'
|
|
70
|
-
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
71
63
|
import { BookmarkProvider, type BookmarkHandle, type TerminalAffordances } from '../terminal/affordances.tsx'
|
|
72
|
-
import {
|
|
73
|
-
|
|
64
|
+
import type { FileLinkOpener } from '../terminal/file-link.tsx'
|
|
65
|
+
import { ApprovalPrompts, type ApprovalPromptProps } from './ApprovalPrompts.tsx'
|
|
66
|
+
import { SessionPanelProviders } from './session-panel-providers.tsx'
|
|
67
|
+
import { useCatchUp } from './use-catch-up.ts'
|
|
68
|
+
import { useHostImage } from './use-host-image.ts'
|
|
74
69
|
import { SessionInfoDialog } from './SessionInfoDialog.tsx'
|
|
75
70
|
import { StatusBar } from './StatusBar.tsx'
|
|
76
71
|
import { Transcript } from './Transcript.tsx'
|
|
77
|
-
import {
|
|
78
|
-
import { ShellActionsProvider } from './shell-actions.tsx'
|
|
79
|
-
import { ToolTitleProvider } from './tool-titles.tsx'
|
|
80
|
-
import { ToolResultImageProvider, useToolResultImages } from './tool-result-image.tsx'
|
|
72
|
+
import { useToolResultImages } from './tool-result-image.tsx'
|
|
81
73
|
import { ImageViewerProvider } from './image-viewer.tsx'
|
|
82
|
-
import {
|
|
74
|
+
import type { TranscriptFont, TranscriptVariant } from './transcript-variant.tsx'
|
|
83
75
|
import { UsageDialog } from './UsageDialog.tsx'
|
|
84
76
|
|
|
85
|
-
function PromptSurface({
|
|
86
|
-
terminal,
|
|
87
|
-
metrics,
|
|
88
|
-
affordances,
|
|
89
|
-
children,
|
|
90
|
-
}: {
|
|
91
|
-
terminal: boolean
|
|
92
|
-
metrics?: TerminalMetrics
|
|
93
|
-
affordances?: TerminalAffordances | boolean
|
|
94
|
-
children: ReactNode
|
|
95
|
-
}) {
|
|
96
|
-
if (!terminal) {
|
|
97
|
-
return <div className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-2">{children}</div>
|
|
98
|
-
}
|
|
99
|
-
return (
|
|
100
|
-
<TerminalSurface
|
|
101
|
-
fontSize={metrics?.fontSize}
|
|
102
|
-
lineHeight={metrics?.lineHeight}
|
|
103
|
-
affordances={affordances}
|
|
104
|
-
bleed="1ch"
|
|
105
|
-
className="term-transcript"
|
|
106
|
-
>
|
|
107
|
-
{children}
|
|
108
|
-
</TerminalSurface>
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
77
|
export type TerminalMetrics = { fontSize?: number; lineHeight?: number }
|
|
113
78
|
|
|
114
|
-
|
|
115
|
-
// `TerminalPermissionPrompt` stay drop-in fallbacks for an input a host declines to draw.
|
|
116
|
-
export interface ApprovalPromptProps {
|
|
117
|
-
request: PermissionRequest
|
|
118
|
-
onApprove: (requestId: string, updatedInput?: Record<string, unknown>) => void
|
|
119
|
-
onDeny: (requestId: string, message?: string, interrupt?: boolean) => void
|
|
120
|
-
}
|
|
79
|
+
export type { ApprovalPromptProps }
|
|
121
80
|
|
|
122
81
|
export interface SessionPanelProps {
|
|
123
82
|
client: WorkerDeckClient
|
|
@@ -192,6 +151,16 @@ const INTERACTIVE = [
|
|
|
192
151
|
|
|
193
152
|
export type SessionSurfacePanel = 'info' | 'context' | 'usage' | 'mcp' | 'files' | 'skills' | 'tasks'
|
|
194
153
|
type Panel = SessionSurfacePanel
|
|
154
|
+
type MenuPanel = Exclude<Panel, 'tasks'>
|
|
155
|
+
|
|
156
|
+
const PANEL_ITEMS: { panel: MenuPanel; label: string; Icon: LucideIcon }[] = [
|
|
157
|
+
{ panel: 'context', label: 'Context', Icon: ChartPie },
|
|
158
|
+
{ panel: 'usage', label: 'Usage', Icon: Gauge },
|
|
159
|
+
{ panel: 'info', label: 'Session info', Icon: Info },
|
|
160
|
+
{ panel: 'mcp', label: 'MCP servers', Icon: Plug },
|
|
161
|
+
{ panel: 'skills', label: 'Skills', Icon: Sparkles },
|
|
162
|
+
{ panel: 'files', label: 'Project files', Icon: FolderTree },
|
|
163
|
+
]
|
|
195
164
|
|
|
196
165
|
export type SessionVitals = {
|
|
197
166
|
status: TranscriptState['status']
|
|
@@ -257,8 +226,10 @@ export function SessionPanel({
|
|
|
257
226
|
fontSize,
|
|
258
227
|
className,
|
|
259
228
|
}: SessionPanelProps) {
|
|
260
|
-
const
|
|
261
|
-
|
|
229
|
+
const cell = {
|
|
230
|
+
fontSize: terminalMetrics?.fontSize ?? fontSize,
|
|
231
|
+
lineHeight: terminalMetrics?.lineHeight ?? (fontSize !== undefined ? Math.round(fontSize * (18 / 13)) : undefined),
|
|
232
|
+
}
|
|
262
233
|
|
|
263
234
|
const external = panelSurface === 'external'
|
|
264
235
|
const statusExternal = statusSurface === 'external'
|
|
@@ -266,8 +237,6 @@ export function SessionPanel({
|
|
|
266
237
|
const controlsExternal = controlsSurface === 'external' || controlsInStatus
|
|
267
238
|
const [protocolError, setProtocolError] = useState<string | undefined>(undefined)
|
|
268
239
|
const [panel, setPanel] = useState<Panel | undefined>()
|
|
269
|
-
const [showCompletedTasks, setShowCompletedTasks] = useState(false)
|
|
270
|
-
const [taskReveal, setTaskReveal] = useState<{ toolUseId: string; nonce: number } | undefined>()
|
|
271
240
|
const {
|
|
272
241
|
state,
|
|
273
242
|
connection,
|
|
@@ -326,12 +295,7 @@ export function SessionPanel({
|
|
|
326
295
|
return returnReveal ?? shellReturnReveal
|
|
327
296
|
}, [returnReveal, shellReturnReveal])
|
|
328
297
|
|
|
329
|
-
const
|
|
330
|
-
useEffect(() => {
|
|
331
|
-
setCaughtUp(false)
|
|
332
|
-
}, [sessionId])
|
|
333
|
-
const [catchUpMark] = useState(unseen)
|
|
334
|
-
const catchUp = caughtUp ? undefined : catchUpMark
|
|
298
|
+
const { mark: catchUp, dismiss: dismissCatchUp } = useCatchUp(sessionId, unseen)
|
|
335
299
|
const newCount = catchUp ? Math.max(0, state.items.length - catchUp.itemCount) : 0
|
|
336
300
|
|
|
337
301
|
const openPanel = useCallback(
|
|
@@ -386,66 +350,11 @@ export function SessionPanel({
|
|
|
386
350
|
[state.checklist, subagents, state.session?.subagents],
|
|
387
351
|
)
|
|
388
352
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
name: 'model',
|
|
395
|
-
description: 'Switch the model for this session',
|
|
396
|
-
argumentHint: '<model>',
|
|
397
|
-
requiresArgs: true,
|
|
398
|
-
run: (args) => {
|
|
399
|
-
const wanted = args.split(/\s+/)[0]
|
|
400
|
-
if (!wanted) {
|
|
401
|
-
return false
|
|
402
|
-
}
|
|
403
|
-
setModel(wanted)
|
|
404
|
-
return true
|
|
405
|
-
},
|
|
406
|
-
})
|
|
407
|
-
}
|
|
408
|
-
if (modes.length > 1) {
|
|
409
|
-
built.push({
|
|
410
|
-
name: 'permissions',
|
|
411
|
-
description: `Set the permission mode (${modes.join(', ')})`,
|
|
412
|
-
argumentHint: '<mode>',
|
|
413
|
-
requiresArgs: true,
|
|
414
|
-
run: (args) => {
|
|
415
|
-
const wanted = args.split(/\s+/)[0] as PermissionMode
|
|
416
|
-
if (!modes.includes(wanted)) {
|
|
417
|
-
return false
|
|
418
|
-
}
|
|
419
|
-
setPermissionMode(wanted)
|
|
420
|
-
return true
|
|
421
|
-
},
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
if (capabilities.clearContext) {
|
|
425
|
-
built.push({
|
|
426
|
-
name: 'clear',
|
|
427
|
-
description: 'Clear the conversation - the session keeps running and the old one stays resumable',
|
|
428
|
-
run: () => {
|
|
429
|
-
clearContext()
|
|
430
|
-
return true
|
|
431
|
-
},
|
|
432
|
-
})
|
|
433
|
-
}
|
|
434
|
-
if (capabilities.mcpStatus) {
|
|
435
|
-
built.push({ name: 'mcp', description: 'MCP servers and their status', run: () => (openPanel('mcp'), true) })
|
|
436
|
-
}
|
|
437
|
-
if (capabilities.contextUsage) {
|
|
438
|
-
built.push({ name: 'context', description: 'Context window usage', run: () => (openPanel('context'), true) })
|
|
439
|
-
}
|
|
440
|
-
built.push({ name: 'status', description: 'Session details', run: () => (openPanel('info'), true) })
|
|
441
|
-
if (capabilities.rateLimits) {
|
|
442
|
-
built.push({ name: 'usage', description: 'Rate limits and spend', run: () => (openPanel('usage'), true) })
|
|
443
|
-
}
|
|
444
|
-
if (capabilities.skillsList) {
|
|
445
|
-
built.push({ name: 'skills', description: 'Browse the skills this session can use', run: () => (openPanel('skills'), true) })
|
|
446
|
-
}
|
|
447
|
-
return built
|
|
448
|
-
}, [capabilities, models.length, openPanel, clearContext, setModel, setPermissionMode])
|
|
353
|
+
const hasModels = models.length > 0
|
|
354
|
+
const clientCommands = useMemo(
|
|
355
|
+
() => buildClientCommands({ capabilities, hasModels, setModel, setPermissionMode, clearContext, openPanel }),
|
|
356
|
+
[capabilities, hasModels, openPanel, clearContext, setModel, setPermissionMode],
|
|
357
|
+
)
|
|
449
358
|
|
|
450
359
|
const composerCommands = useMemo(
|
|
451
360
|
() =>
|
|
@@ -466,45 +375,34 @@ export function SessionPanel({
|
|
|
466
375
|
() => permissionModeChoices(capabilities.permissionModes, state.session?.canBypassPermissions),
|
|
467
376
|
[capabilities.permissionModes, state.session?.canBypassPermissions],
|
|
468
377
|
)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
status: state.status,
|
|
472
|
-
connection,
|
|
473
|
-
engine: state.engine,
|
|
474
|
-
capabilities: state.capabilities,
|
|
475
|
-
model: vitalsModel,
|
|
476
|
-
models,
|
|
477
|
-
permissionMode: state.permissionMode,
|
|
478
|
-
permissionModes,
|
|
479
|
-
skills: state.skills,
|
|
480
|
-
composerCommands,
|
|
481
|
-
cwd: state.cwd,
|
|
482
|
-
contextUsage: state.contextUsage,
|
|
483
|
-
tasks,
|
|
484
|
-
rateLimits,
|
|
485
|
-
rateLimitsUpdatedAt: usageUpdatedAt,
|
|
486
|
-
itemCount: state.items.length,
|
|
487
|
-
totalCostUsd: state.totalCostUsd,
|
|
488
|
-
})
|
|
489
|
-
}, [
|
|
490
|
-
state.status,
|
|
378
|
+
const vitals: SessionVitals = {
|
|
379
|
+
status: state.status,
|
|
491
380
|
connection,
|
|
492
|
-
state.engine,
|
|
493
|
-
state.capabilities,
|
|
494
|
-
vitalsModel,
|
|
381
|
+
engine: state.engine,
|
|
382
|
+
capabilities: state.capabilities,
|
|
383
|
+
model: vitalsModel,
|
|
495
384
|
models,
|
|
496
|
-
state.permissionMode,
|
|
385
|
+
permissionMode: state.permissionMode,
|
|
497
386
|
permissionModes,
|
|
498
|
-
state.skills,
|
|
387
|
+
skills: state.skills,
|
|
499
388
|
composerCommands,
|
|
500
|
-
state.cwd,
|
|
501
|
-
state.contextUsage,
|
|
389
|
+
cwd: state.cwd,
|
|
390
|
+
contextUsage: state.contextUsage,
|
|
502
391
|
tasks,
|
|
503
392
|
rateLimits,
|
|
504
|
-
usageUpdatedAt,
|
|
505
|
-
state.items.length,
|
|
506
|
-
state.totalCostUsd,
|
|
507
|
-
|
|
393
|
+
rateLimitsUpdatedAt: usageUpdatedAt,
|
|
394
|
+
itemCount: state.items.length,
|
|
395
|
+
totalCostUsd: state.totalCostUsd,
|
|
396
|
+
}
|
|
397
|
+
const lastVitals = useRef<SessionVitals | undefined>(undefined)
|
|
398
|
+
// Every render, compared field by field: a streamed token changes none of them and never reaches the host.
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
if (lastVitals.current && sameVitals(lastVitals.current, vitals)) {
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
lastVitals.current = vitals
|
|
404
|
+
onVitalsRef.current?.(vitals)
|
|
405
|
+
})
|
|
508
406
|
|
|
509
407
|
const onControlsRef = useRef(onControls)
|
|
510
408
|
onControlsRef.current = onControls
|
|
@@ -560,11 +458,19 @@ export function SessionPanel({
|
|
|
560
458
|
return
|
|
561
459
|
}
|
|
562
460
|
}
|
|
563
|
-
|
|
461
|
+
dismissCatchUp()
|
|
564
462
|
repinTranscript.current?.()
|
|
565
463
|
send(text, attachmentIds)
|
|
566
464
|
}
|
|
567
465
|
|
|
466
|
+
const menuShows: Record<MenuPanel, boolean> = {
|
|
467
|
+
context: capabilities.contextUsage,
|
|
468
|
+
usage: capabilities.rateLimits,
|
|
469
|
+
info: true,
|
|
470
|
+
mcp: capabilities.mcpStatus,
|
|
471
|
+
skills: capabilities.skillsList,
|
|
472
|
+
files: hostFiles.available,
|
|
473
|
+
}
|
|
568
474
|
const actionsMenu = (
|
|
569
475
|
<Menu>
|
|
570
476
|
<MenuTrigger
|
|
@@ -575,39 +481,17 @@ export function SessionPanel({
|
|
|
575
481
|
}
|
|
576
482
|
/>
|
|
577
483
|
<MenuContent>
|
|
578
|
-
{
|
|
579
|
-
<MenuItem onClick={() => openPanel(
|
|
580
|
-
<
|
|
581
|
-
</MenuItem>
|
|
582
|
-
) : null}
|
|
583
|
-
{capabilities.rateLimits ? (
|
|
584
|
-
<MenuItem onClick={() => openPanel('usage')}>
|
|
585
|
-
<Gauge className="size-3.5 text-fg-3" /> Usage
|
|
586
|
-
</MenuItem>
|
|
587
|
-
) : null}
|
|
588
|
-
<MenuItem onClick={() => openPanel('info')}>
|
|
589
|
-
<Info className="size-3.5 text-fg-3" /> Session info
|
|
590
|
-
</MenuItem>
|
|
591
|
-
{capabilities.mcpStatus ? (
|
|
592
|
-
<MenuItem onClick={() => openPanel('mcp')}>
|
|
593
|
-
<Plug className="size-3.5 text-fg-3" /> MCP servers
|
|
594
|
-
</MenuItem>
|
|
595
|
-
) : null}
|
|
596
|
-
{capabilities.skillsList ? (
|
|
597
|
-
<MenuItem onClick={() => openPanel('skills')}>
|
|
598
|
-
<Sparkles className="size-3.5 text-fg-3" /> Skills
|
|
599
|
-
</MenuItem>
|
|
600
|
-
) : null}
|
|
601
|
-
{hostFiles.available ? (
|
|
602
|
-
<MenuItem onClick={() => openPanel('files')}>
|
|
603
|
-
<FolderTree className="size-3.5 text-fg-3" /> Project files
|
|
484
|
+
{PANEL_ITEMS.filter((item) => menuShows[item.panel]).map(({ panel: target, label, Icon }) => (
|
|
485
|
+
<MenuItem key={target} onClick={() => openPanel(target)}>
|
|
486
|
+
<Icon className="size-3.5 text-fg-3" /> {label}
|
|
604
487
|
</MenuItem>
|
|
605
|
-
)
|
|
488
|
+
))}
|
|
606
489
|
</MenuContent>
|
|
607
490
|
</Menu>
|
|
608
491
|
)
|
|
609
492
|
|
|
610
493
|
const menu = external ? null : actionsMenu
|
|
494
|
+
const dialog = (target: Panel) => ({ open: panel === target, onOpenChange: (next: boolean) => setPanel(next ? target : undefined) })
|
|
611
495
|
const headerTakesActions = typeof header === 'function'
|
|
612
496
|
|
|
613
497
|
const sessionControls = (
|
|
@@ -644,8 +528,6 @@ export function SessionPanel({
|
|
|
644
528
|
onOpenStatus={external && !onOpenPanel ? undefined : () => openPanel('info')}
|
|
645
529
|
onOpenContext={external && !onOpenPanel ? undefined : () => openPanel('context')}
|
|
646
530
|
onOpenUsage={external && !onOpenPanel ? undefined : () => openPanel('usage')}
|
|
647
|
-
tasks={tasks}
|
|
648
|
-
onOpenTasks={external && !onOpenPanel ? undefined : () => openPanel('tasks')}
|
|
649
531
|
actions={headerTakesActions ? undefined : menu}
|
|
650
532
|
/>
|
|
651
533
|
)
|
|
@@ -694,316 +576,190 @@ export function SessionPanel({
|
|
|
694
576
|
}
|
|
695
577
|
|
|
696
578
|
return (
|
|
697
|
-
<
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
<>
|
|
850
|
-
<Composer
|
|
851
|
-
ref={composerRef}
|
|
852
|
-
onSend={handleSend}
|
|
853
|
-
onInterrupt={interrupt}
|
|
854
|
-
busy={busy}
|
|
855
|
-
disabled={ended || !sessionId}
|
|
856
|
-
commands={capabilities.slashCommands ? state.commands : undefined}
|
|
857
|
-
skills={capabilities.skillsList ? state.skills : undefined}
|
|
858
|
-
clientCommands={clientCommands}
|
|
859
|
-
attachments={attachments}
|
|
860
|
-
draft={draft}
|
|
861
|
-
onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
|
|
862
|
-
peers={peers.peers}
|
|
863
|
-
onShellCommand={shell ? runShell : undefined}
|
|
864
|
-
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
865
|
-
toolbar={controlsExternal ? undefined : sessionControls}
|
|
866
|
-
fontSize={effectiveTermFontSize}
|
|
867
|
-
lineHeight={effectiveTermLineHeight}
|
|
868
|
-
affordances={affordances}
|
|
869
|
-
/>
|
|
870
|
-
</>
|
|
871
|
-
)}
|
|
872
|
-
{statusPlacement === 'bottom' ? statusBar : null}
|
|
873
|
-
|
|
874
|
-
{!external ? (
|
|
875
|
-
<>
|
|
876
|
-
<SessionInfoDialog
|
|
877
|
-
state={state}
|
|
878
|
-
client={client}
|
|
879
|
-
sessionId={sessionId}
|
|
880
|
-
open={panel === 'info'}
|
|
881
|
-
onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
|
|
882
|
-
/>
|
|
883
|
-
<ContextDialog
|
|
884
|
-
usage={state.contextUsage}
|
|
885
|
-
engine={state.engine ?? 'claude'}
|
|
886
|
-
open={panel === 'context'}
|
|
887
|
-
onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
|
|
888
|
-
/>
|
|
889
|
-
<UsageDialog
|
|
890
|
-
rateLimits={windows}
|
|
891
|
-
subscriptionType={state.subscriptionType}
|
|
892
|
-
engine={state.engine ?? 'claude'}
|
|
893
|
-
totalCostUsd={state.totalCostUsd}
|
|
894
|
-
costUsd={state.costUsd}
|
|
895
|
-
usageByModel={state.usageByModel}
|
|
896
|
-
pricing={pricing}
|
|
897
|
-
spend={profileSpend}
|
|
898
|
-
updatedAt={usageUpdatedAt}
|
|
899
|
-
open={panel === 'usage'}
|
|
900
|
-
onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
|
|
901
|
-
/>
|
|
902
|
-
<McpDialog
|
|
903
|
-
client={client}
|
|
904
|
-
sessionId={sessionId}
|
|
905
|
-
canManageServers={capabilities.mcpServerActions}
|
|
906
|
-
open={panel === 'mcp'}
|
|
907
|
-
onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
|
|
908
|
-
/>
|
|
909
|
-
<TasksDialog
|
|
910
|
-
tasks={tasks}
|
|
911
|
-
showCompleted={showCompletedTasks}
|
|
912
|
-
onShowCompletedChange={setShowCompletedTasks}
|
|
913
|
-
onSelectTask={(task) => {
|
|
914
|
-
if (task.toolUseId) {
|
|
915
|
-
setTaskReveal({ toolUseId: task.toolUseId, nonce: Date.now() })
|
|
916
|
-
}
|
|
917
|
-
}}
|
|
918
|
-
open={panel === 'tasks'}
|
|
919
|
-
onOpenChange={(next) => setPanel(next ? 'tasks' : undefined)}
|
|
920
|
-
/>
|
|
921
|
-
<SkillsDialog
|
|
922
|
-
skills={state.skills}
|
|
923
|
-
open={panel === 'skills'}
|
|
924
|
-
onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
|
|
925
|
-
onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
|
|
926
|
-
/>
|
|
927
|
-
<HostFilesDialog
|
|
928
|
-
client={client}
|
|
929
|
-
cwd={state.cwd}
|
|
930
|
-
open={panel === 'files'}
|
|
931
|
-
onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
|
|
932
|
-
/>
|
|
933
|
-
</>
|
|
934
|
-
) : null}
|
|
935
|
-
</ImageViewerProvider>
|
|
936
|
-
</div>
|
|
937
|
-
</ToolResultImageProvider>
|
|
938
|
-
</ToolTitleProvider>
|
|
939
|
-
</ShellActionsProvider>
|
|
940
|
-
</ToolResultFetchProvider>
|
|
941
|
-
</FileLinkProvider>
|
|
942
|
-
</TranscriptVariantProvider>
|
|
579
|
+
<SessionPanelProviders
|
|
580
|
+
variant={transcriptVariant}
|
|
581
|
+
fileLinks={fileLinks}
|
|
582
|
+
loadFullResult={loadFullResult}
|
|
583
|
+
shellActions={shellActions}
|
|
584
|
+
toolTitles={state.toolTitles}
|
|
585
|
+
resultImages={resultImages}
|
|
586
|
+
>
|
|
587
|
+
<div
|
|
588
|
+
ref={panelRef}
|
|
589
|
+
data-slot="session-panel"
|
|
590
|
+
data-agent-font={transcriptFont}
|
|
591
|
+
onClick={handleClick}
|
|
592
|
+
className={cn('relative flex h-full min-h-0 flex-col overflow-hidden bg-bg', className)}
|
|
593
|
+
style={fontSize !== undefined ? ({ '--wd-font-size': `${Math.round(fontSize)}px` } as React.CSSProperties) : undefined}
|
|
594
|
+
>
|
|
595
|
+
<ImageViewerProvider>
|
|
596
|
+
{headerTakesActions ? header({ actions: menu }) : header}
|
|
597
|
+
{statusPlacement === 'top' ? statusBar : null}
|
|
598
|
+
{protocolMismatch !== undefined ? (
|
|
599
|
+
<Notice level="warning">
|
|
600
|
+
Server speaks protocol v{protocolMismatch}, this build renders v{PROTOCOL_VERSION}. Some events may not render.
|
|
601
|
+
</Notice>
|
|
602
|
+
) : null}
|
|
603
|
+
{protocolError ? (
|
|
604
|
+
<Notice level="error" onDismiss={() => setProtocolError(undefined)}>
|
|
605
|
+
{protocolError}
|
|
606
|
+
</Notice>
|
|
607
|
+
) : null}
|
|
608
|
+
{framedShellId !== undefined ? (
|
|
609
|
+
<ShellStrip
|
|
610
|
+
shell={framedShell}
|
|
611
|
+
label={framedShellLabel}
|
|
612
|
+
onBack={leaveShell}
|
|
613
|
+
onKill={() => void killShell(framedShellId)}
|
|
614
|
+
onAgentWrite={agentWrite ? (enabled) => void agentWrite(framedShellId, enabled) : undefined}
|
|
615
|
+
terminal={terminal}
|
|
616
|
+
{...cell}
|
|
617
|
+
/>
|
|
618
|
+
) : null}
|
|
619
|
+
{framedShellId === undefined && subagentId !== undefined ? (
|
|
620
|
+
<SubagentStrip
|
|
621
|
+
task={subagentTask}
|
|
622
|
+
items={subagentFrameItems}
|
|
623
|
+
label={subagentFallbackLabel}
|
|
624
|
+
onBack={leaveSubagent}
|
|
625
|
+
terminal={terminal}
|
|
626
|
+
{...cell}
|
|
627
|
+
/>
|
|
628
|
+
) : null}
|
|
629
|
+
{framedShellId !== undefined ? (
|
|
630
|
+
<ShellTerminal key={framedShellId} handle={handle} shellId={framedShellId} fontSize={cell.fontSize} />
|
|
631
|
+
) : (
|
|
632
|
+
<BookmarkProvider value={bookmarkHandle}>
|
|
633
|
+
<Transcript
|
|
634
|
+
key={subagentId ?? 'session'}
|
|
635
|
+
state={state}
|
|
636
|
+
fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
|
|
637
|
+
attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
|
|
638
|
+
canBrowseFiles={hostFiles.available}
|
|
639
|
+
sessionNames={peers.names}
|
|
640
|
+
hostImage={hostImage}
|
|
641
|
+
variant={transcriptVariant}
|
|
642
|
+
{...cell}
|
|
643
|
+
affordances={affordances}
|
|
644
|
+
stickyPrompt={stickyPrompt}
|
|
645
|
+
scrubber={scrubber}
|
|
646
|
+
bookmarks={bookmarks}
|
|
647
|
+
replaying={replaying}
|
|
648
|
+
catchUp={catchUp && newCount > 0 ? { from: catchUp.itemCount, since: catchUp.since } : undefined}
|
|
649
|
+
reveal={frameReturnReveal ?? reveal}
|
|
650
|
+
frame={subagentId === undefined ? undefined : { parentToolUseId: subagentId }}
|
|
651
|
+
onOpenSubagent={enterSubagent}
|
|
652
|
+
emptyState={emptyState}
|
|
653
|
+
jumpToRecapRef={jumpToRecap}
|
|
654
|
+
repinRef={repinTranscript}
|
|
655
|
+
/>
|
|
656
|
+
</BookmarkProvider>
|
|
657
|
+
)}
|
|
658
|
+
{catchUp && newCount > 0 && !replaying && subagentId === undefined && framedShellId === undefined ? (
|
|
659
|
+
<CatchUpBanner
|
|
660
|
+
count={newCount}
|
|
661
|
+
since={catchUp.since}
|
|
662
|
+
terminal={terminal}
|
|
663
|
+
onJump={() => jumpToRecap.current?.()}
|
|
664
|
+
onDismiss={dismissCatchUp}
|
|
665
|
+
/>
|
|
666
|
+
) : null}
|
|
667
|
+
{!readOnly && capabilities.interactiveApprovals && state.pendingApprovals.length > 0 ? (
|
|
668
|
+
<ApprovalPrompts
|
|
669
|
+
requests={state.pendingApprovals}
|
|
670
|
+
terminal={terminal}
|
|
671
|
+
{...cell}
|
|
672
|
+
affordances={affordances}
|
|
673
|
+
hostPrompts={approvalPrompts}
|
|
674
|
+
onApprove={approve}
|
|
675
|
+
onDeny={deny}
|
|
676
|
+
/>
|
|
677
|
+
) : null}
|
|
678
|
+
{readOnly || subagentId !== undefined || framedShellId !== undefined ? null : (
|
|
679
|
+
<>
|
|
680
|
+
<Composer
|
|
681
|
+
ref={composerRef}
|
|
682
|
+
onSend={handleSend}
|
|
683
|
+
onInterrupt={interrupt}
|
|
684
|
+
busy={busy}
|
|
685
|
+
disabled={ended || !sessionId}
|
|
686
|
+
commands={capabilities.slashCommands ? state.commands : undefined}
|
|
687
|
+
skills={capabilities.skillsList ? state.skills : undefined}
|
|
688
|
+
clientCommands={clientCommands}
|
|
689
|
+
attachments={attachments}
|
|
690
|
+
draft={draft}
|
|
691
|
+
onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
|
|
692
|
+
peers={peers.peers}
|
|
693
|
+
onShellCommand={shell ? runShell : undefined}
|
|
694
|
+
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
695
|
+
toolbar={controlsExternal ? undefined : sessionControls}
|
|
696
|
+
{...cell}
|
|
697
|
+
affordances={affordances}
|
|
698
|
+
/>
|
|
699
|
+
</>
|
|
700
|
+
)}
|
|
701
|
+
{statusPlacement === 'bottom' ? statusBar : null}
|
|
702
|
+
|
|
703
|
+
{!external ? (
|
|
704
|
+
<>
|
|
705
|
+
<SessionInfoDialog state={state} client={client} sessionId={sessionId} {...dialog('info')} />
|
|
706
|
+
<ContextDialog usage={state.contextUsage} engine={state.engine ?? 'claude'} {...dialog('context')} />
|
|
707
|
+
<UsageDialog
|
|
708
|
+
rateLimits={windows}
|
|
709
|
+
subscriptionType={state.subscriptionType}
|
|
710
|
+
engine={state.engine ?? 'claude'}
|
|
711
|
+
totalCostUsd={state.totalCostUsd}
|
|
712
|
+
costUsd={state.costUsd}
|
|
713
|
+
usageByModel={state.usageByModel}
|
|
714
|
+
pricing={pricing}
|
|
715
|
+
spend={profileSpend}
|
|
716
|
+
updatedAt={usageUpdatedAt}
|
|
717
|
+
{...dialog('usage')}
|
|
718
|
+
/>
|
|
719
|
+
<McpDialog client={client} sessionId={sessionId} canManageServers={capabilities.mcpServerActions} {...dialog('mcp')} />
|
|
720
|
+
<SkillsDialog
|
|
721
|
+
skills={state.skills}
|
|
722
|
+
{...dialog('skills')}
|
|
723
|
+
onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
|
|
724
|
+
/>
|
|
725
|
+
<HostFilesDialog client={client} cwd={state.cwd} {...dialog('files')} />
|
|
726
|
+
</>
|
|
727
|
+
) : null}
|
|
728
|
+
</ImageViewerProvider>
|
|
729
|
+
</div>
|
|
730
|
+
</SessionPanelProviders>
|
|
943
731
|
)
|
|
944
732
|
}
|
|
945
733
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const pending =
|
|
971
|
-
produced && sessionId
|
|
972
|
-
? client
|
|
973
|
-
.readProducedFile(sessionId, produced.fileId)
|
|
974
|
-
.then((blob) => {
|
|
975
|
-
if (blob.size === 0) {
|
|
976
|
-
return undefined
|
|
977
|
-
}
|
|
978
|
-
const url = URL.createObjectURL(blob)
|
|
979
|
-
objectUrls.current.push(url)
|
|
980
|
-
return url
|
|
981
|
-
})
|
|
982
|
-
.catch(() => undefined)
|
|
983
|
-
: client
|
|
984
|
-
.readHostFile(path)
|
|
985
|
-
.then((file) => {
|
|
986
|
-
if (file.encoding !== 'base64') {
|
|
987
|
-
return undefined
|
|
988
|
-
}
|
|
989
|
-
const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
|
|
990
|
-
const mediaType = IMAGE_MEDIA_TYPES[extension]
|
|
991
|
-
return mediaType ? `data:${mediaType};base64,${file.content}` : undefined
|
|
992
|
-
})
|
|
993
|
-
.catch(() => undefined)
|
|
994
|
-
cache.current.set(key, pending)
|
|
995
|
-
return pending
|
|
996
|
-
},
|
|
997
|
-
[client, sessionId, producedFiles],
|
|
734
|
+
type CatchUpBannerProps = { count: number; since: number | undefined; terminal: boolean; onJump: () => void; onDismiss: () => void }
|
|
735
|
+
|
|
736
|
+
function CatchUpBanner({ count, since, terminal, onJump, onDismiss }: CatchUpBannerProps) {
|
|
737
|
+
return (
|
|
738
|
+
<div className="px-3 pb-1">
|
|
739
|
+
<div
|
|
740
|
+
data-slot="catch-up"
|
|
741
|
+
className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3"
|
|
742
|
+
>
|
|
743
|
+
<span aria-hidden className={cn('select-none', terminal ? 'text-fg-3' : 'text-accent')}>
|
|
744
|
+
※
|
|
745
|
+
</span>
|
|
746
|
+
<span className="min-w-0 flex-1 truncate">
|
|
747
|
+
{count} new {count === 1 ? 'row' : 'rows'}
|
|
748
|
+
{since !== undefined ? ` since you were last here` : ''}
|
|
749
|
+
</span>
|
|
750
|
+
<button type="button" onClick={onJump} className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline">
|
|
751
|
+
jump
|
|
752
|
+
</button>
|
|
753
|
+
<button type="button" onClick={onDismiss} className="shrink-0 underline-offset-2 hover:text-fg-1 hover:underline">
|
|
754
|
+
dismiss
|
|
755
|
+
</button>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
998
758
|
)
|
|
999
759
|
}
|
|
1000
760
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
jpg: 'image/jpeg',
|
|
1004
|
-
jpeg: 'image/jpeg',
|
|
1005
|
-
gif: 'image/gif',
|
|
1006
|
-
webp: 'image/webp',
|
|
761
|
+
function sameVitals(a: SessionVitals, b: SessionVitals): boolean {
|
|
762
|
+
return (Object.keys(b) as (keyof SessionVitals)[]).every((key) => Object.is(a[key], b[key]))
|
|
1007
763
|
}
|
|
1008
764
|
|
|
1009
765
|
function Notice({ level, onDismiss, children }: { level: 'warning' | 'error'; onDismiss?: () => void; children: ReactNode }) {
|