@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.
Files changed (55) hide show
  1. package/build/{SessionPanel-C1bU4yTM.d.mts → SessionPanel-BNNc8grK.d.mts} +9 -7
  2. package/build/index.d.mts +153 -45
  3. package/build/index.mjs +501 -226
  4. package/build/index.mjs.map +1 -1
  5. package/build/scoped.css +15 -6
  6. package/build/{use-path-links-BEWxt0ZM.mjs → use-path-links-C6Fh5zdx.mjs} +1926 -2086
  7. package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
  8. package/build/workspace.d.mts +1 -1
  9. package/build/workspace.mjs +1 -1
  10. package/package.json +4 -4
  11. package/src/components/agent/ApprovalPrompts.tsx +87 -0
  12. package/src/components/agent/Composer.tsx +25 -47
  13. package/src/components/agent/ContextDialog.tsx +75 -67
  14. package/src/components/agent/FrameStrip.tsx +74 -0
  15. package/src/components/agent/HostFilesDialog.tsx +3 -2
  16. package/src/components/agent/McpDialog.tsx +112 -78
  17. package/src/components/agent/PermissionPrompt.tsx +13 -22
  18. package/src/components/agent/QuestionPrompt.tsx +6 -41
  19. package/src/components/agent/Scrubber.tsx +6 -75
  20. package/src/components/agent/SessionBrowser.tsx +125 -214
  21. package/src/components/agent/SessionFilters.tsx +266 -0
  22. package/src/components/agent/SessionInfoDialog.tsx +47 -63
  23. package/src/components/agent/SessionItem.tsx +27 -7
  24. package/src/components/agent/SessionPanel.tsx +244 -488
  25. package/src/components/agent/SessionSteps.tsx +54 -11
  26. package/src/components/agent/ShellStrip.tsx +27 -43
  27. package/src/components/agent/SubagentStrip.tsx +13 -30
  28. package/src/components/agent/Transcript.tsx +13 -0
  29. package/src/components/agent/TranscriptItemView.tsx +6 -36
  30. package/src/components/agent/composer-commands.ts +80 -1
  31. package/src/components/agent/markdown-image.tsx +49 -1
  32. package/src/components/agent/scrubber-marks.ts +33 -11
  33. package/src/components/agent/scrubber-peek.tsx +122 -0
  34. package/src/components/agent/session-panel-providers.tsx +41 -0
  35. package/src/components/agent/shell-actions.tsx +36 -1
  36. package/src/components/agent/use-catch-up.ts +20 -0
  37. package/src/components/agent/use-frame.ts +88 -0
  38. package/src/components/agent/use-host-image.ts +81 -0
  39. package/src/components/agent/use-shell-frame.ts +12 -70
  40. package/src/components/agent/use-subagent-frame.ts +19 -75
  41. package/src/components/prompt-area/prompt-area-engine.ts +21 -20
  42. package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
  43. package/src/components/terminal/PermissionPrompt.tsx +19 -32
  44. package/src/components/terminal/QuestionPrompt.tsx +10 -46
  45. package/src/components/terminal/TerminalTranscript.tsx +3 -86
  46. package/src/components/terminal/items.tsx +5 -43
  47. package/src/components/terminal/scrubber.tsx +25 -272
  48. package/src/components/ui/Popover.tsx +29 -0
  49. package/src/components/ui/Select.tsx +25 -0
  50. package/src/index.ts +46 -8
  51. package/src/lib/file-link.ts +3 -16
  52. package/src/lib/permission-prompt.ts +57 -0
  53. package/src/lib/question-answers.ts +65 -0
  54. package/build/use-path-links-BEWxt0ZM.mjs.map +0 -1
  55. 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
- STATE_LABELS,
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 { SessionItem } from './SessionItem.tsx'
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
- onSelect?: (row: SessionRow) => void
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?: React.ReactNode
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 adapters = useMemo(() => adaptersOf(rows), [rows])
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
- <div className={cn('flex flex-col gap-1.5 px-2', !showControls && 'hidden')}>
93
- <div className="relative">
94
- <Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-fg-4" />
95
- <Input
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.info.id === activeId}
154
+ active={isActive(row)}
231
155
  activeSubagentId={activeSubagentId}
232
156
  activeShellId={activeShellId}
157
+ actions={rowActions?.(row)}
233
158
  now={now}
234
- showGateway={gateways.length > 1 && config.groupBy !== 'gateway'}
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="external"
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
- {onRename && !editing ? (
325
- <RowAction label="Rename session" onClick={() => setEditing(true)}>
326
- <Pencil className="size-3 text-fg-3" />
327
- </RowAction>
328
- ) : null}
329
- {onClearContext && info.capabilities?.clearContext ? (
330
- <RowAction
331
- label="Clear context"
332
- title="Clear the conversation - the session keeps running and the old conversation stays resumable"
333
- onClick={() => onClearContext(row)}
334
- >
335
- <Eraser className="size-3 text-fg-3" />
336
- </RowAction>
337
- ) : null}
338
- {onDelete ? (
339
- <RowAction label="Close session" onClick={() => onDelete(row)}>
340
- <Trash2 className="size-3 text-fg-3" />
341
- </RowAction>
342
- ) : null}
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 FilterRow({ label, children }: { label: string; children: React.ReactNode }) {
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
- <Select value={value} onValueChange={(v) => onChange(v as string)}>
431
- <SelectTrigger aria-label={label} className="w-full min-w-0">
432
- <SelectValue>{options.find((o) => o.value === value)?.label ?? label}</SelectValue>
433
- </SelectTrigger>
434
- <SelectContent>
435
- {options.map((option) => (
436
- <SelectItem key={option.value} value={option.value}>
437
- <SelectItemText>{option.label}</SelectItemText>
438
- </SelectItem>
439
- ))}
440
- </SelectContent>
441
- </Select>
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
  }