@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.
- package/build/index.d.mts +7 -2
- package/build/index.mjs +276 -60
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +36 -0
- package/build/{use-path-links-C6Fh5zdx.mjs → use-path-links-Cby4i_VL.mjs} +6 -5
- package/build/use-path-links-Cby4i_VL.mjs.map +1 -0
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/SessionBrowser.tsx +82 -17
- package/src/components/agent/SessionFilters.tsx +2 -2
- package/src/components/agent/SessionGroups.tsx +248 -0
- package/src/components/terminal/items.tsx +2 -2
- package/src/components/terminal/tool-run.ts +6 -3
- package/src/index.ts +7 -1
- package/build/use-path-links-C6Fh5zdx.mjs.map +0 -1
package/build/workspace.mjs
CHANGED
|
@@ -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-
|
|
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.
|
|
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/
|
|
43
|
-
"@workerdeck/
|
|
44
|
-
"@workerdeck/
|
|
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
|
|
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
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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={
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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,
|