@workerdeck/ui 2.10.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 +39 -20
- package/build/index.mjs +119 -52
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +60 -11
- package/build/{use-path-links-CvcxdUXL.mjs → use-path-links-CepFsMdW.mjs} +541 -254
- 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 +10 -26
- package/src/components/agent/SessionPanel.tsx +12 -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/TranscriptItemView.tsx +2 -0
- package/src/components/agent/TranscriptRows.tsx +60 -56
- package/src/components/agent/UsageDialog.tsx +19 -6
- package/src/components/agent/composer-commands.ts +18 -0
- package/src/components/agent/pulse.tsx +53 -16
- package/src/components/agent/use-transcript-jumps.ts +25 -0
- package/src/components/terminal/TerminalTranscript.tsx +31 -27
- package/src/components/terminal/blocks.ts +15 -7
- package/src/components/terminal/height.ts +14 -5
- package/src/components/terminal/items.tsx +97 -11
- package/src/components/terminal/peer-names.tsx +16 -0
- package/src/components/terminal/row.tsx +1 -1
- package/src/components/terminal/tool-run.ts +68 -0
- package/src/index.ts +10 -2
- package/src/lib/session-cost.ts +12 -2
- package/src/lib/tool-icon.ts +5 -1
- package/src/styles/terminal.css +102 -1
- package/src/styles/theme.css +1 -1
- package/build/use-path-links-CvcxdUXL.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/
|
|
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
|
-
import { projectLabel, projectSubpath, sessionLabel } from '@workerdeck/protocol'
|
|
4
|
-
import type { SessionRow } from '@workerdeck/protocol'
|
|
3
|
+
import { projectLabel, projectName, projectSubpath, sessionLabel } 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) => {
|
|
@@ -68,6 +60,7 @@ export function SessionItem({
|
|
|
68
60
|
|
|
69
61
|
const engine = info.engine ?? 'claude'
|
|
70
62
|
const project = showProject ? projectLabel(row) : projectSubpath(row)
|
|
63
|
+
const projectTitle = showProject ? projectName(row) : undefined
|
|
71
64
|
const projectIcon = showProject ? info.project?.icon : undefined
|
|
72
65
|
const iconSrc = projectIcon?.type === 'image' ? projectIcons?.[projectIcon.hash] : undefined
|
|
73
66
|
const cost = formatCost(info.costUsd ?? info.totalCostUsd)
|
|
@@ -88,8 +81,8 @@ export function SessionItem({
|
|
|
88
81
|
}
|
|
89
82
|
if (project !== undefined) {
|
|
90
83
|
parts.push(
|
|
91
|
-
<span key="project">
|
|
92
|
-
<ProjectIcon icon={projectIcon} src={iconSrc} name={
|
|
84
|
+
<span key="project" title={projectTitle}>
|
|
85
|
+
<ProjectIcon icon={projectIcon} src={iconSrc} name={projectTitle} className="mr-1.5 size-4 align-[-0.3em]" />
|
|
93
86
|
{project}
|
|
94
87
|
</span>,
|
|
95
88
|
)
|
|
@@ -97,7 +90,7 @@ export function SessionItem({
|
|
|
97
90
|
for (const extra of extras) {
|
|
98
91
|
parts.push(<span key={extra}>{extra}</span>)
|
|
99
92
|
}
|
|
100
|
-
const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)))
|
|
93
|
+
const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)), subagents)
|
|
101
94
|
const holdsOpenAgent = steps.some((s) => s.key === activeStepKey)
|
|
102
95
|
|
|
103
96
|
return (
|
|
@@ -194,20 +187,11 @@ export function SessionItem({
|
|
|
194
187
|
{formatRelativeTime(info.lastActivityAt ?? info.createdAt)}
|
|
195
188
|
</span>
|
|
196
189
|
<span className="min-w-0 flex-1" />
|
|
197
|
-
{steps.length > 0 ? (
|
|
198
|
-
<StepToggle
|
|
199
|
-
expanded={open}
|
|
200
|
-
running={runningSteps(steps)}
|
|
201
|
-
total={steps.length}
|
|
202
|
-
noun={steps[0]!.noun}
|
|
203
|
-
onToggle={() => setOpen(!open)}
|
|
204
|
-
/>
|
|
205
|
-
) : null}
|
|
206
190
|
{actions}
|
|
207
191
|
</div>
|
|
208
192
|
</div>
|
|
209
193
|
|
|
210
|
-
{
|
|
194
|
+
{steps.length > 0 ? (
|
|
211
195
|
<div className="flex flex-col">
|
|
212
196
|
{steps.map((step: Step) => (
|
|
213
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,
|
|
@@ -264,6 +269,7 @@ export function SessionPanel({
|
|
|
264
269
|
replaying,
|
|
265
270
|
protocolMismatch,
|
|
266
271
|
shell,
|
|
272
|
+
pricing,
|
|
267
273
|
models,
|
|
268
274
|
effectiveModel,
|
|
269
275
|
handle,
|
|
@@ -498,6 +504,9 @@ export function SessionPanel({
|
|
|
498
504
|
engine: state.engine,
|
|
499
505
|
})
|
|
500
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)
|
|
501
510
|
const draft = useDraft(client, sessionId)
|
|
502
511
|
// Stable identity: an inline arrow here would bust the Composer's `triggers`
|
|
503
512
|
// memo on every streaming re-render, and with it every prompt-area callback
|
|
@@ -698,6 +707,7 @@ export function SessionPanel({
|
|
|
698
707
|
fileUrl={sessionId ? (path) => client.sessionFileUrl(sessionId, path) : undefined}
|
|
699
708
|
attachmentUrl={sessionId ? (id) => client.attachmentUrl(sessionId, id) : undefined}
|
|
700
709
|
canBrowseFiles={hostFiles.available}
|
|
710
|
+
sessionNames={peers.names}
|
|
701
711
|
hostImage={hostImage}
|
|
702
712
|
variant={transcriptVariant}
|
|
703
713
|
density={transcriptDensity}
|
|
@@ -802,6 +812,7 @@ export function SessionPanel({
|
|
|
802
812
|
attachments={attachments}
|
|
803
813
|
draft={draft}
|
|
804
814
|
onSearchFiles={hostFiles.available ? searchComposerFiles : undefined}
|
|
815
|
+
peers={peers.peers}
|
|
805
816
|
onShellCommand={shell ? runShell : undefined}
|
|
806
817
|
layout={controlsExternal ? 'inline' : 'stacked'}
|
|
807
818
|
toolbar={controlsExternal ? undefined : sessionControls}
|
|
@@ -835,6 +846,7 @@ export function SessionPanel({
|
|
|
835
846
|
totalCostUsd={state.totalCostUsd}
|
|
836
847
|
costUsd={state.costUsd}
|
|
837
848
|
usageByModel={state.usageByModel}
|
|
849
|
+
pricing={pricing}
|
|
838
850
|
spend={profileSpend}
|
|
839
851
|
updatedAt={usageUpdatedAt}
|
|
840
852
|
open={panel === 'usage'}
|
|
@@ -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
|
+
}
|