@workerdeck/ui 2.15.0 → 3.1.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/README.md +6 -5
- package/build/{SessionPanel-BVKqPs_3.d.mts → SessionPanel-BNNc8grK.d.mts} +18 -16
- package/build/index.d.mts +194 -49
- package/build/index.mjs +562 -260
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +170 -21
- package/build/{use-path-links-BgWXmfAS.mjs → use-path-links-C6Fh5zdx.mjs} +3312 -2758
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +3 -3
- package/build/workspace.mjs +3 -3
- package/build/workspace.mjs.map +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 +22 -17
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Response.tsx +8 -1
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +131 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +29 -6
- package/src/components/agent/SessionPanel.tsx +249 -498
- package/src/components/agent/SessionSteps.tsx +98 -14
- package/src/components/agent/SessionWorkspace.tsx +3 -3
- package/src/components/agent/ShellStrip.tsx +43 -36
- package/src/components/agent/ShellTerminal.tsx +1 -1
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/ToolCallCard.tsx +18 -41
- package/src/components/agent/Transcript.tsx +111 -93
- package/src/components/agent/TranscriptItemView.tsx +80 -67
- package/src/components/agent/TranscriptRows.tsx +4 -2
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/image-viewer.tsx +326 -0
- package/src/components/agent/markdown-image.tsx +139 -0
- 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 +38 -1
- package/src/components/agent/tool-result-image.tsx +40 -0
- package/src/components/agent/transcript-variant.tsx +3 -21
- 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/prompt-area/use-prompt-area-keydown.ts +4 -2
- package/src/components/terminal/PermissionPrompt.tsx +31 -28
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +23 -89
- package/src/components/terminal/affordances.tsx +100 -74
- package/src/components/terminal/height.ts +48 -32
- package/src/components/terminal/image-box.ts +31 -0
- package/src/components/terminal/items.tsx +70 -30
- package/src/components/terminal/markdown.tsx +4 -1
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/terminal/shell-row.ts +17 -3
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +65 -12
- 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/src/lib/shell-request.ts +115 -0
- package/src/lib/tool-icon.ts +13 -1
- package/src/styles/terminal.css +129 -22
- package/src/styles/theme.css +4 -0
- package/build/use-path-links-BgWXmfAS.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { memo } from 'react'
|
|
2
|
-
import { Streamdown } from 'streamdown'
|
|
2
|
+
import { Streamdown, type Components } from 'streamdown'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
import { MARKDOWN_REHYPE_PLUGINS, MarkdownImage } from './markdown-image.tsx'
|
|
5
|
+
|
|
6
|
+
const COMPONENTS: Components = {
|
|
7
|
+
img: ({ src, alt }) => <MarkdownImage src={src} alt={alt} />,
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
export interface ResponseProps {
|
|
6
11
|
children: string
|
|
@@ -16,6 +21,8 @@ export const Response = memo(
|
|
|
16
21
|
parseIncompleteMarkdown={streaming}
|
|
17
22
|
shikiTheme={['github-light', 'github-dark']}
|
|
18
23
|
linkSafety={{ enabled: false }}
|
|
24
|
+
components={COMPONENTS}
|
|
25
|
+
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
|
|
19
26
|
className={cn('size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0', className)}
|
|
20
27
|
>
|
|
21
28
|
{children}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
3
3
|
import type { TranscriptItem } from '@workerdeck/react'
|
|
4
|
-
import { toolInputPreview } from '../../lib/format.ts'
|
|
5
4
|
import { cn } from '../../lib/utils.ts'
|
|
6
5
|
import {
|
|
7
6
|
approvalCluster,
|
|
8
7
|
buildMarks,
|
|
9
8
|
clusterMarks,
|
|
10
|
-
doneLine,
|
|
11
|
-
excerpt,
|
|
12
|
-
KIND_NAME,
|
|
13
9
|
nearestMember,
|
|
10
|
+
proportionalPlacement,
|
|
14
11
|
type Cluster,
|
|
15
12
|
type Mark,
|
|
16
13
|
} from './scrubber-marks.ts'
|
|
14
|
+
import { CARD_PEEK, peekContent } from './scrubber-peek.tsx'
|
|
17
15
|
|
|
18
16
|
export interface ScrubberProps {
|
|
19
17
|
items: readonly TranscriptItem[]
|
|
@@ -26,74 +24,6 @@ export interface ScrubberProps {
|
|
|
26
24
|
className?: string
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function peekContent(
|
|
30
|
-
cluster: Cluster,
|
|
31
|
-
first: Mark | undefined,
|
|
32
|
-
items: readonly TranscriptItem[],
|
|
33
|
-
pendingApprovals: readonly PermissionRequest[],
|
|
34
|
-
): ReactNode {
|
|
35
|
-
const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : ''
|
|
36
|
-
let body: ReactNode = null
|
|
37
|
-
if (cluster.kind === 'approval') {
|
|
38
|
-
const request = pendingApprovals[0]
|
|
39
|
-
body = request ? (
|
|
40
|
-
<>
|
|
41
|
-
<div>{request.title ?? 'Permission required'}</div>
|
|
42
|
-
<div className="wd-scrub-ex" data-tone="muted">
|
|
43
|
-
{`${request.displayName ?? request.toolName}(${toolInputPreview(request.input)})`}
|
|
44
|
-
</div>
|
|
45
|
-
</>
|
|
46
|
-
) : null
|
|
47
|
-
} else if (first && first.kind !== 'recap') {
|
|
48
|
-
const item = items[first.itemIndex]
|
|
49
|
-
if (first.kind === 'turn' || first.kind === 'turnFailed') {
|
|
50
|
-
const turn = first.turnIndex === undefined ? undefined : items[first.turnIndex]
|
|
51
|
-
body = (
|
|
52
|
-
<>
|
|
53
|
-
{item?.kind === 'assistant_text' ? <div className="wd-scrub-ex">{item.text}</div> : null}
|
|
54
|
-
{turn?.kind === 'turn_result' ? (
|
|
55
|
-
<>
|
|
56
|
-
<div data-tone={turn.isError ? 'danger' : 'muted'}>{doneLine(turn)}</div>
|
|
57
|
-
{turn.errors?.map((message, index) => (
|
|
58
|
-
<div key={index} data-tone="danger">
|
|
59
|
-
{message}
|
|
60
|
-
</div>
|
|
61
|
-
))}
|
|
62
|
-
</>
|
|
63
|
-
) : null}
|
|
64
|
-
</>
|
|
65
|
-
)
|
|
66
|
-
} else if (item) {
|
|
67
|
-
const failure =
|
|
68
|
-
first.kind === 'toolFailed' && item.kind === 'tool_call'
|
|
69
|
-
? item.result?.text.split('\n').find((line) => line.trim() !== '')
|
|
70
|
-
: undefined
|
|
71
|
-
body = (
|
|
72
|
-
<>
|
|
73
|
-
<div className="wd-scrub-ex" data-tone={first.kind === 'error' || first.kind === 'toolFailed' ? 'danger' : undefined}>
|
|
74
|
-
{first.kind === 'user' ? <span data-tone="muted">{'❯ '}</span> : null}
|
|
75
|
-
{excerpt(item)}
|
|
76
|
-
</div>
|
|
77
|
-
{failure ? (
|
|
78
|
-
<div className="wd-scrub-ex" data-tone="danger">
|
|
79
|
-
{failure}
|
|
80
|
-
</div>
|
|
81
|
-
) : null}
|
|
82
|
-
</>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return (
|
|
87
|
-
<>
|
|
88
|
-
<div data-tone="muted">
|
|
89
|
-
{KIND_NAME[first?.kind ?? cluster.kind]}
|
|
90
|
-
{more}
|
|
91
|
-
</div>
|
|
92
|
-
{body}
|
|
93
|
-
</>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
27
|
export function Scrubber({
|
|
98
28
|
items,
|
|
99
29
|
pendingApprovals,
|
|
@@ -138,7 +68,8 @@ export function Scrubber({
|
|
|
138
68
|
if (railH <= 0) {
|
|
139
69
|
return []
|
|
140
70
|
}
|
|
141
|
-
const
|
|
71
|
+
const marks = buildMarks(items, { frameParentId, bookmarks, recapItemIndex })
|
|
72
|
+
const built = clusterMarks(marks, proportionalPlacement(railH, items.length))
|
|
142
73
|
if (pendingApprovals.length > 0) {
|
|
143
74
|
built.push(approvalCluster(railH))
|
|
144
75
|
}
|
|
@@ -191,7 +122,7 @@ export function Scrubber({
|
|
|
191
122
|
))}
|
|
192
123
|
{peek ? (
|
|
193
124
|
<div ref={peekRef} className="wd-scrub-peek" style={{ top: peek.y }}>
|
|
194
|
-
{peekContent(peek.cluster, peek.mark, items, pendingApprovals)}
|
|
125
|
+
{peekContent({ cluster: peek.cluster, first: peek.mark, items, pendingApprovals }, CARD_PEEK)}
|
|
195
126
|
</div>
|
|
196
127
|
) : null}
|
|
197
128
|
</div>
|
|
@@ -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,15 +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
|
|
42
|
+
emptyState?: ReactNode
|
|
43
|
+
// The facet controls, drawn inline. A host with a header puts `SessionFiltersButton` there instead.
|
|
43
44
|
showControls?: boolean
|
|
45
|
+
showSearch?: boolean
|
|
46
|
+
autoFocusSearch?: boolean
|
|
44
47
|
projectIcons?: Record<string, string>
|
|
45
48
|
className?: string
|
|
46
49
|
}
|
|
@@ -58,127 +61,42 @@ export function SessionBrowser({
|
|
|
58
61
|
activeSubagentId,
|
|
59
62
|
activeShellId,
|
|
60
63
|
now,
|
|
64
|
+
isActive = (row) => row.info.id === activeId,
|
|
65
|
+
rowActions,
|
|
66
|
+
gatewayCount,
|
|
67
|
+
showSubset = true,
|
|
61
68
|
onSelect,
|
|
62
69
|
onDelete,
|
|
63
70
|
onRename,
|
|
64
71
|
onClearContext,
|
|
65
72
|
onSelectSubagent,
|
|
73
|
+
onSelectTask,
|
|
74
|
+
onStopTask,
|
|
66
75
|
onSelectShell,
|
|
67
76
|
onKillShell,
|
|
77
|
+
onShellAgentWrite,
|
|
68
78
|
emptyState,
|
|
69
79
|
showControls = true,
|
|
80
|
+
showSearch = showControls,
|
|
81
|
+
autoFocusSearch,
|
|
70
82
|
projectIcons,
|
|
71
83
|
className,
|
|
72
84
|
}: SessionBrowserProps) {
|
|
73
85
|
const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])
|
|
74
86
|
const groups = useMemo(() => groupRows(visible, config), [visible, config])
|
|
75
87
|
const subset = subsetSummary(config, scope, visible.length, rows.length)
|
|
76
|
-
const
|
|
77
|
-
const projects = useMemo(() => projectsOf(rows), [rows])
|
|
78
|
-
const gateways = useMemo(() => {
|
|
79
|
-
const seen = new Map<string, string>()
|
|
80
|
-
for (const row of rows) {
|
|
81
|
-
seen.set(row.hostId, row.hostName)
|
|
82
|
-
}
|
|
83
|
-
return [...seen].map(([id, name]) => ({ id, name }))
|
|
84
|
-
}, [rows])
|
|
88
|
+
const rowGateways = useMemo(() => new Set(rows.map((row) => row.hostId)).size, [rows])
|
|
85
89
|
|
|
86
90
|
const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
|
|
87
91
|
|
|
88
92
|
return (
|
|
89
93
|
<div data-slot="session-browser" className={cn('flex flex-col gap-3', className)}>
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
value={config.search}
|
|
95
|
-
onChange={(e) => set({ search: e.target.value })}
|
|
96
|
-
placeholder="Search sessions"
|
|
97
|
-
aria-label="Search sessions"
|
|
98
|
-
className="pl-8"
|
|
99
|
-
/>
|
|
100
|
-
</div>
|
|
101
|
-
<FilterRow label="State">
|
|
102
|
-
<FacetSelect
|
|
103
|
-
label="State"
|
|
104
|
-
value={config.states}
|
|
105
|
-
options={STATE_ORDER.map((s) => ({ value: s, label: STATE_LABELS[s] }))}
|
|
106
|
-
onChange={(states) => set({ states: states as SessionState[] })}
|
|
107
|
-
/>
|
|
108
|
-
</FilterRow>
|
|
109
|
-
{adapters.length > 1 ? (
|
|
110
|
-
<FilterRow label="Engine">
|
|
111
|
-
<FacetSelect
|
|
112
|
-
label="Engine"
|
|
113
|
-
value={config.adapters}
|
|
114
|
-
options={adapters.map((a) => ({ value: a, label: a }))}
|
|
115
|
-
onChange={(adapters) => set({ adapters })}
|
|
116
|
-
/>
|
|
117
|
-
</FilterRow>
|
|
118
|
-
) : null}
|
|
119
|
-
{gateways.length > 1 ? (
|
|
120
|
-
<FilterRow label="Gateway">
|
|
121
|
-
<FacetSelect
|
|
122
|
-
label="Gateway"
|
|
123
|
-
value={config.gateways}
|
|
124
|
-
options={gateways.map((g) => ({ value: g.id, label: g.name }))}
|
|
125
|
-
onChange={(gateways) => set({ gateways })}
|
|
126
|
-
/>
|
|
127
|
-
</FilterRow>
|
|
128
|
-
) : null}
|
|
129
|
-
{projects.length > 1 ? (
|
|
130
|
-
<FilterRow label="Project">
|
|
131
|
-
<FacetSelect
|
|
132
|
-
label="Project"
|
|
133
|
-
value={config.projects ?? []}
|
|
134
|
-
options={projects.map((p) => ({ value: p.key, label: p.label }))}
|
|
135
|
-
onChange={(next) => set({ projects: next })}
|
|
136
|
-
/>
|
|
137
|
-
</FilterRow>
|
|
138
|
-
) : null}
|
|
139
|
-
<FilterRow label="Group">
|
|
140
|
-
<OneOfSelect
|
|
141
|
-
label="Group"
|
|
142
|
-
value={config.groupBy}
|
|
143
|
-
options={[
|
|
144
|
-
{ value: 'none', label: 'No grouping' },
|
|
145
|
-
{ value: 'state', label: 'By state' },
|
|
146
|
-
{ value: 'adapter', label: 'By engine' },
|
|
147
|
-
...(projects.length > 1 ? [{ value: 'project' as const, label: 'By project' }] : []),
|
|
148
|
-
...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'By gateway' }] : []),
|
|
149
|
-
]}
|
|
150
|
-
onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
|
|
151
|
-
/>
|
|
152
|
-
</FilterRow>
|
|
153
|
-
<FilterRow label="Sub-agents">
|
|
154
|
-
<OneOfSelect
|
|
155
|
-
label="Sub-agents"
|
|
156
|
-
value={config.subagents}
|
|
157
|
-
options={[
|
|
158
|
-
{ value: 'active', label: 'Hide completed' },
|
|
159
|
-
{ value: 'all', label: 'Show all' },
|
|
160
|
-
{ value: 'none', label: 'Hide all' },
|
|
161
|
-
]}
|
|
162
|
-
onChange={(subagents) => set({ subagents: subagents as SubagentDisplay })}
|
|
163
|
-
/>
|
|
164
|
-
</FilterRow>
|
|
165
|
-
<FilterRow label="Sort">
|
|
166
|
-
<OneOfSelect
|
|
167
|
-
label="Sort"
|
|
168
|
-
value={config.sortBy}
|
|
169
|
-
options={[
|
|
170
|
-
{ value: 'recent', label: 'Recent' },
|
|
171
|
-
{ value: 'name', label: 'Name' },
|
|
172
|
-
{ value: 'state', label: 'State' },
|
|
173
|
-
...(projects.length > 1 ? [{ value: 'project' as const, label: 'Project' }] : []),
|
|
174
|
-
...(gateways.length > 1 ? [{ value: 'gateway' as const, label: 'Gateway' }] : []),
|
|
175
|
-
]}
|
|
176
|
-
onChange={(sortBy) => set({ sortBy: sortBy as SortBy })}
|
|
177
|
-
/>
|
|
178
|
-
</FilterRow>
|
|
179
|
-
</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}
|
|
180
98
|
|
|
181
|
-
{subset ? (
|
|
99
|
+
{subset && showSubset ? (
|
|
182
100
|
<div className="flex items-center gap-2 px-3 text-label text-fg-4">
|
|
183
101
|
<span>
|
|
184
102
|
{subset.shown} of {subset.total}
|
|
@@ -205,6 +123,14 @@ export function SessionBrowser({
|
|
|
205
123
|
action="Clear filters"
|
|
206
124
|
onAction={() => onConfigChange(clearFilters(config))}
|
|
207
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
|
+
/>
|
|
208
134
|
) : (
|
|
209
135
|
<Empty icon={<Layers />} title="Nothing here" description="No session to show." />
|
|
210
136
|
)
|
|
@@ -225,21 +151,27 @@ export function SessionBrowser({
|
|
|
225
151
|
<SessionRowItem
|
|
226
152
|
key={`${row.hostId}:${row.info.id}`}
|
|
227
153
|
row={row}
|
|
228
|
-
active={row
|
|
154
|
+
active={isActive(row)}
|
|
229
155
|
activeSubagentId={activeSubagentId}
|
|
230
156
|
activeShellId={activeShellId}
|
|
157
|
+
actions={rowActions?.(row)}
|
|
231
158
|
now={now}
|
|
232
|
-
showGateway={
|
|
159
|
+
showGateway={(gatewayCount ?? rowGateways) > 1 && config.groupBy !== 'gateway'}
|
|
233
160
|
showProject={config.groupBy !== 'project'}
|
|
234
161
|
subagents={config.subagents}
|
|
162
|
+
shells={config.shells}
|
|
163
|
+
tasks={config.tasks}
|
|
235
164
|
projectIcons={projectIcons}
|
|
236
165
|
onSelect={onSelect}
|
|
237
166
|
onDelete={onDelete}
|
|
238
167
|
onRename={onRename}
|
|
239
168
|
onClearContext={onClearContext}
|
|
240
169
|
onSelectSubagent={onSelectSubagent}
|
|
170
|
+
onSelectTask={onSelectTask}
|
|
171
|
+
onStopTask={onStopTask}
|
|
241
172
|
onSelectShell={onSelectShell}
|
|
242
173
|
onKillShell={onKillShell}
|
|
174
|
+
onShellAgentWrite={onShellAgentWrite}
|
|
243
175
|
/>
|
|
244
176
|
))}
|
|
245
177
|
</div>
|
|
@@ -258,39 +190,51 @@ function iconSrcOf(row: SessionRow | undefined, icons: Record<string, string> |
|
|
|
258
190
|
interface SessionRowItemProps {
|
|
259
191
|
row: SessionRow
|
|
260
192
|
active?: boolean
|
|
193
|
+
actions?: ReactNode
|
|
261
194
|
activeSubagentId?: string
|
|
262
195
|
activeShellId?: string
|
|
263
196
|
now?: number
|
|
264
197
|
showGateway?: boolean
|
|
265
198
|
showProject?: boolean
|
|
266
199
|
subagents?: SubagentDisplay
|
|
200
|
+
shells?: StepDisplay
|
|
201
|
+
tasks?: StepDisplay
|
|
267
202
|
projectIcons?: Record<string, string>
|
|
268
|
-
onSelect?: (row: SessionRow) => void
|
|
203
|
+
onSelect?: (row: SessionRow, modifiers: SelectModifiers) => void
|
|
269
204
|
onDelete?: (row: SessionRow) => void
|
|
270
205
|
onRename?: (row: SessionRow, title: string) => void
|
|
271
206
|
onClearContext?: (row: SessionRow) => void
|
|
272
207
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
208
|
+
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
209
|
+
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
273
210
|
onSelectShell?: (row: SessionRow, shellId: string) => void
|
|
274
211
|
onKillShell?: (row: SessionRow, shellId: string) => void
|
|
212
|
+
onShellAgentWrite?: (row: SessionRow, shellId: string, enabled: boolean) => void
|
|
275
213
|
}
|
|
276
214
|
|
|
277
215
|
function SessionRowItem({
|
|
278
216
|
row,
|
|
279
217
|
active,
|
|
218
|
+
actions,
|
|
280
219
|
activeSubagentId,
|
|
281
220
|
activeShellId,
|
|
282
221
|
now,
|
|
283
222
|
showGateway,
|
|
284
223
|
showProject = true,
|
|
285
224
|
subagents,
|
|
225
|
+
shells,
|
|
226
|
+
tasks,
|
|
286
227
|
projectIcons,
|
|
287
228
|
onSelect,
|
|
288
229
|
onDelete,
|
|
289
230
|
onRename,
|
|
290
231
|
onClearContext,
|
|
291
232
|
onSelectSubagent,
|
|
233
|
+
onSelectTask,
|
|
234
|
+
onStopTask,
|
|
292
235
|
onSelectShell,
|
|
293
236
|
onKillShell,
|
|
237
|
+
onShellAgentWrite,
|
|
294
238
|
}: SessionRowItemProps) {
|
|
295
239
|
const { info } = row
|
|
296
240
|
const [editing, setEditing] = useState(false)
|
|
@@ -299,42 +243,49 @@ function SessionRowItem({
|
|
|
299
243
|
<SessionItem
|
|
300
244
|
row={row}
|
|
301
245
|
active={active === true}
|
|
302
|
-
activeStepKey={activeShellId ?? activeSubagentId}
|
|
246
|
+
activeStepKey={active ? (activeShellId ?? activeSubagentId) : undefined}
|
|
303
247
|
now={now}
|
|
304
248
|
showGateway={showGateway}
|
|
305
249
|
showProject={showProject}
|
|
306
250
|
subagents={subagents}
|
|
251
|
+
shells={shells}
|
|
252
|
+
tasks={tasks}
|
|
307
253
|
projectIcons={projectIcons}
|
|
308
|
-
onSelect={() => onSelect?.(row)}
|
|
254
|
+
onSelect={(modifiers) => onSelect?.(row, modifiers)}
|
|
309
255
|
onSelectSubagent={onSelectSubagent ? (id) => onSelectSubagent(row, id) : undefined}
|
|
256
|
+
onSelectTask={onSelectTask ? (task) => onSelectTask(row, task) : undefined}
|
|
257
|
+
onStopTask={onStopTask ? (id) => onStopTask(row, id) : undefined}
|
|
310
258
|
onSelectShell={onSelectShell ? (id) => onSelectShell(row, id) : undefined}
|
|
311
259
|
onKillShell={onKillShell ? (id) => onKillShell(row, id) : undefined}
|
|
260
|
+
onShellAgentWrite={onShellAgentWrite ? (id, enabled) => onShellAgentWrite(row, id, enabled) : undefined}
|
|
312
261
|
onRename={onRename ? (title) => onRename(row, title) : undefined}
|
|
313
|
-
renameOn=
|
|
314
|
-
editing={editing}
|
|
315
|
-
onEditingChange={setEditing}
|
|
262
|
+
renameOn={actions === undefined ? 'external' : 'doubleClick'}
|
|
263
|
+
editing={actions === undefined ? editing : undefined}
|
|
264
|
+
onEditingChange={actions === undefined ? setEditing : undefined}
|
|
316
265
|
actions={
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
)
|
|
338
289
|
}
|
|
339
290
|
/>
|
|
340
291
|
)
|
|
@@ -358,80 +309,46 @@ function RowAction({ label, title, onClick, children }: { label: string; title?:
|
|
|
358
309
|
)
|
|
359
310
|
}
|
|
360
311
|
|
|
361
|
-
function
|
|
362
|
-
return (
|
|
363
|
-
<div className="flex items-center gap-2">
|
|
364
|
-
<span aria-hidden className="w-14 shrink-0 truncate text-label text-fg-3">
|
|
365
|
-
{label}
|
|
366
|
-
</span>
|
|
367
|
-
<div className="min-w-0 flex-1">{children}</div>
|
|
368
|
-
</div>
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function FacetSelect({
|
|
373
|
-
label,
|
|
312
|
+
export function SessionSearch({
|
|
374
313
|
value,
|
|
375
|
-
options,
|
|
376
|
-
onChange,
|
|
377
|
-
}: {
|
|
378
|
-
label: string
|
|
379
|
-
value: string[]
|
|
380
|
-
options: { value: string; label: string }[]
|
|
381
|
-
onChange: (value: string[]) => void
|
|
382
|
-
}) {
|
|
383
|
-
return (
|
|
384
|
-
<div className="flex items-center gap-1">
|
|
385
|
-
<Select multiple value={value} onValueChange={(v) => onChange(v as string[])}>
|
|
386
|
-
<SelectTrigger aria-label={label} className="min-w-0 flex-1">
|
|
387
|
-
<SelectValue>
|
|
388
|
-
{value.length === 0
|
|
389
|
-
? 'All'
|
|
390
|
-
: value.length === 1
|
|
391
|
-
? (options.find((o) => o.value === value[0])?.label ?? 'All')
|
|
392
|
-
: `${value.length} selected`}
|
|
393
|
-
</SelectValue>
|
|
394
|
-
</SelectTrigger>
|
|
395
|
-
<SelectContent>
|
|
396
|
-
{options.map((option) => (
|
|
397
|
-
<SelectItem key={option.value} value={option.value}>
|
|
398
|
-
<SelectItemText>{option.label}</SelectItemText>
|
|
399
|
-
</SelectItem>
|
|
400
|
-
))}
|
|
401
|
-
</SelectContent>
|
|
402
|
-
</Select>
|
|
403
|
-
{value.length > 0 ? (
|
|
404
|
-
<Button variant="ghost" size="icon-sm" aria-label={`Clear ${label}`} onClick={() => onChange([])}>
|
|
405
|
-
<X className="size-3 text-fg-4" />
|
|
406
|
-
</Button>
|
|
407
|
-
) : null}
|
|
408
|
-
</div>
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function OneOfSelect({
|
|
413
|
-
label,
|
|
414
|
-
value,
|
|
415
|
-
options,
|
|
416
314
|
onChange,
|
|
315
|
+
autoFocus,
|
|
316
|
+
className,
|
|
317
|
+
inputClassName,
|
|
417
318
|
}: {
|
|
418
|
-
label: string
|
|
419
319
|
value: string
|
|
420
|
-
options: readonly { value: string; label: string }[]
|
|
421
320
|
onChange: (value: string) => void
|
|
321
|
+
autoFocus?: boolean
|
|
322
|
+
className?: string
|
|
323
|
+
inputClassName?: string
|
|
422
324
|
}) {
|
|
423
325
|
return (
|
|
424
|
-
<
|
|
425
|
-
<
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
{
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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>
|
|
436
353
|
)
|
|
437
354
|
}
|