@workerdeck/ui 3.1.0 → 3.3.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.
@@ -1,4 +1,4 @@
1
- import { $t as Spinner, En as Input, N as ShellTerminal, Zt as Splitter, i as SessionPanel, in as TooltipProvider, jn as cn, kn as Button, n as usePathLinks, nn as Tip, q as EntryIcon, t as resolveAgainstCwd } from "./use-path-links-C6Fh5zdx.mjs";
1
+ import { $t as Spinner, En as Input, N as ShellTerminal, Zt as Splitter, i as SessionPanel, in as TooltipProvider, jn as cn, kn as Button, n as usePathLinks, nn as Tip, q as EntryIcon, t as resolveAgainstCwd } from "./use-path-links-Cby4i_VL.mjs";
2
2
  import { m as formatBytes } from "./status-kmsBhuFG.mjs";
3
3
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerdeck/ui",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "type": "module",
5
5
  "description": "Styled agent-control component library for WorkerDeck: session panel, transcript with streaming, tool-call cards, permission prompts, session list, composer. Tailwind v4 + Base UI + cva. Pairs with the headless @workerdeck/react layer.",
6
6
  "license": "MIT",
@@ -39,9 +39,9 @@
39
39
  "streamdown": "^2.6.0",
40
40
  "tailwind-merge": "^3.7.0",
41
41
  "use-stick-to-bottom": "^1.1.6",
42
- "@workerdeck/client": "3.1.0",
43
- "@workerdeck/protocol": "3.1.0",
44
- "@workerdeck/react": "3.1.0"
42
+ "@workerdeck/protocol": "3.3.0",
43
+ "@workerdeck/react": "3.3.0",
44
+ "@workerdeck/client": "3.3.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "monaco-editor": "^0.56.0",
@@ -1,14 +1,15 @@
1
1
  import { useMemo, useState } from 'react'
2
- import type { ReactNode } from 'react'
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'
2
+ import type { HTMLAttributes, ReactNode } from 'react'
3
+ import { Eraser, FolderOpen, Layers, Pencil, Plus, Search, SearchX, Trash2, X } from 'lucide-react'
4
+ import { clearFilters, filterRows, groupRows, hasFacetFilter, scopeActive, sessionKey, subsetSummary } from '@workerdeck/protocol'
5
+ import type { SessionGroup, SessionRow, SessionTask, StepDisplay, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
6
6
  import { Button } from '../ui/Button.tsx'
7
7
  import { Empty } from '../ui/Empty.tsx'
8
8
  import { Input } from '../ui/Input.tsx'
9
9
  import { ProjectIcon } from './ProjectIcon.tsx'
10
10
  import { SessionFilters } from './SessionFilters.tsx'
11
11
  import { SessionItem, type SelectModifiers } from './SessionItem.tsx'
12
+ import { CustomGroupHeader, GroupHeading, HeadingAction, NewGroupButton, useGroupDrag } from './SessionGroups.tsx'
12
13
  import { cn } from '../../lib/utils.ts'
13
14
 
14
15
  export { SessionStatusIcon } from './SessionStatusIcon.tsx'
@@ -39,6 +40,8 @@ export interface SessionBrowserProps {
39
40
  onSelectShell?: (row: SessionRow, shellId: string) => void
40
41
  onKillShell?: (row: SessionRow, shellId: string) => void
41
42
  onShellAgentWrite?: (row: SessionRow, shellId: string, enabled: boolean) => void
43
+ // Draws a `+` on each project heading: start a session on that gateway, in that project's root.
44
+ onCreateInGroup?: (target: GroupTarget) => void
42
45
  emptyState?: ReactNode
43
46
  // The facet controls, drawn inline. A host with a header puts `SessionFiltersButton` there instead.
44
47
  showControls?: boolean
@@ -48,6 +51,8 @@ export interface SessionBrowserProps {
48
51
  className?: string
49
52
  }
50
53
 
54
+ export type GroupTarget = { hostId: string; cwd?: string }
55
+
51
56
  export function rowShapeClass(active: boolean): string {
52
57
  return cn('px-2 py-1.5 hover:bg-row-hover', active ? 'mr-1 ml-0 rounded-r-md border-l-4 border-l-accent' : 'mx-1 rounded-md')
53
58
  }
@@ -75,6 +80,7 @@ export function SessionBrowser({
75
80
  onSelectShell,
76
81
  onKillShell,
77
82
  onShellAgentWrite,
83
+ onCreateInGroup,
78
84
  emptyState,
79
85
  showControls = true,
80
86
  showSearch = showControls,
@@ -82,12 +88,55 @@ export function SessionBrowser({
82
88
  projectIcons,
83
89
  className,
84
90
  }: SessionBrowserProps) {
91
+ const rowGateways = useMemo(() => new Set(rows.map((row) => row.hostId)).size, [rows])
92
+ const gateways = gatewayCount ?? rowGateways
85
93
  const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])
86
- const groups = useMemo(() => groupRows(visible, config), [visible, config])
94
+ const groups = useMemo(() => groupRows(visible, config, { gatewayCount: gateways }), [visible, config, gateways])
87
95
  const subset = subsetSummary(config, scope, visible.length, rows.length)
88
- const rowGateways = useMemo(() => new Set(rows.map((row) => row.hostId)).size, [rows])
96
+ const custom = config.groupBy === 'custom'
97
+ const [editingGroup, setEditingGroup] = useState<string>()
89
98
 
90
99
  const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
100
+ const drag = useGroupDrag(config.customGroups ?? [], (customGroups) => set({ customGroups }))
101
+
102
+ const heading = (group: SessionGroup) => {
103
+ if (custom) {
104
+ return (
105
+ <CustomGroupHeader
106
+ group={group}
107
+ editing={group.custom !== undefined && editingGroup === group.custom}
108
+ onEditingChange={(editing) => setEditingGroup(editing ? group.custom : undefined)}
109
+ onRename={(name) => group.custom && drag.rename(group.custom, name)}
110
+ onRemove={() => group.custom && drag.remove(group.custom)}
111
+ dragProps={drag.header(group)}
112
+ />
113
+ )
114
+ }
115
+ if (config.groupBy === 'none' || !group.label) {
116
+ return null
117
+ }
118
+ const project = config.groupBy === 'project'
119
+ const target = project && group.hostId ? { hostId: group.hostId, cwd: group.cwd } : undefined
120
+ return (
121
+ <GroupHeading
122
+ label={group.label}
123
+ count={group.rows.length}
124
+ title={project && group.cwd ? group.cwd : undefined}
125
+ leading={
126
+ project ? (
127
+ <ProjectIcon icon={group.rows[0]?.info.project?.icon} src={iconSrcOf(group.rows[0], projectIcons)} name={group.label} />
128
+ ) : undefined
129
+ }
130
+ actions={
131
+ target && onCreateInGroup ? (
132
+ <HeadingAction label={`New session in ${group.label}`} onClick={() => onCreateInGroup(target)}>
133
+ <Plus className="size-3" />
134
+ </HeadingAction>
135
+ ) : undefined
136
+ }
137
+ />
138
+ )
139
+ }
91
140
 
92
141
  return (
93
142
  <div data-slot="session-browser" className={cn('flex flex-col gap-3', className)}>
@@ -137,26 +186,29 @@ export function SessionBrowser({
137
186
  ) : (
138
187
  <div className="flex flex-col gap-4 px-1">
139
188
  {groups.map((group) => (
140
- <div key={group.key} className="flex flex-col gap-1">
141
- {config.groupBy !== 'none' && group.label ? (
142
- <div className="flex items-center gap-2 px-2 text-label font-medium text-fg-4">
143
- {config.groupBy === 'project' ? (
144
- <ProjectIcon icon={group.rows[0]?.info.project?.icon} src={iconSrcOf(group.rows[0], projectIcons)} name={group.label} />
145
- ) : null}
146
- <span className="uppercase tracking-wide">{group.label}</span>
147
- <span className="text-fg-4/70">{group.rows.length}</span>
189
+ <div
190
+ key={group.key}
191
+ {...(custom ? drag.container(group) : {})}
192
+ className={cn('flex flex-col gap-1 rounded-md', custom && drag.isOver(`group:${group.key}`) && 'bg-row-hover/50')}
193
+ >
194
+ {heading(group)}
195
+ {custom && group.custom !== undefined && group.rows.length === 0 ? (
196
+ <div className="mx-1 rounded-md border border-dashed border-border px-2 py-2 text-center text-label text-fg-4">
197
+ Drag sessions here
148
198
  </div>
149
199
  ) : null}
150
200
  {group.rows.map((row) => (
151
201
  <SessionRowItem
152
- key={`${row.hostId}:${row.info.id}`}
202
+ key={sessionKey(row)}
203
+ dragProps={custom ? drag.session(sessionKey(row), group) : undefined}
204
+ dropTarget={custom && drag.isOver(`row:${sessionKey(row)}`)}
153
205
  row={row}
154
206
  active={isActive(row)}
155
207
  activeSubagentId={activeSubagentId}
156
208
  activeShellId={activeShellId}
157
209
  actions={rowActions?.(row)}
158
210
  now={now}
159
- showGateway={(gatewayCount ?? rowGateways) > 1 && config.groupBy !== 'gateway'}
211
+ showGateway={gateways > 1 && config.groupBy !== 'project'}
160
212
  showProject={config.groupBy !== 'project'}
161
213
  subagents={config.subagents}
162
214
  shells={config.shells}
@@ -176,6 +228,7 @@ export function SessionBrowser({
176
228
  ))}
177
229
  </div>
178
230
  ))}
231
+ {custom ? <NewGroupButton onClick={() => setEditingGroup(drag.create())} /> : null}
179
232
  </div>
180
233
  )}
181
234
  </div>
@@ -189,6 +242,8 @@ function iconSrcOf(row: SessionRow | undefined, icons: Record<string, string> |
189
242
 
190
243
  interface SessionRowItemProps {
191
244
  row: SessionRow
245
+ dragProps?: HTMLAttributes<HTMLDivElement>
246
+ dropTarget?: boolean
192
247
  active?: boolean
193
248
  actions?: ReactNode
194
249
  activeSubagentId?: string
@@ -214,6 +269,8 @@ interface SessionRowItemProps {
214
269
 
215
270
  function SessionRowItem({
216
271
  row,
272
+ dragProps,
273
+ dropTarget,
217
274
  active,
218
275
  actions,
219
276
  activeSubagentId,
@@ -239,7 +296,7 @@ function SessionRowItem({
239
296
  const { info } = row
240
297
  const [editing, setEditing] = useState(false)
241
298
 
242
- return (
299
+ const item = (
243
300
  <SessionItem
244
301
  row={row}
245
302
  active={active === true}
@@ -289,6 +346,14 @@ function SessionRowItem({
289
346
  }
290
347
  />
291
348
  )
349
+ if (!dragProps) {
350
+ return item
351
+ }
352
+ return (
353
+ <div {...dragProps} className={cn('rounded-md', dropTarget && 'shadow-[inset_0_2px_0_0_var(--color-accent)]')}>
354
+ {item}
355
+ </div>
356
+ )
292
357
  }
293
358
 
294
359
  function RowAction({ label, title, onClick, children }: { label: string; title?: string; onClick: () => void; children: ReactNode }) {
@@ -106,8 +106,8 @@ export function SessionFilters({ config, onConfigChange, rows, scope, gateways:
106
106
  { value: 'none', label: 'No grouping' },
107
107
  { value: 'state', label: 'By state' },
108
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' }] : []),
109
+ { value: 'project', label: gateways.length > 1 ? 'By gateway & project' : 'By project' },
110
+ { value: 'custom', label: 'Custom' },
111
111
  ]}
112
112
  onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
113
113
  />
@@ -0,0 +1,248 @@
1
+ import { useState } from 'react'
2
+ import type { DragEvent, HTMLAttributes, ReactNode } from 'react'
3
+ import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react'
4
+ import {
5
+ addCustomGroup,
6
+ moveCustomGroup,
7
+ moveToCustomGroup,
8
+ newCustomGroupId,
9
+ removeCustomGroup,
10
+ renameCustomGroup,
11
+ } from '@workerdeck/protocol'
12
+ import type { CustomGroup, SessionGroup } from '@workerdeck/protocol'
13
+ import { Button } from '../ui/Button.tsx'
14
+ import { Input } from '../ui/Input.tsx'
15
+ import { cn } from '../../lib/utils.ts'
16
+
17
+ type DragItem = { kind: 'session'; key: string } | { kind: 'group'; id: string }
18
+
19
+ export type GroupDrag = ReturnType<typeof useGroupDrag>
20
+
21
+ // The payload lives in state rather than `dataTransfer`, which is unreadable during `dragover`; `setData` is still
22
+ // written because some engines refuse to start a drag without it.
23
+ export function useGroupDrag(groups: readonly CustomGroup[], onChange: (groups: CustomGroup[]) => void) {
24
+ const [item, setItem] = useState<DragItem>()
25
+ const [over, setOver] = useState<string>()
26
+ const end = () => {
27
+ setItem(undefined)
28
+ setOver(undefined)
29
+ }
30
+ const start = (e: DragEvent, next: DragItem) => {
31
+ e.stopPropagation()
32
+ e.dataTransfer.effectAllowed = 'move'
33
+ e.dataTransfer.setData('text/plain', next.kind === 'session' ? next.key : next.id)
34
+ setItem(next)
35
+ }
36
+ const accept = (e: DragEvent, spot: string) => {
37
+ e.preventDefault()
38
+ e.stopPropagation()
39
+ e.dataTransfer.dropEffect = 'move'
40
+ setOver(spot)
41
+ }
42
+
43
+ function session(key: string, group: SessionGroup): HTMLAttributes<HTMLDivElement> {
44
+ return {
45
+ draggable: true,
46
+ onDragStart: (e) => start(e, { kind: 'session', key }),
47
+ onDragEnd: end,
48
+ onDragOver: (e) => {
49
+ if (item?.kind === 'session') {
50
+ accept(e, `row:${key}`)
51
+ }
52
+ },
53
+ onDrop: (e) => {
54
+ if (item?.kind !== 'session') {
55
+ return
56
+ }
57
+ e.preventDefault()
58
+ e.stopPropagation()
59
+ if (item.key !== key) {
60
+ onChange(moveToCustomGroup(groups, item.key, group.custom, group.custom ? key : undefined))
61
+ }
62
+ end()
63
+ },
64
+ }
65
+ }
66
+
67
+ function container(group: SessionGroup): HTMLAttributes<HTMLDivElement> {
68
+ return {
69
+ onDragOver: (e) => {
70
+ if (item) {
71
+ accept(e, `group:${group.key}`)
72
+ }
73
+ },
74
+ onDrop: (e) => {
75
+ e.preventDefault()
76
+ if (item?.kind === 'session') {
77
+ onChange(moveToCustomGroup(groups, item.key, group.custom))
78
+ } else if (item?.kind === 'group') {
79
+ onChange(moveCustomGroup(groups, item.id, group.custom))
80
+ }
81
+ end()
82
+ },
83
+ }
84
+ }
85
+
86
+ function header(group: SessionGroup): HTMLAttributes<HTMLDivElement> {
87
+ const id = group.custom
88
+ if (id === undefined) {
89
+ return {}
90
+ }
91
+ return { draggable: true, onDragStart: (e) => start(e, { kind: 'group', id }), onDragEnd: end }
92
+ }
93
+
94
+ return {
95
+ dragging: item !== undefined,
96
+ isOver: (spot: string) => over === spot,
97
+ session,
98
+ container,
99
+ header,
100
+ create: (name = 'New group') => {
101
+ const id = newCustomGroupId()
102
+ onChange(addCustomGroup(groups, name, id))
103
+ return id
104
+ },
105
+ rename: (id: string, name: string) => onChange(renameCustomGroup(groups, id, name)),
106
+ remove: (id: string) => onChange(removeCustomGroup(groups, id)),
107
+ }
108
+ }
109
+
110
+ export function CustomGroupHeader({
111
+ group,
112
+ editing,
113
+ onEditingChange,
114
+ onRename,
115
+ onRemove,
116
+ dragProps,
117
+ }: {
118
+ group: SessionGroup
119
+ editing: boolean
120
+ onEditingChange: (editing: boolean) => void
121
+ onRename: (name: string) => void
122
+ onRemove: () => void
123
+ dragProps: HTMLAttributes<HTMLDivElement>
124
+ }) {
125
+ if (editing) {
126
+ return <GroupNameInput initial={group.label ?? ''} onRename={onRename} onDone={() => onEditingChange(false)} />
127
+ }
128
+
129
+ const custom = group.custom !== undefined
130
+ return (
131
+ <GroupHeading
132
+ {...dragProps}
133
+ label={group.label ?? ''}
134
+ count={group.rows.length}
135
+ className={cn('group/heading', custom && 'cursor-grab')}
136
+ onDoubleClick={custom ? () => onEditingChange(true) : undefined}
137
+ leading={custom ? <GripVertical className="-ml-1.5 size-3 opacity-0 group-hover/heading:opacity-60" /> : undefined}
138
+ actions={
139
+ custom ? (
140
+ <>
141
+ <HeadingAction label="Rename group" onClick={() => onEditingChange(true)}>
142
+ <Pencil className="size-3" />
143
+ </HeadingAction>
144
+ <HeadingAction label="Delete group" title="Delete the group - its sessions move to Ungrouped" onClick={onRemove}>
145
+ <Trash2 className="size-3" />
146
+ </HeadingAction>
147
+ </>
148
+ ) : undefined
149
+ }
150
+ />
151
+ )
152
+ }
153
+
154
+ function GroupNameInput({ initial, onRename, onDone }: { initial: string; onRename: (name: string) => void; onDone: () => void }) {
155
+ const [draft, setDraft] = useState(initial)
156
+ const commit = () => {
157
+ const name = draft.trim()
158
+ if (name && name !== initial) {
159
+ onRename(name)
160
+ }
161
+ onDone()
162
+ }
163
+ return (
164
+ <div className="px-2">
165
+ <Input
166
+ autoFocus
167
+ aria-label="Group name"
168
+ value={draft}
169
+ onFocus={(e) => e.currentTarget.select()}
170
+ onChange={(e) => setDraft(e.target.value)}
171
+ onBlur={commit}
172
+ onKeyDown={(e) => {
173
+ if (e.key === 'Enter') {
174
+ commit()
175
+ } else if (e.key === 'Escape') {
176
+ e.stopPropagation()
177
+ onDone()
178
+ }
179
+ }}
180
+ className="h-6 text-label"
181
+ />
182
+ </div>
183
+ )
184
+ }
185
+
186
+ export function GroupHeading({
187
+ label,
188
+ count,
189
+ leading,
190
+ actions,
191
+ className,
192
+ ...rest
193
+ }: {
194
+ label: string
195
+ count: number
196
+ leading?: ReactNode
197
+ actions?: ReactNode
198
+ } & HTMLAttributes<HTMLDivElement>) {
199
+ return (
200
+ <div {...rest} className={cn('group/heading flex min-h-5 items-center gap-2 px-2 text-label font-medium text-fg-4', className)}>
201
+ {leading}
202
+ <span className="min-w-0 truncate uppercase tracking-wide">{label}</span>
203
+ <span className="text-fg-4/70">{count}</span>
204
+ {actions ? (
205
+ <span className="ml-auto flex items-center gap-0.5 opacity-0 transition-opacity group-hover/heading:opacity-100 focus-within:opacity-100">
206
+ {actions}
207
+ </span>
208
+ ) : null}
209
+ </div>
210
+ )
211
+ }
212
+
213
+ export function HeadingAction({
214
+ label,
215
+ title,
216
+ onClick,
217
+ children,
218
+ }: {
219
+ label: string
220
+ title?: string
221
+ onClick: () => void
222
+ children: ReactNode
223
+ }) {
224
+ return (
225
+ <Button
226
+ variant="ghost"
227
+ size="icon-sm"
228
+ aria-label={label}
229
+ title={title ?? label}
230
+ className="size-5 text-fg-3"
231
+ onClick={(e) => {
232
+ e.stopPropagation()
233
+ onClick()
234
+ }}
235
+ >
236
+ {children}
237
+ </Button>
238
+ )
239
+ }
240
+
241
+ export function NewGroupButton({ onClick }: { onClick: () => void }) {
242
+ return (
243
+ <Button variant="ghost" size="xs" className="mx-1 self-start text-fg-3" onClick={onClick}>
244
+ <Plus />
245
+ New group
246
+ </Button>
247
+ )
248
+ }
@@ -1,5 +1,5 @@
1
1
  import { Fragment, useEffect, useState, type ReactNode } from 'react'
2
- import type { MessageOrigin } from '@workerdeck/protocol'
2
+ import { peerShortId, type MessageOrigin } from '@workerdeck/protocol'
3
3
  import type { ShellItem, TranscriptItem } from '@workerdeck/react'
4
4
  import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
5
5
  import { isMutatingTool } from '../../lib/tool-icon.ts'
@@ -41,7 +41,7 @@ function clipToChars(lines: string[], maxChars: number): string[] {
41
41
  }
42
42
 
43
43
  export function peerLabel(origin: MessageOrigin): string {
44
- const who = origin.name ? `${origin.name} (${origin.sessionId.slice(0, 8)})` : origin.sessionId.slice(0, 8)
44
+ const who = origin.name ? `${origin.name} (${peerShortId(origin.sessionId)})` : peerShortId(origin.sessionId)
45
45
  return `message from ${origin.engine ? `${origin.engine} session ` : 'session '}${who}`
46
46
  }
47
47
 
@@ -1,4 +1,4 @@
1
- import { isPeerSendTool, peerDeliveredTo, type MessageOrigin } from '@workerdeck/protocol'
1
+ import { isPeerSendTool, peerDeliveredTo, peerShortId, type MessageOrigin } from '@workerdeck/protocol'
2
2
  import type { TranscriptItem } from '@workerdeck/react'
3
3
  import { toolInputPreview } from '../../lib/format.ts'
4
4
  import { isShellTool } from '../../lib/tool-icon.ts'
@@ -12,7 +12,10 @@ export function isPeerSend(item: TranscriptItem): boolean {
12
12
  }
13
13
 
14
14
  export function peerName(origin: MessageOrigin): string {
15
- return origin.name ?? origin.sessionId.slice(0, 8)
15
+ if (!origin.name) {
16
+ return peerShortId(origin.sessionId)
17
+ }
18
+ return origin.hostId ? `${origin.name}@${origin.hostId}` : origin.name
16
19
  }
17
20
 
18
21
  // Every peer this transcript has named, by session id. A conversation names its peers twice over: an
@@ -48,7 +51,7 @@ export function peerSendTarget(item: ToolCallItem, names?: ReadonlyMap<string, s
48
51
  if (sessionId === undefined) {
49
52
  return 'peer'
50
53
  }
51
- return delivered?.name ?? names?.get(sessionId) ?? sessionId.slice(0, 8)
54
+ return delivered?.name ?? names?.get(sessionId) ?? peerShortId(sessionId)
52
55
  }
53
56
 
54
57
  export function peerSendText(item: ToolCallItem): string {
package/src/index.ts CHANGED
@@ -156,7 +156,13 @@ export {
156
156
  export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsDialog.tsx'
157
157
  export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
158
158
  export { SessionList, SessionListItem, type SessionListItemProps, type SessionListProps } from './components/agent/SessionList.tsx'
159
- export { SessionBrowser, SessionSearch, rowShapeClass, type SessionBrowserProps } from './components/agent/SessionBrowser.tsx'
159
+ export {
160
+ SessionBrowser,
161
+ SessionSearch,
162
+ rowShapeClass,
163
+ type GroupTarget,
164
+ type SessionBrowserProps,
165
+ } from './components/agent/SessionBrowser.tsx'
160
166
  export {
161
167
  STEP_DISPLAY_OPTIONS,
162
168
  SessionFilters,