@workerdeck/ui 3.4.0 → 3.6.1
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/index.d.mts +55 -4
- package/build/index.mjs +702 -163
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +131 -0
- package/package.json +4 -4
- package/src/components/agent/AgentAvatar.tsx +117 -0
- package/src/components/agent/SessionBrowser.tsx +246 -43
- package/src/components/agent/SessionGroups.tsx +256 -23
- package/src/components/agent/SessionItem.tsx +200 -68
- package/src/index.ts +11 -0
- package/src/lib/team-drop.ts +65 -0
- package/src/styles/theme.css +22 -0
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
import type { HTMLAttributes, ReactNode } from 'react'
|
|
3
|
-
import { BedDouble, Eraser, FolderOpen, Layers, Pencil, Plus, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { BedDouble, ChevronDown, ChevronRight, Eraser, FolderOpen, Layers, Pencil, Plus, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
clearFilters,
|
|
6
|
+
filterRows,
|
|
7
|
+
groupRows,
|
|
8
|
+
hasFacetFilter,
|
|
9
|
+
isTeamCollapsed,
|
|
10
|
+
scopeActive,
|
|
11
|
+
sessionKey,
|
|
12
|
+
subsetSummary,
|
|
13
|
+
teamSummary,
|
|
14
|
+
toggleTeamCollapsed,
|
|
15
|
+
} from '@workerdeck/protocol'
|
|
16
|
+
import type {
|
|
17
|
+
SessionGroup,
|
|
18
|
+
SessionRow,
|
|
19
|
+
SessionState,
|
|
20
|
+
SessionTask,
|
|
21
|
+
StepDisplay,
|
|
22
|
+
SubagentDisplay,
|
|
23
|
+
ViewConfig,
|
|
24
|
+
WorkspaceScope,
|
|
25
|
+
} from '@workerdeck/protocol'
|
|
26
|
+
import { AgentAvatar, avatarOf, type AgentAvatars } from './AgentAvatar.tsx'
|
|
6
27
|
import { Button } from '../ui/Button.tsx'
|
|
7
28
|
import { Empty } from '../ui/Empty.tsx'
|
|
8
29
|
import { Input } from '../ui/Input.tsx'
|
|
9
30
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
10
31
|
import { SessionFilters } from './SessionFilters.tsx'
|
|
11
32
|
import { SessionItem, type SelectModifiers } from './SessionItem.tsx'
|
|
12
|
-
import { CustomGroupHeader, GroupHeading, HeadingAction, NewGroupButton, useGroupDrag } from './SessionGroups.tsx'
|
|
33
|
+
import { CustomGroupHeader, GroupHeading, HeadingAction, NewGroupButton, useGroupDrag, type DropCue } from './SessionGroups.tsx'
|
|
34
|
+
import type { TeamMove } from '../../lib/team-drop.ts'
|
|
13
35
|
import { cn } from '../../lib/utils.ts'
|
|
14
36
|
|
|
15
37
|
export { SessionStatusIcon } from './SessionStatusIcon.tsx'
|
|
@@ -33,6 +55,8 @@ export interface SessionBrowserProps {
|
|
|
33
55
|
onSelect?: (row: SessionRow, modifiers: SelectModifiers) => void
|
|
34
56
|
onDelete?: (row: SessionRow) => void
|
|
35
57
|
onRename?: (row: SessionRow, title: string) => void
|
|
58
|
+
// An agent card's name is the agent's, not the session title; without this, agent cards are not renamable.
|
|
59
|
+
onRenameAgent?: (row: SessionRow, name: string) => void
|
|
36
60
|
onClearContext?: (row: SessionRow) => void
|
|
37
61
|
onSleep?: (row: SessionRow) => void
|
|
38
62
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
@@ -41,6 +65,10 @@ export interface SessionBrowserProps {
|
|
|
41
65
|
onSelectShell?: (row: SessionRow, shellId: string) => void
|
|
42
66
|
onKillShell?: (row: SessionRow, shellId: string) => void
|
|
43
67
|
onShellAgentWrite?: (row: SessionRow, shellId: string, enabled: boolean) => void
|
|
68
|
+
avatars?: AgentAvatars
|
|
69
|
+
// Makes cards draggable onto each other: the middle of a card joins its team, a member dropped outside leaves.
|
|
70
|
+
// Reject with an Error to draw its message under the card.
|
|
71
|
+
onTeamMove?: (move: TeamMove) => Promise<void> | void
|
|
44
72
|
// Draws a `+` on each project heading: start a session on that gateway, in that project's root.
|
|
45
73
|
onCreateInGroup?: (target: GroupTarget) => void
|
|
46
74
|
emptyState?: ReactNode
|
|
@@ -74,6 +102,7 @@ export function SessionBrowser({
|
|
|
74
102
|
onSelect,
|
|
75
103
|
onDelete,
|
|
76
104
|
onRename,
|
|
105
|
+
onRenameAgent,
|
|
77
106
|
onClearContext,
|
|
78
107
|
onSleep,
|
|
79
108
|
onSelectSubagent,
|
|
@@ -82,6 +111,8 @@ export function SessionBrowser({
|
|
|
82
111
|
onSelectShell,
|
|
83
112
|
onKillShell,
|
|
84
113
|
onShellAgentWrite,
|
|
114
|
+
avatars,
|
|
115
|
+
onTeamMove,
|
|
85
116
|
onCreateInGroup,
|
|
86
117
|
emptyState,
|
|
87
118
|
showControls = true,
|
|
@@ -93,19 +124,27 @@ export function SessionBrowser({
|
|
|
93
124
|
const rowGateways = useMemo(() => new Set(rows.map((row) => row.hostId)).size, [rows])
|
|
94
125
|
const gateways = gatewayCount ?? rowGateways
|
|
95
126
|
const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])
|
|
96
|
-
const groups = useMemo(() => groupRows(visible, config, { gatewayCount: gateways }), [visible, config, gateways])
|
|
127
|
+
const groups = useMemo(() => groupRows(visible, config, { gatewayCount: gateways, all: rows }), [visible, config, gateways, rows])
|
|
97
128
|
const subset = subsetSummary(config, scope, visible.length, rows.length)
|
|
129
|
+
const tiles = useMemo(() => rows.some((row) => row.info.agent !== undefined), [rows])
|
|
98
130
|
const custom = config.groupBy === 'custom'
|
|
99
131
|
const [editingGroup, setEditingGroup] = useState<string>()
|
|
100
132
|
|
|
101
133
|
const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
|
|
102
|
-
const
|
|
134
|
+
const byKey = useMemo(() => new Map(rows.map((row) => [sessionKey(row), row])), [rows])
|
|
135
|
+
const drag = useGroupDrag(config.customGroups ?? [], (customGroups) => set({ customGroups }), {
|
|
136
|
+
custom,
|
|
137
|
+
team: onTeamMove ? { rowOf: (key) => byKey.get(key), onMove: onTeamMove } : undefined,
|
|
138
|
+
})
|
|
139
|
+
const draggable = custom || onTeamMove !== undefined
|
|
103
140
|
|
|
104
141
|
const heading = (group: SessionGroup) => {
|
|
105
142
|
if (custom) {
|
|
106
143
|
return (
|
|
107
144
|
<CustomGroupHeader
|
|
108
145
|
group={group}
|
|
146
|
+
look={config.customGroups?.find((custom) => custom.id === group.custom)}
|
|
147
|
+
onStyle={(look) => group.custom && drag.style(group.custom, look)}
|
|
109
148
|
editing={group.custom !== undefined && editingGroup === group.custom}
|
|
110
149
|
onEditingChange={(editing) => setEditingGroup(editing ? group.custom : undefined)}
|
|
111
150
|
onRename={(name) => group.custom && drag.rename(group.custom, name)}
|
|
@@ -114,6 +153,19 @@ export function SessionBrowser({
|
|
|
114
153
|
/>
|
|
115
154
|
)
|
|
116
155
|
}
|
|
156
|
+
if (group.earlier) {
|
|
157
|
+
return (
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
aria-expanded={config.earlierOpen === true}
|
|
161
|
+
onClick={() => set({ earlierOpen: !config.earlierOpen })}
|
|
162
|
+
className="flex items-center gap-1 px-2 text-left text-label text-fg-4 hover:text-fg-2"
|
|
163
|
+
>
|
|
164
|
+
{config.earlierOpen ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
|
|
165
|
+
Earlier · {group.rows.length} one-off session{group.rows.length === 1 ? '' : 's'}
|
|
166
|
+
</button>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
117
169
|
if (config.groupBy === 'none' || !group.label) {
|
|
118
170
|
return null
|
|
119
171
|
}
|
|
@@ -196,12 +248,12 @@ export function SessionBrowser({
|
|
|
196
248
|
<Empty icon={<Layers />} title="Nothing here" description="No session to show." />
|
|
197
249
|
)
|
|
198
250
|
) : (
|
|
199
|
-
<div className="flex flex-col gap-
|
|
251
|
+
<div className="flex flex-col gap-2 px-1">
|
|
200
252
|
{groups.map((group) => (
|
|
201
253
|
<div
|
|
202
254
|
key={group.key}
|
|
203
|
-
{...(
|
|
204
|
-
className={cn('flex flex-col gap-1 rounded-md',
|
|
255
|
+
{...(draggable ? drag.container(group) : {})}
|
|
256
|
+
className={cn('flex flex-col gap-1 rounded-md', draggable && drag.isOver(`group:${group.key}`) && 'bg-row-hover/50')}
|
|
205
257
|
>
|
|
206
258
|
{heading(group)}
|
|
207
259
|
{custom && group.custom !== undefined && group.rows.length === 0 ? (
|
|
@@ -209,36 +261,70 @@ export function SessionBrowser({
|
|
|
209
261
|
Drag sessions here
|
|
210
262
|
</div>
|
|
211
263
|
) : null}
|
|
212
|
-
{group.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
{group.earlier && !config.earlierOpen
|
|
265
|
+
? null
|
|
266
|
+
: group.rows.map((row) => {
|
|
267
|
+
const item = (member: SessionRow, variant: 'card' | 'member', extra: Partial<SessionRowItemProps> = {}) => (
|
|
268
|
+
<SessionRowItem
|
|
269
|
+
key={sessionKey(member)}
|
|
270
|
+
dragProps={
|
|
271
|
+
onTeamMove || (custom && variant === 'card')
|
|
272
|
+
? drag.session(sessionKey(member), group, variant === 'member' ? { row: member, lead: row } : { row })
|
|
273
|
+
: undefined
|
|
274
|
+
}
|
|
275
|
+
dropCue={draggable ? drag.cue(sessionKey(member)) : undefined}
|
|
276
|
+
row={member}
|
|
277
|
+
variant={variant}
|
|
278
|
+
active={isActive(member)}
|
|
279
|
+
activeSubagentId={activeSubagentId}
|
|
280
|
+
activeShellId={activeShellId}
|
|
281
|
+
actions={rowActions?.(member)}
|
|
282
|
+
now={now}
|
|
283
|
+
showGateway={gateways > 1 && config.groupBy !== 'project'}
|
|
284
|
+
showProject={config.groupBy !== 'project'}
|
|
285
|
+
subagents={config.subagents}
|
|
286
|
+
shells={config.shells}
|
|
287
|
+
tasks={config.tasks}
|
|
288
|
+
projectIcons={projectIcons}
|
|
289
|
+
avatars={avatars}
|
|
290
|
+
tile={tiles}
|
|
291
|
+
onSelect={onSelect}
|
|
292
|
+
onDelete={onDelete}
|
|
293
|
+
onRename={member.info.agent ? onRenameAgent : onRename}
|
|
294
|
+
onClearContext={onClearContext}
|
|
295
|
+
onSleep={onSleep}
|
|
296
|
+
onSelectSubagent={onSelectSubagent}
|
|
297
|
+
onSelectTask={onSelectTask}
|
|
298
|
+
onStopTask={onStopTask}
|
|
299
|
+
onSelectShell={onSelectShell}
|
|
300
|
+
onKillShell={onKillShell}
|
|
301
|
+
onShellAgentWrite={onShellAgentWrite}
|
|
302
|
+
{...extra}
|
|
303
|
+
/>
|
|
304
|
+
)
|
|
305
|
+
if (!row.members?.length) {
|
|
306
|
+
return item(row, 'card', { className: row.context ? 'opacity-50' : undefined })
|
|
307
|
+
}
|
|
308
|
+
const collapsed = isTeamCollapsed(config, row)
|
|
309
|
+
return (
|
|
310
|
+
<TeamBlock
|
|
311
|
+
key={sessionKey(row)}
|
|
312
|
+
lead={row}
|
|
313
|
+
collapsed={collapsed}
|
|
314
|
+
avatars={avatars}
|
|
315
|
+
onToggle={() => onConfigChange(toggleTeamCollapsed(config, row))}
|
|
316
|
+
renderLead={(summary, tray) =>
|
|
317
|
+
item(row, 'card', {
|
|
318
|
+
className: row.context ? 'opacity-50' : undefined,
|
|
319
|
+
badgeState: collapsed ? row.teamState : undefined,
|
|
320
|
+
teamUnseen: collapsed ? summary.unseen : 0,
|
|
321
|
+
teamTray: tray,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
renderMember={(member) => item(member, 'member')}
|
|
325
|
+
/>
|
|
326
|
+
)
|
|
327
|
+
})}
|
|
242
328
|
</div>
|
|
243
329
|
))}
|
|
244
330
|
{custom ? <NewGroupButton onClick={() => setEditingGroup(drag.create())} /> : null}
|
|
@@ -255,8 +341,15 @@ function iconSrcOf(row: SessionRow | undefined, icons: Record<string, string> |
|
|
|
255
341
|
|
|
256
342
|
interface SessionRowItemProps {
|
|
257
343
|
row: SessionRow
|
|
344
|
+
variant?: 'card' | 'member'
|
|
345
|
+
avatars?: AgentAvatars
|
|
346
|
+
badgeState?: SessionState
|
|
347
|
+
teamUnseen?: number
|
|
348
|
+
teamTray?: ReactNode
|
|
349
|
+
tile?: boolean
|
|
350
|
+
className?: string
|
|
258
351
|
dragProps?: HTMLAttributes<HTMLDivElement>
|
|
259
|
-
|
|
352
|
+
dropCue?: DropCue
|
|
260
353
|
active?: boolean
|
|
261
354
|
actions?: ReactNode
|
|
262
355
|
activeSubagentId?: string
|
|
@@ -283,8 +376,15 @@ interface SessionRowItemProps {
|
|
|
283
376
|
|
|
284
377
|
function SessionRowItem({
|
|
285
378
|
row,
|
|
379
|
+
variant,
|
|
380
|
+
avatars,
|
|
381
|
+
badgeState,
|
|
382
|
+
teamUnseen,
|
|
383
|
+
teamTray,
|
|
384
|
+
tile,
|
|
385
|
+
className,
|
|
286
386
|
dragProps,
|
|
287
|
-
|
|
387
|
+
dropCue,
|
|
288
388
|
active,
|
|
289
389
|
actions,
|
|
290
390
|
activeSubagentId,
|
|
@@ -314,6 +414,13 @@ function SessionRowItem({
|
|
|
314
414
|
const item = (
|
|
315
415
|
<SessionItem
|
|
316
416
|
row={row}
|
|
417
|
+
variant={variant}
|
|
418
|
+
avatars={avatars}
|
|
419
|
+
badgeState={badgeState}
|
|
420
|
+
teamUnseen={teamUnseen}
|
|
421
|
+
teamTray={teamTray}
|
|
422
|
+
tile={tile}
|
|
423
|
+
className={className}
|
|
317
424
|
active={active === true}
|
|
318
425
|
activeStepKey={active ? (activeShellId ?? activeSubagentId) : undefined}
|
|
319
426
|
now={now}
|
|
@@ -338,7 +445,7 @@ function SessionRowItem({
|
|
|
338
445
|
actions ?? (
|
|
339
446
|
<>
|
|
340
447
|
{onRename && !editing ? (
|
|
341
|
-
<RowAction label=
|
|
448
|
+
<RowAction label={info.agent ? 'Rename agent' : 'Rename session'} onClick={() => setEditing(true)}>
|
|
342
449
|
<Pencil className="size-3 text-fg-3" />
|
|
343
450
|
</RowAction>
|
|
344
451
|
) : null}
|
|
@@ -373,9 +480,105 @@ function SessionRowItem({
|
|
|
373
480
|
if (!dragProps) {
|
|
374
481
|
return item
|
|
375
482
|
}
|
|
483
|
+
const zone = dropCue?.zone
|
|
376
484
|
return (
|
|
377
|
-
<div
|
|
485
|
+
<div
|
|
486
|
+
{...dragProps}
|
|
487
|
+
title={zone === 'refused' ? dropCue?.message : undefined}
|
|
488
|
+
className={cn(
|
|
489
|
+
'rounded-md',
|
|
490
|
+
zone === 'before' && 'shadow-[inset_0_2px_0_0_var(--color-accent)]',
|
|
491
|
+
zone === 'after' && 'shadow-[inset_0_-2px_0_0_var(--color-accent)]',
|
|
492
|
+
zone === 'join' && 'bg-accent/10 ring-1 ring-accent',
|
|
493
|
+
zone === 'refused' && 'ring-1 ring-danger/60',
|
|
494
|
+
)}
|
|
495
|
+
>
|
|
378
496
|
{item}
|
|
497
|
+
{zone === 'refused' && dropCue?.message ? (
|
|
498
|
+
<div role="status" className="px-3 pb-1 text-label text-danger">
|
|
499
|
+
{dropCue.message}
|
|
500
|
+
</div>
|
|
501
|
+
) : null}
|
|
502
|
+
</div>
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const TRAY_MAX = 4
|
|
507
|
+
|
|
508
|
+
// A folded team draws its members on the lead's first line (avatar and status each) and no row of its own; the tray
|
|
509
|
+
// unfolds it. Expanded, a tree line runs from the lead's avatar with the knob atop it, so neither state needs a twistie.
|
|
510
|
+
function TeamBlock({
|
|
511
|
+
lead,
|
|
512
|
+
collapsed,
|
|
513
|
+
avatars,
|
|
514
|
+
onToggle,
|
|
515
|
+
renderLead,
|
|
516
|
+
renderMember,
|
|
517
|
+
}: {
|
|
518
|
+
lead: SessionRow
|
|
519
|
+
collapsed: boolean
|
|
520
|
+
avatars?: AgentAvatars
|
|
521
|
+
onToggle: () => void
|
|
522
|
+
renderLead: (summary: ReturnType<typeof teamSummary>, tray?: ReactNode) => ReactNode
|
|
523
|
+
renderMember: (member: SessionRow) => ReactNode
|
|
524
|
+
}) {
|
|
525
|
+
const members = lead.members ?? []
|
|
526
|
+
const summary = teamSummary(lead)
|
|
527
|
+
if (collapsed) {
|
|
528
|
+
const surfaced = members.filter((member) => member.state === 'attention')
|
|
529
|
+
const counts = [
|
|
530
|
+
`${summary.members} member${summary.members === 1 ? '' : 's'}`,
|
|
531
|
+
summary.working ? `${summary.working} working` : undefined,
|
|
532
|
+
summary.attention ? `${summary.attention} need${summary.attention === 1 ? 's' : ''} you` : undefined,
|
|
533
|
+
].filter(Boolean)
|
|
534
|
+
const hidden = members.length - TRAY_MAX
|
|
535
|
+
const tray = (
|
|
536
|
+
<button
|
|
537
|
+
type="button"
|
|
538
|
+
aria-expanded={false}
|
|
539
|
+
aria-label={`Expand ${lead.info.agent?.name ?? 'team'}: ${counts.join(', ')}`}
|
|
540
|
+
title={counts.join(' · ')}
|
|
541
|
+
onClick={(e) => {
|
|
542
|
+
e.stopPropagation()
|
|
543
|
+
onToggle()
|
|
544
|
+
}}
|
|
545
|
+
className="flex shrink-0 items-center gap-1 rounded-[4px] px-0.5 hover:bg-row-hover"
|
|
546
|
+
>
|
|
547
|
+
{members.slice(0, hidden > 0 ? TRAY_MAX - 1 : TRAY_MAX).map((member) => (
|
|
548
|
+
<AgentAvatar
|
|
549
|
+
key={sessionKey(member)}
|
|
550
|
+
row={member}
|
|
551
|
+
image={avatarOf(avatars, member.info.agent)}
|
|
552
|
+
size={16}
|
|
553
|
+
className="rounded-[3px]"
|
|
554
|
+
/>
|
|
555
|
+
))}
|
|
556
|
+
{hidden > 0 ? <span className="text-label text-fg-4 tabular-nums">+{hidden + 1}</span> : null}
|
|
557
|
+
</button>
|
|
558
|
+
)
|
|
559
|
+
return (
|
|
560
|
+
<div data-slot="team" data-collapsed className="flex flex-col">
|
|
561
|
+
{renderLead(summary, tray)}
|
|
562
|
+
{surfaced.length ? <div className="ml-3 flex flex-col">{surfaced.map(renderMember)}</div> : null}
|
|
563
|
+
</div>
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
return (
|
|
567
|
+
<div data-slot="team" className="flex flex-col">
|
|
568
|
+
{renderLead(summary)}
|
|
569
|
+
<div className="relative ml-3.5 flex flex-col pl-1">
|
|
570
|
+
<span aria-hidden className="absolute top-0 bottom-3 left-0 w-px bg-border" />
|
|
571
|
+
<button
|
|
572
|
+
type="button"
|
|
573
|
+
aria-expanded
|
|
574
|
+
aria-label={`Collapse ${lead.info.agent?.name ?? 'team'}`}
|
|
575
|
+
onClick={onToggle}
|
|
576
|
+
className="absolute -top-1 -left-[7px] flex size-3.5 items-center justify-center rounded-full border border-border bg-bg text-fg-4 hover:text-fg-1"
|
|
577
|
+
>
|
|
578
|
+
<ChevronDown className="size-2.5" />
|
|
579
|
+
</button>
|
|
580
|
+
{members.map(renderMember)}
|
|
581
|
+
</div>
|
|
379
582
|
</div>
|
|
380
583
|
)
|
|
381
584
|
}
|