@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
|
@@ -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,95 +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 {
|
|
79
|
-
import {
|
|
80
|
-
import { ToolResultImageProvider, useToolResultImages } from './tool-result-image.tsx'
|
|
81
|
-
import {
|
|
82
|
-
TranscriptDensityProvider,
|
|
83
|
-
TranscriptVariantProvider,
|
|
84
|
-
type TranscriptDensity,
|
|
85
|
-
type TranscriptFont,
|
|
86
|
-
type TranscriptVariant,
|
|
87
|
-
} from './transcript-variant.tsx'
|
|
72
|
+
import { useToolResultImages } from './tool-result-image.tsx'
|
|
73
|
+
import { ImageViewerProvider } from './image-viewer.tsx'
|
|
74
|
+
import type { TranscriptFont, TranscriptVariant } from './transcript-variant.tsx'
|
|
88
75
|
import { UsageDialog } from './UsageDialog.tsx'
|
|
89
76
|
|
|
90
|
-
function PromptSurface({
|
|
91
|
-
terminal,
|
|
92
|
-
metrics,
|
|
93
|
-
affordances,
|
|
94
|
-
children,
|
|
95
|
-
}: {
|
|
96
|
-
terminal: boolean
|
|
97
|
-
metrics?: TerminalMetrics
|
|
98
|
-
affordances?: TerminalAffordances | boolean
|
|
99
|
-
children: ReactNode
|
|
100
|
-
}) {
|
|
101
|
-
if (!terminal) {
|
|
102
|
-
return <div className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-2">{children}</div>
|
|
103
|
-
}
|
|
104
|
-
return (
|
|
105
|
-
<TerminalSurface
|
|
106
|
-
fontSize={metrics?.fontSize}
|
|
107
|
-
lineHeight={metrics?.lineHeight}
|
|
108
|
-
affordances={affordances}
|
|
109
|
-
bleed="1ch"
|
|
110
|
-
className="term-transcript"
|
|
111
|
-
>
|
|
112
|
-
{children}
|
|
113
|
-
</TerminalSurface>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
77
|
export type TerminalMetrics = { fontSize?: number; lineHeight?: number }
|
|
118
78
|
|
|
119
|
-
|
|
120
|
-
// `TerminalPermissionPrompt` stay drop-in fallbacks for an input a host declines to draw.
|
|
121
|
-
export interface ApprovalPromptProps {
|
|
122
|
-
request: PermissionRequest
|
|
123
|
-
onApprove: (requestId: string, updatedInput?: Record<string, unknown>) => void
|
|
124
|
-
onDeny: (requestId: string, message?: string, interrupt?: boolean) => void
|
|
125
|
-
}
|
|
79
|
+
export type { ApprovalPromptProps }
|
|
126
80
|
|
|
127
81
|
export interface SessionPanelProps {
|
|
128
82
|
client: WorkerDeckClient
|
|
@@ -155,7 +109,6 @@ export interface SessionPanelProps {
|
|
|
155
109
|
subagents?: SubagentInfo[]
|
|
156
110
|
shells?: ShellInfo[]
|
|
157
111
|
stickyPrompt?: boolean
|
|
158
|
-
transcriptDensity?: TranscriptDensity
|
|
159
112
|
transcriptFont?: TranscriptFont
|
|
160
113
|
controlsSurface?: 'internal' | 'external' | 'status'
|
|
161
114
|
onControls?: (controls: SessionControls | undefined) => void
|
|
@@ -198,6 +151,16 @@ const INTERACTIVE = [
|
|
|
198
151
|
|
|
199
152
|
export type SessionSurfacePanel = 'info' | 'context' | 'usage' | 'mcp' | 'files' | 'skills' | 'tasks'
|
|
200
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
|
+
]
|
|
201
164
|
|
|
202
165
|
export type SessionVitals = {
|
|
203
166
|
status: TranscriptState['status']
|
|
@@ -233,7 +196,6 @@ export function SessionPanel({
|
|
|
233
196
|
onOpenPanel,
|
|
234
197
|
onVitals,
|
|
235
198
|
transcriptVariant = 'cards',
|
|
236
|
-
transcriptDensity = 'comfortable',
|
|
237
199
|
transcriptFont = 'sans',
|
|
238
200
|
affordances,
|
|
239
201
|
terminalMetrics,
|
|
@@ -264,8 +226,10 @@ export function SessionPanel({
|
|
|
264
226
|
fontSize,
|
|
265
227
|
className,
|
|
266
228
|
}: SessionPanelProps) {
|
|
267
|
-
const
|
|
268
|
-
|
|
229
|
+
const cell = {
|
|
230
|
+
fontSize: terminalMetrics?.fontSize ?? fontSize,
|
|
231
|
+
lineHeight: terminalMetrics?.lineHeight ?? (fontSize !== undefined ? Math.round(fontSize * (18 / 13)) : undefined),
|
|
232
|
+
}
|
|
269
233
|
|
|
270
234
|
const external = panelSurface === 'external'
|
|
271
235
|
const statusExternal = statusSurface === 'external'
|
|
@@ -273,8 +237,6 @@ export function SessionPanel({
|
|
|
273
237
|
const controlsExternal = controlsSurface === 'external' || controlsInStatus
|
|
274
238
|
const [protocolError, setProtocolError] = useState<string | undefined>(undefined)
|
|
275
239
|
const [panel, setPanel] = useState<Panel | undefined>()
|
|
276
|
-
const [showCompletedTasks, setShowCompletedTasks] = useState(false)
|
|
277
|
-
const [taskReveal, setTaskReveal] = useState<{ toolUseId: string; nonce: number } | undefined>()
|
|
278
240
|
const {
|
|
279
241
|
state,
|
|
280
242
|
connection,
|
|
@@ -298,6 +260,7 @@ export function SessionPanel({
|
|
|
298
260
|
loadShellOutput,
|
|
299
261
|
verifyShell,
|
|
300
262
|
killShell,
|
|
263
|
+
setShellAgentWrite,
|
|
301
264
|
} = useClaudeSession(client, sessionId, { onProtocolError: setProtocolError, cacheTranscript })
|
|
302
265
|
const {
|
|
303
266
|
shellId: framedShellId,
|
|
@@ -307,9 +270,10 @@ export function SessionPanel({
|
|
|
307
270
|
shell: framedShell,
|
|
308
271
|
label: framedShellLabel,
|
|
309
272
|
} = useShellFrame({ sessionId, items: state.items, session: state.session, shells, reveal, openShell, onShellChange })
|
|
273
|
+
const agentWrite = state.session?.shellAgentWrite !== undefined ? setShellAgentWrite : undefined
|
|
310
274
|
const shellActions = useMemo(
|
|
311
|
-
() => ({ loadOutput: loadShellOutput, verify: verifyShell, kill: killShell, open: enterShell }),
|
|
312
|
-
[loadShellOutput, verifyShell, killShell, enterShell],
|
|
275
|
+
() => ({ loadOutput: loadShellOutput, verify: verifyShell, kill: killShell, open: enterShell, agentWrite }),
|
|
276
|
+
[loadShellOutput, verifyShell, killShell, enterShell, agentWrite],
|
|
313
277
|
)
|
|
314
278
|
useEffect(() => setProtocolError(undefined), [sessionId])
|
|
315
279
|
const {
|
|
@@ -331,12 +295,7 @@ export function SessionPanel({
|
|
|
331
295
|
return returnReveal ?? shellReturnReveal
|
|
332
296
|
}, [returnReveal, shellReturnReveal])
|
|
333
297
|
|
|
334
|
-
const
|
|
335
|
-
useEffect(() => {
|
|
336
|
-
setCaughtUp(false)
|
|
337
|
-
}, [sessionId])
|
|
338
|
-
const [catchUpMark] = useState(unseen)
|
|
339
|
-
const catchUp = caughtUp ? undefined : catchUpMark
|
|
298
|
+
const { mark: catchUp, dismiss: dismissCatchUp } = useCatchUp(sessionId, unseen)
|
|
340
299
|
const newCount = catchUp ? Math.max(0, state.items.length - catchUp.itemCount) : 0
|
|
341
300
|
|
|
342
301
|
const openPanel = useCallback(
|
|
@@ -391,66 +350,11 @@ export function SessionPanel({
|
|
|
391
350
|
[state.checklist, subagents, state.session?.subagents],
|
|
392
351
|
)
|
|
393
352
|
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
name: 'model',
|
|
400
|
-
description: 'Switch the model for this session',
|
|
401
|
-
argumentHint: '<model>',
|
|
402
|
-
requiresArgs: true,
|
|
403
|
-
run: (args) => {
|
|
404
|
-
const wanted = args.split(/\s+/)[0]
|
|
405
|
-
if (!wanted) {
|
|
406
|
-
return false
|
|
407
|
-
}
|
|
408
|
-
setModel(wanted)
|
|
409
|
-
return true
|
|
410
|
-
},
|
|
411
|
-
})
|
|
412
|
-
}
|
|
413
|
-
if (modes.length > 1) {
|
|
414
|
-
built.push({
|
|
415
|
-
name: 'permissions',
|
|
416
|
-
description: `Set the permission mode (${modes.join(', ')})`,
|
|
417
|
-
argumentHint: '<mode>',
|
|
418
|
-
requiresArgs: true,
|
|
419
|
-
run: (args) => {
|
|
420
|
-
const wanted = args.split(/\s+/)[0] as PermissionMode
|
|
421
|
-
if (!modes.includes(wanted)) {
|
|
422
|
-
return false
|
|
423
|
-
}
|
|
424
|
-
setPermissionMode(wanted)
|
|
425
|
-
return true
|
|
426
|
-
},
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
if (capabilities.clearContext) {
|
|
430
|
-
built.push({
|
|
431
|
-
name: 'clear',
|
|
432
|
-
description: 'Clear the conversation - the session keeps running and the old one stays resumable',
|
|
433
|
-
run: () => {
|
|
434
|
-
clearContext()
|
|
435
|
-
return true
|
|
436
|
-
},
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
if (capabilities.mcpStatus) {
|
|
440
|
-
built.push({ name: 'mcp', description: 'MCP servers and their status', run: () => (openPanel('mcp'), true) })
|
|
441
|
-
}
|
|
442
|
-
if (capabilities.contextUsage) {
|
|
443
|
-
built.push({ name: 'context', description: 'Context window usage', run: () => (openPanel('context'), true) })
|
|
444
|
-
}
|
|
445
|
-
built.push({ name: 'status', description: 'Session details', run: () => (openPanel('info'), true) })
|
|
446
|
-
if (capabilities.rateLimits) {
|
|
447
|
-
built.push({ name: 'usage', description: 'Rate limits and spend', run: () => (openPanel('usage'), true) })
|
|
448
|
-
}
|
|
449
|
-
if (capabilities.skillsList) {
|
|
450
|
-
built.push({ name: 'skills', description: 'Browse the skills this session can use', run: () => (openPanel('skills'), true) })
|
|
451
|
-
}
|
|
452
|
-
return built
|
|
453
|
-
}, [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
|
+
)
|
|
454
358
|
|
|
455
359
|
const composerCommands = useMemo(
|
|
456
360
|
() =>
|
|
@@ -471,45 +375,34 @@ export function SessionPanel({
|
|
|
471
375
|
() => permissionModeChoices(capabilities.permissionModes, state.session?.canBypassPermissions),
|
|
472
376
|
[capabilities.permissionModes, state.session?.canBypassPermissions],
|
|
473
377
|
)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
status: state.status,
|
|
477
|
-
connection,
|
|
478
|
-
engine: state.engine,
|
|
479
|
-
capabilities: state.capabilities,
|
|
480
|
-
model: vitalsModel,
|
|
481
|
-
models,
|
|
482
|
-
permissionMode: state.permissionMode,
|
|
483
|
-
permissionModes,
|
|
484
|
-
skills: state.skills,
|
|
485
|
-
composerCommands,
|
|
486
|
-
cwd: state.cwd,
|
|
487
|
-
contextUsage: state.contextUsage,
|
|
488
|
-
tasks,
|
|
489
|
-
rateLimits,
|
|
490
|
-
rateLimitsUpdatedAt: usageUpdatedAt,
|
|
491
|
-
itemCount: state.items.length,
|
|
492
|
-
totalCostUsd: state.totalCostUsd,
|
|
493
|
-
})
|
|
494
|
-
}, [
|
|
495
|
-
state.status,
|
|
378
|
+
const vitals: SessionVitals = {
|
|
379
|
+
status: state.status,
|
|
496
380
|
connection,
|
|
497
|
-
state.engine,
|
|
498
|
-
state.capabilities,
|
|
499
|
-
vitalsModel,
|
|
381
|
+
engine: state.engine,
|
|
382
|
+
capabilities: state.capabilities,
|
|
383
|
+
model: vitalsModel,
|
|
500
384
|
models,
|
|
501
|
-
state.permissionMode,
|
|
385
|
+
permissionMode: state.permissionMode,
|
|
502
386
|
permissionModes,
|
|
503
|
-
state.skills,
|
|
387
|
+
skills: state.skills,
|
|
504
388
|
composerCommands,
|
|
505
|
-
state.cwd,
|
|
506
|
-
state.contextUsage,
|
|
389
|
+
cwd: state.cwd,
|
|
390
|
+
contextUsage: state.contextUsage,
|
|
507
391
|
tasks,
|
|
508
392
|
rateLimits,
|
|
509
|
-
usageUpdatedAt,
|
|
510
|
-
state.items.length,
|
|
511
|
-
state.totalCostUsd,
|
|
512
|
-
|
|
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
|
+
})
|
|
513
406
|
|
|
514
407
|
const onControlsRef = useRef(onControls)
|
|
515
408
|
onControlsRef.current = onControls
|
|
@@ -565,11 +458,19 @@ export function SessionPanel({
|
|
|
565
458
|
return
|
|
566
459
|
}
|
|
567
460
|
}
|
|
568
|
-
|
|
461
|
+
dismissCatchUp()
|
|
569
462
|
repinTranscript.current?.()
|
|
570
463
|
send(text, attachmentIds)
|
|
571
464
|
}
|
|
572
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
|
+
}
|
|
573
474
|
const actionsMenu = (
|
|
574
475
|
<Menu>
|
|
575
476
|
<MenuTrigger
|
|
@@ -580,39 +481,17 @@ export function SessionPanel({
|
|
|
580
481
|
}
|
|
581
482
|
/>
|
|
582
483
|
<MenuContent>
|
|
583
|
-
{
|
|
584
|
-
<MenuItem onClick={() => openPanel(
|
|
585
|
-
<
|
|
586
|
-
</MenuItem>
|
|
587
|
-
) : null}
|
|
588
|
-
{capabilities.rateLimits ? (
|
|
589
|
-
<MenuItem onClick={() => openPanel('usage')}>
|
|
590
|
-
<Gauge className="size-3.5 text-fg-3" /> Usage
|
|
591
|
-
</MenuItem>
|
|
592
|
-
) : null}
|
|
593
|
-
<MenuItem onClick={() => openPanel('info')}>
|
|
594
|
-
<Info className="size-3.5 text-fg-3" /> Session info
|
|
595
|
-
</MenuItem>
|
|
596
|
-
{capabilities.mcpStatus ? (
|
|
597
|
-
<MenuItem onClick={() => openPanel('mcp')}>
|
|
598
|
-
<Plug className="size-3.5 text-fg-3" /> MCP servers
|
|
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}
|
|
599
487
|
</MenuItem>
|
|
600
|
-
)
|
|
601
|
-
{capabilities.skillsList ? (
|
|
602
|
-
<MenuItem onClick={() => openPanel('skills')}>
|
|
603
|
-
<Sparkles className="size-3.5 text-fg-3" /> Skills
|
|
604
|
-
</MenuItem>
|
|
605
|
-
) : null}
|
|
606
|
-
{hostFiles.available ? (
|
|
607
|
-
<MenuItem onClick={() => openPanel('files')}>
|
|
608
|
-
<FolderTree className="size-3.5 text-fg-3" /> Project files
|
|
609
|
-
</MenuItem>
|
|
610
|
-
) : null}
|
|
488
|
+
))}
|
|
611
489
|
</MenuContent>
|
|
612
490
|
</Menu>
|
|
613
491
|
)
|
|
614
492
|
|
|
615
493
|
const menu = external ? null : actionsMenu
|
|
494
|
+
const dialog = (target: Panel) => ({ open: panel === target, onOpenChange: (next: boolean) => setPanel(next ? target : undefined) })
|
|
616
495
|
const headerTakesActions = typeof header === 'function'
|
|
617
496
|
|
|
618
497
|
const sessionControls = (
|
|
@@ -649,8 +528,6 @@ export function SessionPanel({
|
|
|
649
528
|
onOpenStatus={external && !onOpenPanel ? undefined : () => openPanel('info')}
|
|
650
529
|
onOpenContext={external && !onOpenPanel ? undefined : () => openPanel('context')}
|
|
651
530
|
onOpenUsage={external && !onOpenPanel ? undefined : () => openPanel('usage')}
|
|
652
|
-
tasks={tasks}
|
|
653
|
-
onOpenTasks={external && !onOpenPanel ? undefined : () => openPanel('tasks')}
|
|
654
531
|
actions={headerTakesActions ? undefined : menu}
|
|
655
532
|
/>
|
|
656
533
|
)
|
|
@@ -699,316 +576,190 @@ export function SessionPanel({
|
|
|
699
576
|
}
|
|
700
577
|
|
|
701
578
|
return (
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
<>
|
|
855
|
-
<Composer
|
|
856
|
-
ref={composerRef}
|
|
857
|
-
onSend={handleSend}
|
|
858
|
-
onInterrupt={interrupt}
|
|
859
|
-
busy={busy}
|
|
860
|
-
disabled={ended || !sessionId}
|
|
861
|
-
commands={capabilities.slashCommands ? state.commands : undefined}
|
|
862
|
-
skills={capabilities.skillsList ? state.skills : undefined}
|
|
863
|
-
clientCommands={clientCommands}
|
|
864
|
-
attachments={attachments}
|
|
865
|
-
draft={draft}
|
|
866
|
-
onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
|
|
867
|
-
peers={peers.peers}
|
|
868
|
-
onShellCommand={shell ? runShell : undefined}
|
|
869
|
-
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
870
|
-
toolbar={controlsExternal ? undefined : sessionControls}
|
|
871
|
-
fontSize={effectiveTermFontSize}
|
|
872
|
-
lineHeight={effectiveTermLineHeight}
|
|
873
|
-
affordances={affordances}
|
|
874
|
-
/>
|
|
875
|
-
</>
|
|
876
|
-
)}
|
|
877
|
-
{statusPlacement === 'bottom' ? statusBar : null}
|
|
878
|
-
|
|
879
|
-
{!external ? (
|
|
880
|
-
<>
|
|
881
|
-
<SessionInfoDialog
|
|
882
|
-
state={state}
|
|
883
|
-
client={client}
|
|
884
|
-
sessionId={sessionId}
|
|
885
|
-
open={panel === 'info'}
|
|
886
|
-
onOpenChange={(next) => setPanel(next ? 'info' : undefined)}
|
|
887
|
-
/>
|
|
888
|
-
<ContextDialog
|
|
889
|
-
usage={state.contextUsage}
|
|
890
|
-
engine={state.engine ?? 'claude'}
|
|
891
|
-
open={panel === 'context'}
|
|
892
|
-
onOpenChange={(next) => setPanel(next ? 'context' : undefined)}
|
|
893
|
-
/>
|
|
894
|
-
<UsageDialog
|
|
895
|
-
rateLimits={windows}
|
|
896
|
-
subscriptionType={state.subscriptionType}
|
|
897
|
-
engine={state.engine ?? 'claude'}
|
|
898
|
-
totalCostUsd={state.totalCostUsd}
|
|
899
|
-
costUsd={state.costUsd}
|
|
900
|
-
usageByModel={state.usageByModel}
|
|
901
|
-
pricing={pricing}
|
|
902
|
-
spend={profileSpend}
|
|
903
|
-
updatedAt={usageUpdatedAt}
|
|
904
|
-
open={panel === 'usage'}
|
|
905
|
-
onOpenChange={(next) => setPanel(next ? 'usage' : undefined)}
|
|
906
|
-
/>
|
|
907
|
-
<McpDialog
|
|
908
|
-
client={client}
|
|
909
|
-
sessionId={sessionId}
|
|
910
|
-
canManageServers={capabilities.mcpServerActions}
|
|
911
|
-
open={panel === 'mcp'}
|
|
912
|
-
onOpenChange={(next) => setPanel(next ? 'mcp' : undefined)}
|
|
913
|
-
/>
|
|
914
|
-
<TasksDialog
|
|
915
|
-
tasks={tasks}
|
|
916
|
-
showCompleted={showCompletedTasks}
|
|
917
|
-
onShowCompletedChange={setShowCompletedTasks}
|
|
918
|
-
onSelectTask={(task) => {
|
|
919
|
-
if (task.toolUseId) {
|
|
920
|
-
setTaskReveal({ toolUseId: task.toolUseId, nonce: Date.now() })
|
|
921
|
-
}
|
|
922
|
-
}}
|
|
923
|
-
open={panel === 'tasks'}
|
|
924
|
-
onOpenChange={(next) => setPanel(next ? 'tasks' : undefined)}
|
|
925
|
-
/>
|
|
926
|
-
<SkillsDialog
|
|
927
|
-
skills={state.skills}
|
|
928
|
-
open={panel === 'skills'}
|
|
929
|
-
onOpenChange={(next) => setPanel(next ? 'skills' : undefined)}
|
|
930
|
-
onUse={(skill) => composerRef.current?.insertText(skillPrompt(skill))}
|
|
931
|
-
/>
|
|
932
|
-
<HostFilesDialog
|
|
933
|
-
client={client}
|
|
934
|
-
cwd={state.cwd}
|
|
935
|
-
open={panel === 'files'}
|
|
936
|
-
onOpenChange={(next) => setPanel(next ? 'files' : undefined)}
|
|
937
|
-
/>
|
|
938
|
-
</>
|
|
939
|
-
) : null}
|
|
940
|
-
</div>
|
|
941
|
-
</ToolResultImageProvider>
|
|
942
|
-
</ToolTitleProvider>
|
|
943
|
-
</ShellActionsProvider>
|
|
944
|
-
</ToolResultFetchProvider>
|
|
945
|
-
</FileLinkProvider>
|
|
946
|
-
</TranscriptDensityProvider>
|
|
947
|
-
</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>
|
|
948
731
|
)
|
|
949
732
|
}
|
|
950
733
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const pending =
|
|
976
|
-
produced && sessionId
|
|
977
|
-
? client
|
|
978
|
-
.readProducedFile(sessionId, produced.fileId)
|
|
979
|
-
.then((blob) => {
|
|
980
|
-
if (blob.size === 0) {
|
|
981
|
-
return undefined
|
|
982
|
-
}
|
|
983
|
-
const url = URL.createObjectURL(blob)
|
|
984
|
-
objectUrls.current.push(url)
|
|
985
|
-
return url
|
|
986
|
-
})
|
|
987
|
-
.catch(() => undefined)
|
|
988
|
-
: client
|
|
989
|
-
.readHostFile(path)
|
|
990
|
-
.then((file) => {
|
|
991
|
-
if (file.encoding !== 'base64') {
|
|
992
|
-
return undefined
|
|
993
|
-
}
|
|
994
|
-
const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
|
|
995
|
-
const mediaType = IMAGE_MEDIA_TYPES[extension]
|
|
996
|
-
return mediaType ? `data:${mediaType};base64,${file.content}` : undefined
|
|
997
|
-
})
|
|
998
|
-
.catch(() => undefined)
|
|
999
|
-
cache.current.set(key, pending)
|
|
1000
|
-
return pending
|
|
1001
|
-
},
|
|
1002
|
-
[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>
|
|
1003
758
|
)
|
|
1004
759
|
}
|
|
1005
760
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
jpg: 'image/jpeg',
|
|
1009
|
-
jpeg: 'image/jpeg',
|
|
1010
|
-
gif: 'image/gif',
|
|
1011
|
-
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]))
|
|
1012
763
|
}
|
|
1013
764
|
|
|
1014
765
|
function Notice({ level, onDismiss, children }: { level: 'warning' | 'error'; onDismiss?: () => void; children: ReactNode }) {
|