@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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { Filter, X } from 'lucide-react'
|
|
4
|
+
import { STATE_LABELS, STATE_ORDER, adaptersOf, displayCustomized, facetFilterCount, projectsOf } from '@workerdeck/protocol'
|
|
5
|
+
import type { GroupBy, SessionRow, SessionState, SortBy, StepDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
6
|
+
import { Button } from '../ui/Button.tsx'
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/Popover.tsx'
|
|
8
|
+
import { OptionSelect, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
9
|
+
import { cn } from '../../lib/utils.ts'
|
|
10
|
+
|
|
11
|
+
export type SessionFiltersProps = {
|
|
12
|
+
config: ViewConfig
|
|
13
|
+
onConfigChange: (config: ViewConfig) => void
|
|
14
|
+
rows: readonly SessionRow[]
|
|
15
|
+
scope?: WorkspaceScope
|
|
16
|
+
gateways?: readonly { id: string; name: string }[]
|
|
17
|
+
className?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const STEP_DISPLAY_OPTIONS: readonly { value: StepDisplay; label: string }[] = [
|
|
21
|
+
{ value: 'all', label: 'All' },
|
|
22
|
+
{ value: 'active', label: 'Active' },
|
|
23
|
+
{ value: 'none', label: 'Hide' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export function SessionFilters({ config, onConfigChange, rows, scope, gateways: gatewaysProp, className }: SessionFiltersProps) {
|
|
27
|
+
const adapters = useMemo(() => adaptersOf(rows), [rows])
|
|
28
|
+
const projects = useMemo(() => projectsOf(rows), [rows])
|
|
29
|
+
const gateways = useMemo(() => {
|
|
30
|
+
if (gatewaysProp) {
|
|
31
|
+
return gatewaysProp
|
|
32
|
+
}
|
|
33
|
+
const seen = new Map<string, string>()
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
seen.set(row.hostId, row.hostName)
|
|
36
|
+
}
|
|
37
|
+
return [...seen].map(([id, name]) => ({ id, name }))
|
|
38
|
+
}, [rows, gatewaysProp])
|
|
39
|
+
|
|
40
|
+
const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div data-slot="session-filters" className={cn('flex flex-col gap-3', className)}>
|
|
44
|
+
<Section title="Filter">
|
|
45
|
+
{scope ? (
|
|
46
|
+
<FilterRow label="Scope">
|
|
47
|
+
<OptionSelect
|
|
48
|
+
className="w-full min-w-0"
|
|
49
|
+
label="Scope"
|
|
50
|
+
value={config.scoped ? 'scoped' : 'all'}
|
|
51
|
+
options={[
|
|
52
|
+
{ value: 'scoped', label: scope.label },
|
|
53
|
+
{ value: 'all', label: 'All folders' },
|
|
54
|
+
]}
|
|
55
|
+
onChange={(value) => set({ scoped: value === 'scoped' })}
|
|
56
|
+
/>
|
|
57
|
+
</FilterRow>
|
|
58
|
+
) : null}
|
|
59
|
+
<FilterRow label="State">
|
|
60
|
+
<FacetSelect
|
|
61
|
+
label="State"
|
|
62
|
+
value={config.states}
|
|
63
|
+
options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}
|
|
64
|
+
onChange={(states) => set({ states: states as SessionState[] })}
|
|
65
|
+
/>
|
|
66
|
+
</FilterRow>
|
|
67
|
+
{adapters.length > 1 ? (
|
|
68
|
+
<FilterRow label="Engine">
|
|
69
|
+
<FacetSelect
|
|
70
|
+
label="Engine"
|
|
71
|
+
value={config.adapters}
|
|
72
|
+
options={adapters.map((a) => ({ value: a, label: a }))}
|
|
73
|
+
onChange={(next) => set({ adapters: next })}
|
|
74
|
+
/>
|
|
75
|
+
</FilterRow>
|
|
76
|
+
) : null}
|
|
77
|
+
{gateways.length > 1 ? (
|
|
78
|
+
<FilterRow label="Gateway">
|
|
79
|
+
<FacetSelect
|
|
80
|
+
label="Gateway"
|
|
81
|
+
value={config.gateways}
|
|
82
|
+
options={gateways.map((g) => ({ value: g.id, label: g.name }))}
|
|
83
|
+
onChange={(next) => set({ gateways: next })}
|
|
84
|
+
/>
|
|
85
|
+
</FilterRow>
|
|
86
|
+
) : null}
|
|
87
|
+
{projects.length > 1 ? (
|
|
88
|
+
<FilterRow label="Project">
|
|
89
|
+
<FacetSelect
|
|
90
|
+
label="Project"
|
|
91
|
+
value={config.projects ?? []}
|
|
92
|
+
options={projects.map((p) => ({ value: p.key, label: p.label }))}
|
|
93
|
+
onChange={(next) => set({ projects: next })}
|
|
94
|
+
/>
|
|
95
|
+
</FilterRow>
|
|
96
|
+
) : null}
|
|
97
|
+
</Section>
|
|
98
|
+
|
|
99
|
+
<Section title="Layout">
|
|
100
|
+
<FilterRow label="Group">
|
|
101
|
+
<OptionSelect
|
|
102
|
+
className="w-full min-w-0"
|
|
103
|
+
label="Group"
|
|
104
|
+
value={config.groupBy}
|
|
105
|
+
options={[
|
|
106
|
+
{ value: 'none', label: 'No grouping' },
|
|
107
|
+
{ value: 'state', label: 'By state' },
|
|
108
|
+
{ value: 'adapter', label: 'By engine' },
|
|
109
|
+
...(projects.length > 1 ? [{ value: 'project' as const, label: 'By project' }] : []),
|
|
110
|
+
...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'By gateway' }] : []),
|
|
111
|
+
]}
|
|
112
|
+
onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
|
|
113
|
+
/>
|
|
114
|
+
</FilterRow>
|
|
115
|
+
<FilterRow label="Sort">
|
|
116
|
+
<OptionSelect
|
|
117
|
+
className="w-full min-w-0"
|
|
118
|
+
label="Sort"
|
|
119
|
+
value={config.sortBy}
|
|
120
|
+
options={[
|
|
121
|
+
{ value: 'recent', label: 'Recent' },
|
|
122
|
+
{ value: 'name', label: 'Name' },
|
|
123
|
+
{ value: 'state', label: 'State' },
|
|
124
|
+
...(projects.length > 1 ? [{ value: 'project' as const, label: 'Project' }] : []),
|
|
125
|
+
...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'Gateway' }] : []),
|
|
126
|
+
]}
|
|
127
|
+
onChange={(sortBy) => set({ sortBy: sortBy as SortBy })}
|
|
128
|
+
/>
|
|
129
|
+
</FilterRow>
|
|
130
|
+
</Section>
|
|
131
|
+
|
|
132
|
+
<Section title="Show under each session">
|
|
133
|
+
<FilterRow label="Agents">
|
|
134
|
+
<StepDisplayControl label="Agents" value={config.subagents} onChange={(subagents) => set({ subagents })} />
|
|
135
|
+
</FilterRow>
|
|
136
|
+
<FilterRow label="Shells">
|
|
137
|
+
<StepDisplayControl label="Shells" value={config.shells ?? 'active'} onChange={(shells) => set({ shells })} />
|
|
138
|
+
</FilterRow>
|
|
139
|
+
<FilterRow label="Tasks">
|
|
140
|
+
<StepDisplayControl label="Tasks" value={config.tasks ?? 'active'} onChange={(tasks) => set({ tasks })} />
|
|
141
|
+
</FilterRow>
|
|
142
|
+
</Section>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type SessionFiltersButtonProps = SessionFiltersProps & {
|
|
148
|
+
open?: boolean
|
|
149
|
+
onOpenChange?: (open: boolean) => void
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function SessionFiltersButton({ open, onOpenChange, className, ...props }: SessionFiltersButtonProps) {
|
|
153
|
+
const count = facetFilterCount(props.config)
|
|
154
|
+
const engaged = count > 0 || displayCustomized(props.config)
|
|
155
|
+
return (
|
|
156
|
+
<Popover open={open} onOpenChange={onOpenChange}>
|
|
157
|
+
<PopoverTrigger
|
|
158
|
+
render={
|
|
159
|
+
<Button
|
|
160
|
+
variant="ghost"
|
|
161
|
+
size="icon-sm"
|
|
162
|
+
aria-label={count > 0 ? `Filters (${count} active)` : 'Filters'}
|
|
163
|
+
title={count > 0 ? `${count} filter${count === 1 ? '' : 's'} active` : 'Filters'}
|
|
164
|
+
className={className}
|
|
165
|
+
>
|
|
166
|
+
<Filter className={cn('size-3.5', engaged && 'fill-current text-fg-1')} />
|
|
167
|
+
</Button>
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
<PopoverContent className="w-80">
|
|
171
|
+
<SessionFilters {...props} />
|
|
172
|
+
</PopoverContent>
|
|
173
|
+
</Popover>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function StepDisplayControl({
|
|
178
|
+
label,
|
|
179
|
+
value,
|
|
180
|
+
onChange,
|
|
181
|
+
}: {
|
|
182
|
+
label: string
|
|
183
|
+
value: StepDisplay
|
|
184
|
+
onChange: (value: StepDisplay) => void
|
|
185
|
+
}) {
|
|
186
|
+
return (
|
|
187
|
+
<div role="radiogroup" aria-label={label} className="flex w-full rounded-md border border-border p-px">
|
|
188
|
+
{STEP_DISPLAY_OPTIONS.map((option) => (
|
|
189
|
+
<button
|
|
190
|
+
key={option.value}
|
|
191
|
+
type="button"
|
|
192
|
+
role="radio"
|
|
193
|
+
aria-checked={option.value === value}
|
|
194
|
+
onClick={() => onChange(option.value)}
|
|
195
|
+
className={cn(
|
|
196
|
+
'min-w-0 flex-1 truncate rounded-[4px] px-1.5 py-0.5 text-label outline-none transition-colors',
|
|
197
|
+
'focus-visible:ring-2 focus-visible:ring-ring',
|
|
198
|
+
option.value === value ? 'bg-row-selected text-fg-1' : 'text-fg-3 hover:bg-row-hover hover:text-fg-1',
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
{option.label}
|
|
202
|
+
</button>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="flex flex-col gap-1.5">
|
|
211
|
+
<div className="text-label font-medium uppercase tracking-wide text-fg-4">{title}</div>
|
|
212
|
+
{children}
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function FilterRow({ label, children }: { label: string; children: ReactNode }) {
|
|
218
|
+
return (
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<span aria-hidden className="w-16 shrink-0 truncate text-label text-fg-3">
|
|
221
|
+
{label}
|
|
222
|
+
</span>
|
|
223
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function FacetSelect({
|
|
229
|
+
label,
|
|
230
|
+
value,
|
|
231
|
+
options,
|
|
232
|
+
onChange,
|
|
233
|
+
}: {
|
|
234
|
+
label: string
|
|
235
|
+
value: string[]
|
|
236
|
+
options: { value: string; label: string }[]
|
|
237
|
+
onChange: (value: string[]) => void
|
|
238
|
+
}) {
|
|
239
|
+
return (
|
|
240
|
+
<div className="flex items-center gap-1">
|
|
241
|
+
<Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>
|
|
242
|
+
<SelectTrigger aria-label={label} className="min-w-0 flex-1">
|
|
243
|
+
<SelectValue>
|
|
244
|
+
{value.length === 0
|
|
245
|
+
? 'All'
|
|
246
|
+
: value.length === 1
|
|
247
|
+
? (options.find((o) => o.value === value[0])?.label ?? 'All')
|
|
248
|
+
: `${value.length} selected`}
|
|
249
|
+
</SelectValue>
|
|
250
|
+
</SelectTrigger>
|
|
251
|
+
<SelectContent>
|
|
252
|
+
{options.map((option) => (
|
|
253
|
+
<SelectItem key={option.value} value={option.value}>
|
|
254
|
+
<SelectItemText>{option.label}</SelectItemText>
|
|
255
|
+
</SelectItem>
|
|
256
|
+
))}
|
|
257
|
+
</SelectContent>
|
|
258
|
+
</Select>
|
|
259
|
+
{value.length > 0 ? (
|
|
260
|
+
<Button variant="ghost" size="icon-sm" aria-label={`Clear ${label}`} onClick={() => onChange([])}>
|
|
261
|
+
<X className="size-3 text-fg-4" />
|
|
262
|
+
</Button>
|
|
263
|
+
) : null}
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
1
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
3
|
-
import type
|
|
4
|
-
import type { TranscriptState } from '@workerdeck/react'
|
|
2
|
+
import { useAsync, type TranscriptState } from '@workerdeck/react'
|
|
5
3
|
import { Download } from 'lucide-react'
|
|
6
4
|
import { CopyButton } from '../ui/CopyButton.tsx'
|
|
7
5
|
import { Dialog, DialogBody, DialogContent, DialogHeader, DialogRow } from '../ui/Dialog.tsx'
|
|
@@ -17,46 +15,59 @@ export interface SessionInfoDialogProps {
|
|
|
17
15
|
onOpenChange: (open: boolean) => void
|
|
18
16
|
}
|
|
19
17
|
|
|
18
|
+
export interface SessionInfoPanelProps {
|
|
19
|
+
state: TranscriptState
|
|
20
|
+
client: WorkerDeckClient
|
|
21
|
+
sessionId: string | undefined
|
|
22
|
+
active?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export function SessionInfoDialog({ state, client, sessionId, open, onOpenChange }: SessionInfoDialogProps) {
|
|
21
|
-
const session = state.session
|
|
22
|
-
const mode = state.permissionMode ? permissionModeMeta(state.permissionMode) : undefined
|
|
23
26
|
return (
|
|
24
27
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
25
28
|
<DialogContent>
|
|
26
|
-
<DialogHeader title="Session info" description={session?.title} />
|
|
29
|
+
<DialogHeader title="Session info" description={state.session?.title} />
|
|
27
30
|
<DialogBody>
|
|
28
|
-
<
|
|
29
|
-
<div className="pb-2">
|
|
30
|
-
<DialogRow label="Engine">{state.engine ?? 'claude'}</DialogRow>
|
|
31
|
-
{session?.profile ? <DialogRow label="Profile">{session.profile}</DialogRow> : null}
|
|
32
|
-
{state.model ? (
|
|
33
|
-
<DialogRow label="Model" mono>
|
|
34
|
-
{state.model}
|
|
35
|
-
</DialogRow>
|
|
36
|
-
) : null}
|
|
37
|
-
{mode ? <DialogRow label="Permission mode">{mode.label}</DialogRow> : null}
|
|
38
|
-
{session?.apiKeySource ? <DialogRow label="Credentials">{session.apiKeySource}</DialogRow> : null}
|
|
39
|
-
</div>
|
|
40
|
-
<div className="py-2">
|
|
41
|
-
{state.cwd ? <CopyRow label="Working directory" value={state.cwd} /> : null}
|
|
42
|
-
{state.sdkSessionId ? <CopyRow label="Engine session id" value={state.sdkSessionId} /> : null}
|
|
43
|
-
{session ? <CopyRow label="Gateway session id" value={session.id} /> : null}
|
|
44
|
-
</div>
|
|
45
|
-
<div className="py-2">
|
|
46
|
-
{session?.createdAt ? <DialogRow label="Started">{formatRelativeTime(session.createdAt)}</DialogRow> : null}
|
|
47
|
-
{session?.numTurns !== undefined ? <DialogRow label="Turns">{session.numTurns}</DialogRow> : null}
|
|
48
|
-
<DialogRow label="Cost" mono>
|
|
49
|
-
{formatCost(state.costUsd ?? (state.totalCostUsd || undefined))}
|
|
50
|
-
</DialogRow>
|
|
51
|
-
</div>
|
|
52
|
-
{state.capabilities.vfs ? <SessionFiles client={client} sessionId={sessionId} open={open} /> : null}
|
|
53
|
-
</div>
|
|
31
|
+
<SessionInfoPanel state={state} client={client} sessionId={sessionId} active={open} />
|
|
54
32
|
</DialogBody>
|
|
55
33
|
</DialogContent>
|
|
56
34
|
</Dialog>
|
|
57
35
|
)
|
|
58
36
|
}
|
|
59
37
|
|
|
38
|
+
export function SessionInfoPanel({ state, client, sessionId, active = true }: SessionInfoPanelProps) {
|
|
39
|
+
const session = state.session
|
|
40
|
+
const mode = state.permissionMode ? permissionModeMeta(state.permissionMode) : undefined
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col divide-y divide-border">
|
|
43
|
+
<div className="pb-2">
|
|
44
|
+
<DialogRow label="Engine">{state.engine ?? 'claude'}</DialogRow>
|
|
45
|
+
{session?.profile ? <DialogRow label="Profile">{session.profile}</DialogRow> : null}
|
|
46
|
+
{state.model ? (
|
|
47
|
+
<DialogRow label="Model" mono>
|
|
48
|
+
{state.model}
|
|
49
|
+
</DialogRow>
|
|
50
|
+
) : null}
|
|
51
|
+
{mode ? <DialogRow label="Permission mode">{mode.label}</DialogRow> : null}
|
|
52
|
+
{session?.apiKeySource ? <DialogRow label="Credentials">{session.apiKeySource}</DialogRow> : null}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="py-2">
|
|
55
|
+
{state.cwd ? <CopyRow label="Working directory" value={state.cwd} /> : null}
|
|
56
|
+
{state.sdkSessionId ? <CopyRow label="Engine session id" value={state.sdkSessionId} /> : null}
|
|
57
|
+
{session ? <CopyRow label="Gateway session id" value={session.id} /> : null}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="py-2">
|
|
60
|
+
{session?.createdAt ? <DialogRow label="Started">{formatRelativeTime(session.createdAt)}</DialogRow> : null}
|
|
61
|
+
{session?.numTurns !== undefined ? <DialogRow label="Turns">{session.numTurns}</DialogRow> : null}
|
|
62
|
+
<DialogRow label="Cost" mono>
|
|
63
|
+
{formatCost(state.costUsd ?? (state.totalCostUsd || undefined))}
|
|
64
|
+
</DialogRow>
|
|
65
|
+
</div>
|
|
66
|
+
{state.capabilities.vfs ? <SessionFiles client={client} sessionId={sessionId} active={active} /> : null}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
function CopyRow({ label, value }: { label: string; value: string }) {
|
|
61
72
|
return (
|
|
62
73
|
<div className="flex items-start justify-between gap-2 py-1.5">
|
|
@@ -69,37 +80,10 @@ function CopyRow({ label, value }: { label: string; value: string }) {
|
|
|
69
80
|
)
|
|
70
81
|
}
|
|
71
82
|
|
|
72
|
-
function SessionFiles({ client, sessionId,
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (!open || !sessionId) {
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
let cancelled = false
|
|
81
|
-
setLoading(true)
|
|
82
|
-
client
|
|
83
|
-
.listSessionFiles(sessionId)
|
|
84
|
-
.then((list) => {
|
|
85
|
-
if (!cancelled) {
|
|
86
|
-
setFiles(list)
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
.catch(() => {
|
|
90
|
-
if (!cancelled) {
|
|
91
|
-
setFiles([])
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
.finally(() => {
|
|
95
|
-
if (!cancelled) {
|
|
96
|
-
setLoading(false)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
return () => {
|
|
100
|
-
cancelled = true
|
|
101
|
-
}
|
|
102
|
-
}, [client, sessionId, open])
|
|
83
|
+
function SessionFiles({ client, sessionId, active }: { client: WorkerDeckClient; sessionId: string | undefined; active: boolean }) {
|
|
84
|
+
const { data: files, loading } = useAsync(() => client.listSessionFiles(sessionId!), [client, sessionId], {
|
|
85
|
+
enabled: active && !!sessionId,
|
|
86
|
+
})
|
|
103
87
|
|
|
104
88
|
return (
|
|
105
89
|
<div className="pt-3">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import { projectLabel, projectName, projectSubpath, sessionLabel } from '@workerdeck/protocol'
|
|
4
|
-
import type { SessionRow, SubagentDisplay } from '@workerdeck/protocol'
|
|
4
|
+
import type { SessionRow, SessionTask, StepDisplay, SubagentDisplay } from '@workerdeck/protocol'
|
|
5
5
|
import { ContextRing } from './ContextRing.tsx'
|
|
6
6
|
import { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'
|
|
7
7
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
@@ -21,10 +21,15 @@ export interface SessionItemProps {
|
|
|
21
21
|
showProject?: boolean
|
|
22
22
|
projectIcons?: Record<string, string>
|
|
23
23
|
subagents?: SubagentDisplay
|
|
24
|
+
shells?: StepDisplay
|
|
25
|
+
tasks?: StepDisplay
|
|
24
26
|
onSelect?: (modifiers: SelectModifiers) => void
|
|
25
27
|
onSelectSubagent?: (toolUseId: string) => void
|
|
28
|
+
onSelectTask?: (task: SessionTask) => void
|
|
29
|
+
onStopTask?: (toolUseId: string) => void
|
|
26
30
|
onSelectShell?: (shellId: string) => void
|
|
27
31
|
onKillShell?: (shellId: string) => void
|
|
32
|
+
onShellAgentWrite?: (shellId: string, enabled: boolean) => void
|
|
28
33
|
now?: number
|
|
29
34
|
onRename?: (title: string) => void
|
|
30
35
|
renameOn?: 'doubleClick' | 'external'
|
|
@@ -44,10 +49,15 @@ export function SessionItem({
|
|
|
44
49
|
showProject = true,
|
|
45
50
|
projectIcons,
|
|
46
51
|
subagents = 'active',
|
|
52
|
+
shells = 'active',
|
|
53
|
+
tasks = 'active',
|
|
47
54
|
onSelect,
|
|
48
55
|
onSelectSubagent,
|
|
56
|
+
onSelectTask,
|
|
57
|
+
onStopTask,
|
|
49
58
|
onSelectShell,
|
|
50
59
|
onKillShell,
|
|
60
|
+
onShellAgentWrite,
|
|
51
61
|
now,
|
|
52
62
|
onRename,
|
|
53
63
|
renameOn = 'doubleClick',
|
|
@@ -96,11 +106,24 @@ export function SessionItem({
|
|
|
96
106
|
for (const extra of extras) {
|
|
97
107
|
parts.push(<span key={extra}>{extra}</span>)
|
|
98
108
|
}
|
|
99
|
-
const steps = sessionSteps(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
const steps = sessionSteps(
|
|
110
|
+
info,
|
|
111
|
+
(toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)),
|
|
112
|
+
subagents,
|
|
113
|
+
{
|
|
114
|
+
now: now ?? Date.now(),
|
|
115
|
+
show: shells,
|
|
116
|
+
onSelect: (shellId) => (onSelectShell ? onSelectShell(shellId) : onSelect?.(NO_MODIFIERS)),
|
|
117
|
+
onKill: onKillShell,
|
|
118
|
+
onAgentWrite: onShellAgentWrite,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
show: tasks,
|
|
122
|
+
live: info.status === 'running' || info.status === 'starting' || info.status === 'awaiting_approval',
|
|
123
|
+
onSelect: (task) => (onSelectTask ? onSelectTask(task) : onSelect?.(NO_MODIFIERS)),
|
|
124
|
+
onStop: onStopTask,
|
|
125
|
+
},
|
|
126
|
+
)
|
|
104
127
|
const holdsOpenStep = steps.some((s) => s.key === activeStepKey)
|
|
105
128
|
|
|
106
129
|
return (
|