@workerdeck/ui 2.11.0 → 2.12.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/{SessionPanel-BVbau2eR.d.mts → SessionPanel-CJwp5B3q.d.mts} +8 -3
- package/build/index.d.mts +36 -18
- package/build/index.mjs +109 -46
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +4 -10
- package/build/{use-path-links-DO_79-wX.mjs → use-path-links-CepFsMdW.mjs} +137 -79
- package/build/use-path-links-CepFsMdW.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/Composer.tsx +28 -5
- package/src/components/agent/PromptTokenText.tsx +9 -1
- package/src/components/agent/SessionBrowser.tsx +17 -1
- package/src/components/agent/SessionItem.tsx +6 -23
- package/src/components/agent/SessionPanel.tsx +10 -0
- package/src/components/agent/SessionSteps.tsx +15 -55
- package/src/components/agent/SubagentToggle.tsx +69 -0
- package/src/components/agent/Transcript.tsx +91 -80
- package/src/components/agent/composer-commands.ts +18 -0
- package/src/index.ts +10 -2
- package/src/styles/terminal.css +9 -1
- package/build/use-path-links-DO_79-wX.mjs.map +0 -1
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as SessionPanelProps } from "./SessionPanel-
|
|
1
|
+
import { i as SessionPanelProps } from "./SessionPanel-CJwp5B3q.mjs";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
3
|
import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
|
|
4
4
|
import { WorkerDeckClient } from "@workerdeck/client";
|
package/build/workspace.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as EntryIcon, It as Tip, Nt as Spinner, Rt as TooltipProvider, an as Input, cn as Button, i as SessionPanel, jt as Splitter, n as usePathLinks, t as resolveAgainstCwd, un as cn } from "./use-path-links-CepFsMdW.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": "2.
|
|
3
|
+
"version": "2.12.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/react": "2.
|
|
42
|
+
"@workerdeck/protocol": "2.12.0",
|
|
43
|
+
"@workerdeck/client": "2.12.0",
|
|
44
|
+
"@workerdeck/react": "2.12.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"monaco-editor": "^0.56.0",
|
|
@@ -10,15 +10,15 @@ import {
|
|
|
10
10
|
type Ref,
|
|
11
11
|
} from 'react'
|
|
12
12
|
import type { SkillInfo, SlashCommandInfo } from '@workerdeck/protocol'
|
|
13
|
-
import type { StagedAttachment, UseAttachmentsResult } from '@workerdeck/react'
|
|
14
|
-
import { ArrowUp, FileText, Paperclip, RotateCw, SlidersHorizontal, Sparkles, Square, TriangleAlert, X } from 'lucide-react'
|
|
13
|
+
import type { PeerSessionOption, StagedAttachment, UseAttachmentsResult } from '@workerdeck/react'
|
|
14
|
+
import { ArrowUp, FileText, Paperclip, RotateCw, SlidersHorizontal, Sparkles, Square, TriangleAlert, Users, X } from 'lucide-react'
|
|
15
15
|
import { Button } from '../ui/Button.tsx'
|
|
16
16
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
17
17
|
import { PromptArea } from '../prompt-area/prompt-area.tsx'
|
|
18
18
|
import { usePromptAreaState } from '../prompt-area/use-prompt-area-state.ts'
|
|
19
19
|
import { plainTextToSegments } from '../prompt-area/prompt-area-engine.ts'
|
|
20
|
-
import { commandTrigger, launchTrigger, mentionTrigger } from '../prompt-area/trigger-presets.ts'
|
|
21
|
-
import { mergeComposerRows, rankComposerRows, skillTrailingText, type ClientCommand } from './composer-commands.ts'
|
|
20
|
+
import { commandTrigger, hashtagTrigger, launchTrigger, mentionTrigger } from '../prompt-area/trigger-presets.ts'
|
|
21
|
+
import { mergeComposerRows, rankComposerRows, rankPeerSessions, skillTrailingText, type ClientCommand } from './composer-commands.ts'
|
|
22
22
|
import { useTranscriptVariant } from './transcript-variant.tsx'
|
|
23
23
|
import type { TerminalAffordances } from '../terminal/affordances.tsx'
|
|
24
24
|
import { PROMPT_GLYPH } from '../terminal/items.tsx'
|
|
@@ -49,6 +49,9 @@ export interface ComposerProps {
|
|
|
49
49
|
// commands, and suppressed name-for-name by an engine command so the real one always wins.
|
|
50
50
|
clientCommands?: ClientCommand[]
|
|
51
51
|
onSearchFiles?: (query: string, options: { signal: AbortSignal }) => Promise<ComposerFileMatch[]>
|
|
52
|
+
// The other sessions on this gateway, offered on `#`. The token is a hint the gateway resolves at
|
|
53
|
+
// send: it never messages the named session, and an unresolvable name stays ordinary text.
|
|
54
|
+
peers?: readonly PeerSessionOption[]
|
|
52
55
|
// Shell mode: `!` as the first character turns the composer into a host shell prompt. Omit to leave the
|
|
53
56
|
// mode off entirely - the gateway only offers it to an operator on a session whose engine reaches a host cwd.
|
|
54
57
|
onShellCommand?: (command: string) => void
|
|
@@ -75,6 +78,7 @@ export function Composer({
|
|
|
75
78
|
skills,
|
|
76
79
|
clientCommands,
|
|
77
80
|
onSearchFiles,
|
|
81
|
+
peers,
|
|
78
82
|
onShellCommand,
|
|
79
83
|
attachments,
|
|
80
84
|
toolbar,
|
|
@@ -172,6 +176,24 @@ export function Composer({
|
|
|
172
176
|
}),
|
|
173
177
|
)
|
|
174
178
|
}
|
|
179
|
+
if (peers?.length) {
|
|
180
|
+
configured.push(
|
|
181
|
+
hashtagTrigger({
|
|
182
|
+
accessibilityLabel: 'session',
|
|
183
|
+
onSearch: (query: string): TriggerSuggestion[] =>
|
|
184
|
+
rankPeerSessions(query, peers).map((peer) => ({
|
|
185
|
+
value: peer.slug,
|
|
186
|
+
label: peer.label,
|
|
187
|
+
description: [peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
188
|
+
icon: <Users className="size-3.5 text-fg-3" />,
|
|
189
|
+
})),
|
|
190
|
+
onSelect: (suggestion) => suggestion.value,
|
|
191
|
+
chipStyle: 'inline',
|
|
192
|
+
chipClassName: 'font-mono',
|
|
193
|
+
emptyMessage: 'No matching sessions',
|
|
194
|
+
}),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
175
197
|
if (onShellCommand) {
|
|
176
198
|
configured.push(
|
|
177
199
|
launchTrigger({
|
|
@@ -182,7 +204,7 @@ export function Composer({
|
|
|
182
204
|
)
|
|
183
205
|
}
|
|
184
206
|
return configured.length > 0 ? configured : undefined
|
|
185
|
-
}, [commands, skills, clientCommands, onSearchFiles, onShellCommand])
|
|
207
|
+
}, [commands, skills, clientCommands, onSearchFiles, peers, onShellCommand])
|
|
186
208
|
|
|
187
209
|
const saveDraft = draft?.save
|
|
188
210
|
useEffect(() => {
|
|
@@ -223,6 +245,7 @@ export function Composer({
|
|
|
223
245
|
const hints = [
|
|
224
246
|
triggers?.some((t) => t.char === '/') ? { key: '/', what: 'commands and skills' } : undefined,
|
|
225
247
|
onSearchFiles ? { key: '@', what: 'mention a file' } : undefined,
|
|
248
|
+
peers?.length ? { key: '#', what: 'refer to another session' } : undefined,
|
|
226
249
|
onShellCommand ? { key: '!', what: 'run a shell command' } : undefined,
|
|
227
250
|
{ key: '?', what: 'this list, on an empty composer' },
|
|
228
251
|
].filter((h) => h !== undefined)
|
|
@@ -4,14 +4,22 @@ import { cn } from '../../lib/utils.ts'
|
|
|
4
4
|
|
|
5
5
|
const SkillNamesContext = createContext<readonly string[] | undefined>(undefined)
|
|
6
6
|
|
|
7
|
+
const SessionNamesContext = createContext<readonly string[] | undefined>(undefined)
|
|
8
|
+
|
|
7
9
|
// The skill names a `$name` in a sent message may resolve to; unset, nothing is a skill token.
|
|
8
10
|
export function SkillNamesProvider({ names, children }: { names: readonly string[] | undefined; children: ReactNode }) {
|
|
9
11
|
return <SkillNamesContext.Provider value={names}>{children}</SkillNamesContext.Provider>
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
// The folded peer slugs a `#Name` in a sent message may resolve to; unset, nothing is a session token.
|
|
15
|
+
export function SessionNamesProvider({ names, children }: { names: readonly string[] | undefined; children: ReactNode }) {
|
|
16
|
+
return <SessionNamesContext.Provider value={names}>{children}</SessionNamesContext.Provider>
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export function PromptTokenText({ text, className }: { text: string; className?: string }) {
|
|
13
20
|
const skills = useContext(SkillNamesContext)
|
|
14
|
-
const
|
|
21
|
+
const sessions = useContext(SessionNamesContext)
|
|
22
|
+
const tokens = scanPromptTokens(text, { skills, sessions })
|
|
15
23
|
if (tokens.length === 0) {
|
|
16
24
|
return <span className={className}>{text}</span>
|
|
17
25
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
projectsOf,
|
|
13
13
|
subsetSummary,
|
|
14
14
|
} from '@workerdeck/protocol'
|
|
15
|
-
import type { GroupBy, SessionRow, SessionState, SortBy, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
15
|
+
import type { GroupBy, SessionRow, SessionState, SortBy, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
16
16
|
import { Button } from '../ui/Button.tsx'
|
|
17
17
|
import { Empty } from '../ui/Empty.tsx'
|
|
18
18
|
import { Input } from '../ui/Input.tsx'
|
|
@@ -142,6 +142,18 @@ export function SessionBrowser({
|
|
|
142
142
|
onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
|
|
143
143
|
/>
|
|
144
144
|
</FilterRow>
|
|
145
|
+
<FilterRow label="Sub-agents">
|
|
146
|
+
<OneOfSelect
|
|
147
|
+
label="Sub-agents"
|
|
148
|
+
value={config.subagents}
|
|
149
|
+
options={[
|
|
150
|
+
{ value: 'active', label: 'Hide completed' },
|
|
151
|
+
{ value: 'all', label: 'Show all' },
|
|
152
|
+
{ value: 'none', label: 'Hide all' },
|
|
153
|
+
]}
|
|
154
|
+
onChange={(subagents) => set({ subagents: subagents as SubagentDisplay })}
|
|
155
|
+
/>
|
|
156
|
+
</FilterRow>
|
|
145
157
|
<FilterRow label="Sort">
|
|
146
158
|
<OneOfSelect
|
|
147
159
|
label="Sort"
|
|
@@ -209,6 +221,7 @@ export function SessionBrowser({
|
|
|
209
221
|
activeSubagentId={activeSubagentId}
|
|
210
222
|
showGateway={gateways.length > 1 && config.groupBy !== 'gateway'}
|
|
211
223
|
showProject={config.groupBy !== 'project'}
|
|
224
|
+
subagents={config.subagents}
|
|
212
225
|
projectIcons={projectIcons}
|
|
213
226
|
onSelect={onSelect}
|
|
214
227
|
onDelete={onDelete}
|
|
@@ -236,6 +249,7 @@ interface SessionRowItemProps {
|
|
|
236
249
|
activeSubagentId?: string
|
|
237
250
|
showGateway?: boolean
|
|
238
251
|
showProject?: boolean
|
|
252
|
+
subagents?: SubagentDisplay
|
|
239
253
|
projectIcons?: Record<string, string>
|
|
240
254
|
onSelect?: (row: SessionRow) => void
|
|
241
255
|
onDelete?: (row: SessionRow) => void
|
|
@@ -250,6 +264,7 @@ function SessionRowItem({
|
|
|
250
264
|
activeSubagentId,
|
|
251
265
|
showGateway,
|
|
252
266
|
showProject = true,
|
|
267
|
+
subagents,
|
|
253
268
|
projectIcons,
|
|
254
269
|
onSelect,
|
|
255
270
|
onDelete,
|
|
@@ -267,6 +282,7 @@ function SessionRowItem({
|
|
|
267
282
|
activeStepKey={activeSubagentId}
|
|
268
283
|
showGateway={showGateway}
|
|
269
284
|
showProject={showProject}
|
|
285
|
+
subagents={subagents}
|
|
270
286
|
projectIcons={projectIcons}
|
|
271
287
|
onSelect={() => onSelect?.(row)}
|
|
272
288
|
onSelectSubagent={onSelectSubagent ? (id) => onSelectSubagent(row, id) : undefined}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Fragment, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
import { projectLabel, projectName, projectSubpath, sessionLabel } from '@workerdeck/protocol'
|
|
4
|
-
import type { SessionRow } from '@workerdeck/protocol'
|
|
4
|
+
import type { SessionRow, SubagentDisplay } from '@workerdeck/protocol'
|
|
5
5
|
import { ContextRing } from './ContextRing.tsx'
|
|
6
6
|
import { EngineIcon, vendorMarkClass, vendorTextClass } from './EngineIcon.tsx'
|
|
7
7
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
8
8
|
import { SessionStatusIcon } from './SessionStatusIcon.tsx'
|
|
9
|
-
import { StepRow,
|
|
9
|
+
import { StepRow, sessionSteps } from './SessionSteps.tsx'
|
|
10
10
|
import type { Step } from './SessionSteps.tsx'
|
|
11
11
|
import { cn } from '../../lib/utils.ts'
|
|
12
12
|
import { formatCost, formatRelativeTime, friendlyModel } from '../../lib/format.ts'
|
|
@@ -20,8 +20,7 @@ export interface SessionItemProps {
|
|
|
20
20
|
showGateway?: boolean
|
|
21
21
|
showProject?: boolean
|
|
22
22
|
projectIcons?: Record<string, string>
|
|
23
|
-
|
|
24
|
-
onExpandedChange?: (expanded: boolean) => void
|
|
23
|
+
subagents?: SubagentDisplay
|
|
25
24
|
onSelect?: (modifiers: SelectModifiers) => void
|
|
26
25
|
onSelectSubagent?: (toolUseId: string) => void
|
|
27
26
|
onRename?: (title: string) => void
|
|
@@ -41,8 +40,7 @@ export function SessionItem({
|
|
|
41
40
|
showGateway,
|
|
42
41
|
showProject = true,
|
|
43
42
|
projectIcons,
|
|
44
|
-
|
|
45
|
-
onExpandedChange,
|
|
43
|
+
subagents = 'active',
|
|
46
44
|
onSelect,
|
|
47
45
|
onSelectSubagent,
|
|
48
46
|
onRename,
|
|
@@ -53,12 +51,6 @@ export function SessionItem({
|
|
|
53
51
|
className,
|
|
54
52
|
}: SessionItemProps) {
|
|
55
53
|
const { info } = row
|
|
56
|
-
const [ownExpanded, setOwnExpanded] = useState(false)
|
|
57
|
-
const open = expanded ?? ownExpanded
|
|
58
|
-
const setOpen = (next: boolean) => {
|
|
59
|
-
setOwnExpanded(next)
|
|
60
|
-
onExpandedChange?.(next)
|
|
61
|
-
}
|
|
62
54
|
const [ownEditing, setOwnEditing] = useState(false)
|
|
63
55
|
const isEditing = editing ?? ownEditing
|
|
64
56
|
const setEditing = (next: boolean) => {
|
|
@@ -98,7 +90,7 @@ export function SessionItem({
|
|
|
98
90
|
for (const extra of extras) {
|
|
99
91
|
parts.push(<span key={extra}>{extra}</span>)
|
|
100
92
|
}
|
|
101
|
-
const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)))
|
|
93
|
+
const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)), subagents)
|
|
102
94
|
const holdsOpenAgent = steps.some((s) => s.key === activeStepKey)
|
|
103
95
|
|
|
104
96
|
return (
|
|
@@ -195,20 +187,11 @@ export function SessionItem({
|
|
|
195
187
|
{formatRelativeTime(info.lastActivityAt ?? info.createdAt)}
|
|
196
188
|
</span>
|
|
197
189
|
<span className="min-w-0 flex-1" />
|
|
198
|
-
{steps.length > 0 ? (
|
|
199
|
-
<StepToggle
|
|
200
|
-
expanded={open}
|
|
201
|
-
running={runningSteps(steps)}
|
|
202
|
-
total={steps.length}
|
|
203
|
-
noun={steps[0]!.noun}
|
|
204
|
-
onToggle={() => setOpen(!open)}
|
|
205
|
-
/>
|
|
206
|
-
) : null}
|
|
207
190
|
{actions}
|
|
208
191
|
</div>
|
|
209
192
|
</div>
|
|
210
193
|
|
|
211
|
-
{
|
|
194
|
+
{steps.length > 0 ? (
|
|
212
195
|
<div className="flex flex-col">
|
|
213
196
|
{steps.map((step: Step) => (
|
|
214
197
|
<StepRow key={step.key} step={step} active={step.key === activeStepKey} onSelect={step.onSelect} />
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
useClaudeSession,
|
|
29
29
|
useDraft,
|
|
30
30
|
useHostFileSearch,
|
|
31
|
+
usePeerSessions,
|
|
31
32
|
useProfileUsage,
|
|
32
33
|
useToolCallHost,
|
|
33
34
|
type ClientToolHandler,
|
|
@@ -133,6 +134,9 @@ export interface SessionPanelProps {
|
|
|
133
134
|
scrubber?: boolean
|
|
134
135
|
// Bookmarked transcript item ids - the host owns membership and persistence.
|
|
135
136
|
bookmarks?: readonly string[]
|
|
137
|
+
// `#` mentions of the other sessions on this gateway. On by default; an embedder whose people
|
|
138
|
+
// should not learn that the other sessions exist turns it off.
|
|
139
|
+
peerMentions?: boolean
|
|
136
140
|
onToggleBookmark?: (itemId: string) => void
|
|
137
141
|
// Where a file link in the transcript goes. Without it, such a link stays an ordinary anchor.
|
|
138
142
|
onOpenFile?: FileLinkOpener
|
|
@@ -226,6 +230,7 @@ export function SessionPanel({
|
|
|
226
230
|
terminalMetrics,
|
|
227
231
|
scrubber = false,
|
|
228
232
|
bookmarks,
|
|
233
|
+
peerMentions = true,
|
|
229
234
|
onToggleBookmark,
|
|
230
235
|
reveal,
|
|
231
236
|
subagents,
|
|
@@ -499,6 +504,9 @@ export function SessionPanel({
|
|
|
499
504
|
engine: state.engine,
|
|
500
505
|
})
|
|
501
506
|
const hostFiles = useHostFileSearch(client, state.cwd)
|
|
507
|
+
// Read here rather than taken as a prop, the way file search is: every embedding that has a
|
|
508
|
+
// gateway has the sessions list, and a host that has to wire it would be a host that forgets to.
|
|
509
|
+
const peers = usePeerSessions(client, sessionId, peerMentions)
|
|
502
510
|
const draft = useDraft(client, sessionId)
|
|
503
511
|
// Stable identity: an inline arrow here would bust the Composer's `triggers`
|
|
504
512
|
// memo on every streaming re-render, and with it every prompt-area callback
|
|
@@ -699,6 +707,7 @@ export function SessionPanel({
|
|
|
699
707
|
fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
|
|
700
708
|
attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
|
|
701
709
|
canBrowseFiles={hostFiles.available}
|
|
710
|
+
sessionNames={peers.names}
|
|
702
711
|
hostImage={hostImage}
|
|
703
712
|
variant={transcriptVariant}
|
|
704
713
|
density={transcriptDensity}
|
|
@@ -803,6 +812,7 @@ export function SessionPanel({
|
|
|
803
812
|
attachments={attachments}
|
|
804
813
|
draft={draft}
|
|
805
814
|
onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
|
|
815
|
+
peers={peers.peers}
|
|
806
816
|
onShellCommand={shell ? runShell : undefined}
|
|
807
817
|
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
808
818
|
toolbar={controlsExternal ? undefined : sessionControls}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ArrowRight, Check,
|
|
2
|
-
import { isAgentRecord, subagentLabel } from '@workerdeck/protocol'
|
|
3
|
-
import type { SessionInfo, SubagentInfo } from '@workerdeck/protocol'
|
|
1
|
+
import { ArrowRight, Check, CircleAlert } from 'lucide-react'
|
|
2
|
+
import { isAgentRecord, subagentLabel, visibleSubagents } from '@workerdeck/protocol'
|
|
3
|
+
import type { SessionInfo, SubagentDisplay, SubagentInfo } from '@workerdeck/protocol'
|
|
4
4
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
5
5
|
import { cn } from '../../lib/utils.ts'
|
|
6
6
|
|
|
@@ -14,16 +14,18 @@ export type Step = {
|
|
|
14
14
|
onSelect: () => void
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function sessionSteps(info: SessionInfo, onSelect: (toolUseId: string) => void): Step[] {
|
|
18
|
-
return (info
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
export function sessionSteps(info: SessionInfo, onSelect: (toolUseId: string) => void, show: SubagentDisplay = 'all'): Step[] {
|
|
18
|
+
return visibleSubagents(info, show)
|
|
19
|
+
.filter(isAgentRecord)
|
|
20
|
+
.map((sub) => ({
|
|
21
|
+
key: sub.toolUseId,
|
|
22
|
+
label: subagentLabel(sub),
|
|
23
|
+
noun: 'agent',
|
|
24
|
+
state: stepState(sub.status),
|
|
25
|
+
detail: sub.toolCount > 0 ? String(sub.toolCount) : undefined,
|
|
26
|
+
title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,
|
|
27
|
+
onSelect: () => onSelect(sub.toolUseId),
|
|
28
|
+
}))
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
function stepState(status: SubagentInfo['status']): Step['state'] {
|
|
@@ -40,48 +42,6 @@ function stepState(status: SubagentInfo['status']): Step['state'] {
|
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
export function runningSteps(steps: readonly Step[]): number {
|
|
44
|
-
return steps.filter((s) => s.state === 'running').length
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function StepToggle({
|
|
48
|
-
expanded,
|
|
49
|
-
running,
|
|
50
|
-
total,
|
|
51
|
-
noun,
|
|
52
|
-
onToggle,
|
|
53
|
-
}: {
|
|
54
|
-
expanded: boolean
|
|
55
|
-
running: number
|
|
56
|
-
total: number
|
|
57
|
-
noun: string
|
|
58
|
-
onToggle: () => void
|
|
59
|
-
}) {
|
|
60
|
-
const label = running > 0 && running < total ? `${running}/${total}` : String(total)
|
|
61
|
-
const words = running > 0 && running < total ? `${running} of ${total} ${noun}s running` : `${total} ${noun}${total === 1 ? '' : 's'}`
|
|
62
|
-
const Chevron = expanded ? ChevronDown : ChevronRight
|
|
63
|
-
return (
|
|
64
|
-
<button
|
|
65
|
-
type="button"
|
|
66
|
-
aria-expanded={expanded}
|
|
67
|
-
aria-label={`${expanded ? 'Hide' : 'Show'} ${words}`}
|
|
68
|
-
title={`${expanded ? 'Hide' : 'Show'} ${words}`}
|
|
69
|
-
onClick={(e) => {
|
|
70
|
-
e.stopPropagation()
|
|
71
|
-
onToggle()
|
|
72
|
-
}}
|
|
73
|
-
className={cn(
|
|
74
|
-
'flex shrink-0 items-center gap-0.5 rounded-[4px] py-0.5 pr-1 pl-0.5 outline-none',
|
|
75
|
-
'text-[0.75rem] leading-3 hover:bg-row-hover hover:text-fg-2',
|
|
76
|
-
running > 0 ? 'text-info' : 'text-fg-4',
|
|
77
|
-
)}
|
|
78
|
-
>
|
|
79
|
-
<Chevron className="size-3" />
|
|
80
|
-
<span className="tabular-nums">{label}</span>
|
|
81
|
-
</button>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
45
|
export function StepRow({ step, active = false, onSelect }: { step: Step; active?: boolean; onSelect: () => void }) {
|
|
86
46
|
const body = step.state === 'failed' ? 'text-danger' : 'text-success'
|
|
87
47
|
return (
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Check, ChevronDown } from 'lucide-react'
|
|
2
|
+
import type { SubagentDisplay } from '@workerdeck/protocol'
|
|
3
|
+
import { Button } from '../ui/Button.tsx'
|
|
4
|
+
import { Menu, MenuContent, MenuItem, MenuTrigger } from '../ui/Menu.tsx'
|
|
5
|
+
import { cn } from '../../lib/utils.ts'
|
|
6
|
+
|
|
7
|
+
export const SUBAGENT_DISPLAY_ORDER: readonly SubagentDisplay[] = ['all', 'active', 'none']
|
|
8
|
+
|
|
9
|
+
export const SUBAGENT_DISPLAY_META: Record<SubagentDisplay, { label: string; hint: string }> = {
|
|
10
|
+
all: { label: 'All sub-agents', hint: 'Showing every sub-agent' },
|
|
11
|
+
active: { label: 'Hide completed', hint: 'Showing running and failed sub-agents' },
|
|
12
|
+
none: { label: 'Hide sub-agents', hint: 'Sub-agents hidden' },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HIDDEN_ROW = 0.34
|
|
16
|
+
|
|
17
|
+
// A session line at full strength over two child rows, and a hidden child is greyed rather than
|
|
18
|
+
// struck: a sub-agent the card is not drawing has not failed, and a cross would say it had.
|
|
19
|
+
export function SubagentDisplayIcon({ value, className }: { value: SubagentDisplay; className?: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
strokeWidth={2}
|
|
26
|
+
strokeLinecap="round"
|
|
27
|
+
strokeLinejoin="round"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
className={className}
|
|
30
|
+
>
|
|
31
|
+
<path d="M3 5h18" />
|
|
32
|
+
<path d="M9 12h12" opacity={value === 'none' ? HIDDEN_ROW : 1} />
|
|
33
|
+
<path d="M9 19h12" opacity={value === 'all' ? 1 : HIDDEN_ROW} />
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SubagentToggleProps = {
|
|
39
|
+
value: SubagentDisplay
|
|
40
|
+
onChange: (value: SubagentDisplay) => void
|
|
41
|
+
className?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A menu rather than a press-to-cycle button: three stops are one more than a person can read off
|
|
45
|
+
// a single glyph, and the caret is what promises the other two.
|
|
46
|
+
export function SubagentToggle({ value, onChange, className }: SubagentToggleProps) {
|
|
47
|
+
const meta = SUBAGENT_DISPLAY_META[value]
|
|
48
|
+
return (
|
|
49
|
+
<Menu>
|
|
50
|
+
<MenuTrigger
|
|
51
|
+
render={
|
|
52
|
+
<Button variant="ghost" size="icon-sm" aria-label={`Sub-agents: ${meta.label}`} title={meta.hint} className={cn('w-9 gap-0.5', className)}>
|
|
53
|
+
<SubagentDisplayIcon value={value} className="size-4" />
|
|
54
|
+
<ChevronDown className="size-2.5 text-fg-4" />
|
|
55
|
+
</Button>
|
|
56
|
+
}
|
|
57
|
+
/>
|
|
58
|
+
<MenuContent className="min-w-44">
|
|
59
|
+
{SUBAGENT_DISPLAY_ORDER.map((option) => (
|
|
60
|
+
<MenuItem key={option} onClick={() => onChange(option)}>
|
|
61
|
+
<SubagentDisplayIcon value={option} className="size-4 shrink-0 text-fg-3" />
|
|
62
|
+
<span className="flex-1">{SUBAGENT_DISPLAY_META[option].label}</span>
|
|
63
|
+
{option === value ? <Check className="size-3.5 shrink-0 text-accent" /> : null}
|
|
64
|
+
</MenuItem>
|
|
65
|
+
))}
|
|
66
|
+
</MenuContent>
|
|
67
|
+
</Menu>
|
|
68
|
+
)
|
|
69
|
+
}
|