@workerdeck/ui 3.0.1 → 3.2.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
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
|
-
import { Eraser, Layers, Pencil, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
STATE_ORDER,
|
|
7
|
-
adaptersOf,
|
|
8
|
-
clearFilters,
|
|
9
|
-
filterRows,
|
|
10
|
-
groupRows,
|
|
11
|
-
hasFacetFilter,
|
|
12
|
-
projectsOf,
|
|
13
|
-
subsetSummary,
|
|
14
|
-
} from '@workerdeck/protocol'
|
|
15
|
-
import type { GroupBy, SessionRow, SessionState, SortBy, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
3
|
+
import { Eraser, FolderOpen, Layers, Pencil, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
|
+
import { clearFilters, filterRows, groupRows, hasFacetFilter, scopeActive, subsetSummary } from '@workerdeck/protocol'
|
|
5
|
+
import type { SessionRow, SessionTask, StepDisplay, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
16
6
|
import { Button } from '../ui/Button.tsx'
|
|
17
7
|
import { Empty } from '../ui/Empty.tsx'
|
|
18
8
|
import { Input } from '../ui/Input.tsx'
|
|
19
|
-
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
20
9
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
21
|
-
import {
|
|
10
|
+
import { SessionFilters } from './SessionFilters.tsx'
|
|
11
|
+
import { SessionItem, type SelectModifiers } from './SessionItem.tsx'
|
|
22
12
|
import { cn } from '../../lib/utils.ts'
|
|
23
13
|
|
|
24
14
|
export { SessionStatusIcon } from './SessionStatusIcon.tsx'
|
|
@@ -32,16 +22,28 @@ export interface SessionBrowserProps {
|
|
|
32
22
|
activeSubagentId?: string
|
|
33
23
|
activeShellId?: string
|
|
34
24
|
now?: number
|
|
35
|
-
|
|
25
|
+
// Overrides `activeId`, for a host whose session ids are unique only per gateway.
|
|
26
|
+
isActive?: (row: SessionRow) => boolean
|
|
27
|
+
// Replaces the pencil / eraser / trash; rename then falls back to the card's own double-click.
|
|
28
|
+
rowActions?: (row: SessionRow) => ReactNode
|
|
29
|
+
// Overrides the count of gateways among `rows` when deciding whether a card names its gateway.
|
|
30
|
+
gatewayCount?: number
|
|
31
|
+
showSubset?: boolean
|
|
32
|
+
onSelect?: (row: SessionRow, modifiers: SelectModifiers) => void
|
|
36
33
|
onDelete?: (row: SessionRow) => void
|
|
37
34
|
onRename?: (row: SessionRow, title: string) => void
|
|
38
35
|
onClearContext?: (row: SessionRow) => void
|
|
39
36
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
37
|
+
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
38
|
+
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
40
39
|
onSelectShell?: (row: SessionRow, shellId: string) => void
|
|
41
40
|
onKillShell?: (row: SessionRow, shellId: string) => void
|
|
42
41
|
onShellAgentWrite?: (row: SessionRow, shellId: string, enabled: boolean) => void
|
|
43
|
-
emptyState?:
|
|
42
|
+
emptyState?: ReactNode
|
|
43
|
+
// The facet controls, drawn inline. A host with a header puts `SessionFiltersButton` there instead.
|
|
44
44
|
showControls?: boolean
|
|
45
|
+
showSearch?: boolean
|
|
46
|
+
autoFocusSearch?: boolean
|
|
45
47
|
projectIcons?: Record<string, string>
|
|
46
48
|
className?: string
|
|
47
49
|
}
|
|
@@ -59,128 +61,42 @@ export function SessionBrowser({
|
|
|
59
61
|
activeSubagentId,
|
|
60
62
|
activeShellId,
|
|
61
63
|
now,
|
|
64
|
+
isActive = (row) => row.info.id === activeId,
|
|
65
|
+
rowActions,
|
|
66
|
+
gatewayCount,
|
|
67
|
+
showSubset = true,
|
|
62
68
|
onSelect,
|
|
63
69
|
onDelete,
|
|
64
70
|
onRename,
|
|
65
71
|
onClearContext,
|
|
66
72
|
onSelectSubagent,
|
|
73
|
+
onSelectTask,
|
|
74
|
+
onStopTask,
|
|
67
75
|
onSelectShell,
|
|
68
76
|
onKillShell,
|
|
69
77
|
onShellAgentWrite,
|
|
70
78
|
emptyState,
|
|
71
79
|
showControls = true,
|
|
80
|
+
showSearch = showControls,
|
|
81
|
+
autoFocusSearch,
|
|
72
82
|
projectIcons,
|
|
73
83
|
className,
|
|
74
84
|
}: SessionBrowserProps) {
|
|
75
85
|
const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])
|
|
76
86
|
const groups = useMemo(() => groupRows(visible, config), [visible, config])
|
|
77
87
|
const subset = subsetSummary(config, scope, visible.length, rows.length)
|
|
78
|
-
const
|
|
79
|
-
const projects = useMemo(() => projectsOf(rows), [rows])
|
|
80
|
-
const gateways = useMemo(() => {
|
|
81
|
-
const seen = new Map<string, string>()
|
|
82
|
-
for (const row of rows) {
|
|
83
|
-
seen.set(row.hostId, row.hostName)
|
|
84
|
-
}
|
|
85
|
-
return [...seen].map(([id, name]) => ({ id, name }))
|
|
86
|
-
}, [rows])
|
|
88
|
+
const rowGateways = useMemo(() => new Set(rows.map((row) => row.hostId)).size, [rows])
|
|
87
89
|
|
|
88
90
|
const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
|
|
89
91
|
|
|
90
92
|
return (
|
|
91
93
|
<div data-slot="session-browser" className={cn('flex flex-col gap-3', className)}>
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
value={config.search}
|
|
97
|
-
onChange={(e) => set({ search: e.target.value })}
|
|
98
|
-
placeholder="Search sessions"
|
|
99
|
-
aria-label="Search sessions"
|
|
100
|
-
className="pl-8"
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
103
|
-
<FilterRow label="State">
|
|
104
|
-
<FacetSelect
|
|
105
|
-
label="State"
|
|
106
|
-
value={config.states}
|
|
107
|
-
options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}
|
|
108
|
-
onChange={(states) => set({ states: states as SessionState[] })}
|
|
109
|
-
/>
|
|
110
|
-
</FilterRow>
|
|
111
|
-
{adapters.length > 1 ? (
|
|
112
|
-
<FilterRow label="Engine">
|
|
113
|
-
<FacetSelect
|
|
114
|
-
label="Engine"
|
|
115
|
-
value={config.adapters}
|
|
116
|
-
options={adapters.map((a) => ({ value: a, label: a }))}
|
|
117
|
-
onChange={(adapters) => set({ adapters })}
|
|
118
|
-
/>
|
|
119
|
-
</FilterRow>
|
|
120
|
-
) : null}
|
|
121
|
-
{gateways.length > 1 ? (
|
|
122
|
-
<FilterRow label="Gateway">
|
|
123
|
-
<FacetSelect
|
|
124
|
-
label="Gateway"
|
|
125
|
-
value={config.gateways}
|
|
126
|
-
options={gateways.map((g) => ({ value: g.id, label: g.name }))}
|
|
127
|
-
onChange={(gateways) => set({ gateways })}
|
|
128
|
-
/>
|
|
129
|
-
</FilterRow>
|
|
130
|
-
) : null}
|
|
131
|
-
{projects.length > 1 ? (
|
|
132
|
-
<FilterRow label="Project">
|
|
133
|
-
<FacetSelect
|
|
134
|
-
label="Project"
|
|
135
|
-
value={config.projects ?? []}
|
|
136
|
-
options={projects.map((p) => ({ value: p.key, label: p.label }))}
|
|
137
|
-
onChange={(next) => set({ projects: next })}
|
|
138
|
-
/>
|
|
139
|
-
</FilterRow>
|
|
140
|
-
) : null}
|
|
141
|
-
<FilterRow label="Group">
|
|
142
|
-
<OneOfSelect
|
|
143
|
-
label="Group"
|
|
144
|
-
value={config.groupBy}
|
|
145
|
-
options={[
|
|
146
|
-
{ value: 'none', label: 'No grouping' },
|
|
147
|
-
{ value: 'state', label: 'By state' },
|
|
148
|
-
{ value: 'adapter', label: 'By engine' },
|
|
149
|
-
...(projects.length > 1 ? [{ value: 'project' as const, label: 'By project' }] : []),
|
|
150
|
-
...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'By gateway' }] : []),
|
|
151
|
-
]}
|
|
152
|
-
onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
|
|
153
|
-
/>
|
|
154
|
-
</FilterRow>
|
|
155
|
-
<FilterRow label="Sub-agents">
|
|
156
|
-
<OneOfSelect
|
|
157
|
-
label="Sub-agents"
|
|
158
|
-
value={config.subagents}
|
|
159
|
-
options={[
|
|
160
|
-
{ value: 'active', label: 'Hide completed' },
|
|
161
|
-
{ value: 'all', label: 'Show all' },
|
|
162
|
-
{ value: 'none', label: 'Hide all' },
|
|
163
|
-
]}
|
|
164
|
-
onChange={(subagents) => set({ subagents: subagents as SubagentDisplay })}
|
|
165
|
-
/>
|
|
166
|
-
</FilterRow>
|
|
167
|
-
<FilterRow label="Sort">
|
|
168
|
-
<OneOfSelect
|
|
169
|
-
label="Sort"
|
|
170
|
-
value={config.sortBy}
|
|
171
|
-
options={[
|
|
172
|
-
{ value: 'recent', label: 'Recent' },
|
|
173
|
-
{ value: 'name', label: 'Name' },
|
|
174
|
-
{ value: 'state', label: 'State' },
|
|
175
|
-
...(projects.length > 1 ? [{ value: 'project' as const, label: 'Project' }] : []),
|
|
176
|
-
...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'Gateway' }] : []),
|
|
177
|
-
]}
|
|
178
|
-
onChange={(sortBy) => set({ sortBy: sortBy as SortBy })}
|
|
179
|
-
/>
|
|
180
|
-
</FilterRow>
|
|
181
|
-
</div>
|
|
94
|
+
{showSearch ? (
|
|
95
|
+
<SessionSearch value={config.search} onChange={(search) => set({ search })} autoFocus={autoFocusSearch} className="px-2" />
|
|
96
|
+
) : null}
|
|
97
|
+
{showControls ? <SessionFilters config={config} onConfigChange={onConfigChange} rows={rows} scope={scope} className="px-2" /> : null}
|
|
182
98
|
|
|
183
|
-
{subset ? (
|
|
99
|
+
{subset && showSubset ? (
|
|
184
100
|
<div className="flex items-center gap-2 px-3 text-label text-fg-4">
|
|
185
101
|
<span>
|
|
186
102
|
{subset.shown} of {subset.total}
|
|
@@ -207,6 +123,14 @@ export function SessionBrowser({
|
|
|
207
123
|
action="Clear filters"
|
|
208
124
|
onAction={() => onConfigChange(clearFilters(config))}
|
|
209
125
|
/>
|
|
126
|
+
) : scope && scopeActive(config, scope) ? (
|
|
127
|
+
<Empty
|
|
128
|
+
icon={<FolderOpen />}
|
|
129
|
+
title="Nothing in this folder"
|
|
130
|
+
description={`No session is running in ${scope.label}.`}
|
|
131
|
+
action="Show all folders"
|
|
132
|
+
onAction={() => set({ scoped: false })}
|
|
133
|
+
/>
|
|
210
134
|
) : (
|
|
211
135
|
<Empty icon={<Layers />} title="Nothing here" description="No session to show." />
|
|
212
136
|
)
|
|
@@ -227,19 +151,24 @@ export function SessionBrowser({
|
|
|
227
151
|
<SessionRowItem
|
|
228
152
|
key={`${row.hostId}:${row.info.id}`}
|
|
229
153
|
row={row}
|
|
230
|
-
active={row
|
|
154
|
+
active={isActive(row)}
|
|
231
155
|
activeSubagentId={activeSubagentId}
|
|
232
156
|
activeShellId={activeShellId}
|
|
157
|
+
actions={rowActions?.(row)}
|
|
233
158
|
now={now}
|
|
234
|
-
showGateway={
|
|
159
|
+
showGateway={(gatewayCount ?? rowGateways) > 1 && config.groupBy !== 'gateway'}
|
|
235
160
|
showProject={config.groupBy !== 'project'}
|
|
236
161
|
subagents={config.subagents}
|
|
162
|
+
shells={config.shells}
|
|
163
|
+
tasks={config.tasks}
|
|
237
164
|
projectIcons={projectIcons}
|
|
238
165
|
onSelect={onSelect}
|
|
239
166
|
onDelete={onDelete}
|
|
240
167
|
onRename={onRename}
|
|
241
168
|
onClearContext={onClearContext}
|
|
242
169
|
onSelectSubagent={onSelectSubagent}
|
|
170
|
+
onSelectTask={onSelectTask}
|
|
171
|
+
onStopTask={onStopTask}
|
|
243
172
|
onSelectShell={onSelectShell}
|
|
244
173
|
onKillShell={onKillShell}
|
|
245
174
|
onShellAgentWrite={onShellAgentWrite}
|
|
@@ -261,18 +190,23 @@ function iconSrcOf(row: SessionRow | undefined, icons: Record<string, string> |
|
|
|
261
190
|
interface SessionRowItemProps {
|
|
262
191
|
row: SessionRow
|
|
263
192
|
active?: boolean
|
|
193
|
+
actions?: ReactNode
|
|
264
194
|
activeSubagentId?: string
|
|
265
195
|
activeShellId?: string
|
|
266
196
|
now?: number
|
|
267
197
|
showGateway?: boolean
|
|
268
198
|
showProject?: boolean
|
|
269
199
|
subagents?: SubagentDisplay
|
|
200
|
+
shells?: StepDisplay
|
|
201
|
+
tasks?: StepDisplay
|
|
270
202
|
projectIcons?: Record<string, string>
|
|
271
|
-
onSelect?: (row: SessionRow) => void
|
|
203
|
+
onSelect?: (row: SessionRow, modifiers: SelectModifiers) => void
|
|
272
204
|
onDelete?: (row: SessionRow) => void
|
|
273
205
|
onRename?: (row: SessionRow, title: string) => void
|
|
274
206
|
onClearContext?: (row: SessionRow) => void
|
|
275
207
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
208
|
+
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
209
|
+
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
276
210
|
onSelectShell?: (row: SessionRow, shellId: string) => void
|
|
277
211
|
onKillShell?: (row: SessionRow, shellId: string) => void
|
|
278
212
|
onShellAgentWrite?: (row: SessionRow, shellId: string, enabled: boolean) => void
|
|
@@ -281,18 +215,23 @@ interface SessionRowItemProps {
|
|
|
281
215
|
function SessionRowItem({
|
|
282
216
|
row,
|
|
283
217
|
active,
|
|
218
|
+
actions,
|
|
284
219
|
activeSubagentId,
|
|
285
220
|
activeShellId,
|
|
286
221
|
now,
|
|
287
222
|
showGateway,
|
|
288
223
|
showProject = true,
|
|
289
224
|
subagents,
|
|
225
|
+
shells,
|
|
226
|
+
tasks,
|
|
290
227
|
projectIcons,
|
|
291
228
|
onSelect,
|
|
292
229
|
onDelete,
|
|
293
230
|
onRename,
|
|
294
231
|
onClearContext,
|
|
295
232
|
onSelectSubagent,
|
|
233
|
+
onSelectTask,
|
|
234
|
+
onStopTask,
|
|
296
235
|
onSelectShell,
|
|
297
236
|
onKillShell,
|
|
298
237
|
onShellAgentWrite,
|
|
@@ -304,43 +243,49 @@ function SessionRowItem({
|
|
|
304
243
|
<SessionItem
|
|
305
244
|
row={row}
|
|
306
245
|
active={active === true}
|
|
307
|
-
activeStepKey={activeShellId ?? activeSubagentId}
|
|
246
|
+
activeStepKey={active ? (activeShellId ?? activeSubagentId) : undefined}
|
|
308
247
|
now={now}
|
|
309
248
|
showGateway={showGateway}
|
|
310
249
|
showProject={showProject}
|
|
311
250
|
subagents={subagents}
|
|
251
|
+
shells={shells}
|
|
252
|
+
tasks={tasks}
|
|
312
253
|
projectIcons={projectIcons}
|
|
313
|
-
onSelect={() => onSelect?.(row)}
|
|
254
|
+
onSelect={(modifiers) => onSelect?.(row, modifiers)}
|
|
314
255
|
onSelectSubagent={onSelectSubagent ? (id) => onSelectSubagent(row, id) : undefined}
|
|
256
|
+
onSelectTask={onSelectTask ? (task) => onSelectTask(row, task) : undefined}
|
|
257
|
+
onStopTask={onStopTask ? (id) => onStopTask(row, id) : undefined}
|
|
315
258
|
onSelectShell={onSelectShell ? (id) => onSelectShell(row, id) : undefined}
|
|
316
259
|
onKillShell={onKillShell ? (id) => onKillShell(row, id) : undefined}
|
|
317
260
|
onShellAgentWrite={onShellAgentWrite ? (id, enabled) => onShellAgentWrite(row, id, enabled) : undefined}
|
|
318
261
|
onRename={onRename ? (title) => onRename(row, title) : undefined}
|
|
319
|
-
renameOn=
|
|
320
|
-
editing={editing}
|
|
321
|
-
onEditingChange={setEditing}
|
|
262
|
+
renameOn={actions === undefined ? 'external' : 'doubleClick'}
|
|
263
|
+
editing={actions === undefined ? editing : undefined}
|
|
264
|
+
onEditingChange={actions === undefined ? setEditing : undefined}
|
|
322
265
|
actions={
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
266
|
+
actions ?? (
|
|
267
|
+
<>
|
|
268
|
+
{onRename && !editing ? (
|
|
269
|
+
<RowAction label="Rename session" onClick={() => setEditing(true)}>
|
|
270
|
+
<Pencil className="size-3 text-fg-3" />
|
|
271
|
+
</RowAction>
|
|
272
|
+
) : null}
|
|
273
|
+
{onClearContext && info.capabilities?.clearContext ? (
|
|
274
|
+
<RowAction
|
|
275
|
+
label="Clear context"
|
|
276
|
+
title="Clear the conversation - the session keeps running and the old conversation stays resumable"
|
|
277
|
+
onClick={() => onClearContext(row)}
|
|
278
|
+
>
|
|
279
|
+
<Eraser className="size-3 text-fg-3" />
|
|
280
|
+
</RowAction>
|
|
281
|
+
) : null}
|
|
282
|
+
{onDelete ? (
|
|
283
|
+
<RowAction label="Close session" onClick={() => onDelete(row)}>
|
|
284
|
+
<Trash2 className="size-3 text-fg-3" />
|
|
285
|
+
</RowAction>
|
|
286
|
+
) : null}
|
|
287
|
+
</>
|
|
288
|
+
)
|
|
344
289
|
}
|
|
345
290
|
/>
|
|
346
291
|
)
|
|
@@ -364,80 +309,46 @@ function RowAction({ label, title, onClick, children }: { label: string; title?:
|
|
|
364
309
|
)
|
|
365
310
|
}
|
|
366
311
|
|
|
367
|
-
function
|
|
368
|
-
return (
|
|
369
|
-
<div className="flex items-center gap-2">
|
|
370
|
-
<span aria-hidden className="w-14 shrink-0 truncate text-label text-fg-3">
|
|
371
|
-
{label}
|
|
372
|
-
</span>
|
|
373
|
-
<div className="min-w-0 flex-1">{children}</div>
|
|
374
|
-
</div>
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function FacetSelect({
|
|
379
|
-
label,
|
|
312
|
+
export function SessionSearch({
|
|
380
313
|
value,
|
|
381
|
-
options,
|
|
382
|
-
onChange,
|
|
383
|
-
}: {
|
|
384
|
-
label: string
|
|
385
|
-
value: string[]
|
|
386
|
-
options: { value: string; label: string }[]
|
|
387
|
-
onChange: (value: string[]) => void
|
|
388
|
-
}) {
|
|
389
|
-
return (
|
|
390
|
-
<div className="flex items-center gap-1">
|
|
391
|
-
<Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>
|
|
392
|
-
<SelectTrigger aria-label={label} className="min-w-0 flex-1">
|
|
393
|
-
<SelectValue>
|
|
394
|
-
{value.length === 0
|
|
395
|
-
? 'All'
|
|
396
|
-
: value.length === 1
|
|
397
|
-
? (options.find((o) => o.value === value[0])?.label ?? 'All')
|
|
398
|
-
: `${value.length} selected`}
|
|
399
|
-
</SelectValue>
|
|
400
|
-
</SelectTrigger>
|
|
401
|
-
<SelectContent>
|
|
402
|
-
{options.map((option) => (
|
|
403
|
-
<SelectItem key={option.value} value={option.value}>
|
|
404
|
-
<SelectItemText>{option.label}</SelectItemText>
|
|
405
|
-
</SelectItem>
|
|
406
|
-
))}
|
|
407
|
-
</SelectContent>
|
|
408
|
-
</Select>
|
|
409
|
-
{value.length > 0 ? (
|
|
410
|
-
<Button variant="ghost" size="icon-sm" aria-label={`Clear ${label}`} onClick={() => onChange([])}>
|
|
411
|
-
<X className="size-3 text-fg-4" />
|
|
412
|
-
</Button>
|
|
413
|
-
) : null}
|
|
414
|
-
</div>
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function OneOfSelect({
|
|
419
|
-
label,
|
|
420
|
-
value,
|
|
421
|
-
options,
|
|
422
314
|
onChange,
|
|
315
|
+
autoFocus,
|
|
316
|
+
className,
|
|
317
|
+
inputClassName,
|
|
423
318
|
}: {
|
|
424
|
-
label: string
|
|
425
319
|
value: string
|
|
426
|
-
options: readonly { value: string; label: string }[]
|
|
427
320
|
onChange: (value: string) => void
|
|
321
|
+
autoFocus?: boolean
|
|
322
|
+
className?: string
|
|
323
|
+
inputClassName?: string
|
|
428
324
|
}) {
|
|
429
325
|
return (
|
|
430
|
-
<
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
{
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
326
|
+
<div className={cn('relative', className)}>
|
|
327
|
+
<Search className="pointer-events-none absolute top-1/2 left-4.5 size-3.5 -translate-y-1/2 text-fg-4" />
|
|
328
|
+
<Input
|
|
329
|
+
value={value}
|
|
330
|
+
onChange={(e) => onChange(e.target.value)}
|
|
331
|
+
onKeyDown={(e) => {
|
|
332
|
+
if (e.key === 'Escape' && value) {
|
|
333
|
+
e.stopPropagation()
|
|
334
|
+
onChange('')
|
|
335
|
+
}
|
|
336
|
+
}}
|
|
337
|
+
placeholder="Search sessions"
|
|
338
|
+
aria-label="Search sessions"
|
|
339
|
+
autoFocus={autoFocus}
|
|
340
|
+
className={cn('pr-7 pl-8', inputClassName)}
|
|
341
|
+
/>
|
|
342
|
+
{value ? (
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
aria-label="Clear search"
|
|
346
|
+
onClick={() => onChange('')}
|
|
347
|
+
className="absolute top-1/2 right-3.5 -translate-y-1/2 rounded p-0.5 text-fg-4 hover:text-fg-1"
|
|
348
|
+
>
|
|
349
|
+
<X className="size-3" />
|
|
350
|
+
</button>
|
|
351
|
+
) : null}
|
|
352
|
+
</div>
|
|
442
353
|
)
|
|
443
354
|
}
|