@workerdeck/ui 3.3.0 → 3.5.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-BNNc8grK.d.mts → SessionPanel-DIi4sEgh.d.mts} +5 -2
- package/build/index.d.mts +71 -6
- package/build/index.mjs +613 -159
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +104 -3
- package/build/{use-path-links-Cby4i_VL.mjs → use-path-links-WCsysQPy.mjs} +332 -79
- package/build/use-path-links-WCsysQPy.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/AgentAvatar.tsx +117 -0
- package/src/components/agent/Composer.tsx +29 -6
- package/src/components/agent/EffortSelect.tsx +64 -0
- package/src/components/agent/ModelSelect.tsx +1 -1
- package/src/components/agent/SessionBrowser.tsx +267 -42
- package/src/components/agent/SessionGroups.tsx +131 -21
- package/src/components/agent/SessionItem.tsx +182 -69
- package/src/components/agent/SessionPanel.tsx +75 -28
- package/src/components/agent/TranscriptItemView.tsx +20 -2
- package/src/components/agent/composer-commands.ts +24 -0
- package/src/components/terminal/affordances.tsx +29 -1
- package/src/components/terminal/height.ts +12 -2
- package/src/components/terminal/items.tsx +33 -1
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/index.ts +12 -0
- package/src/lib/team-drop.ts +65 -0
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +20 -1
- package/build/use-path-links-Cby4i_VL.mjs.map +0 -1
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SessionPanelProps } from "./SessionPanel-
|
|
1
|
+
import { r as SessionPanelProps } from "./SessionPanel-DIi4sEgh.mjs";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
3
|
import { ShellInfo } from "@workerdeck/protocol";
|
|
4
4
|
import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
|
package/build/workspace.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $t as Splitter, N as ShellTerminal, Nn as cn, On as Input, Y as EntryIcon, i as SessionPanel, in as Tip, jn as Button, n as usePathLinks, on as TooltipProvider, t as resolveAgainstCwd, tn as Spinner } from "./use-path-links-WCsysQPy.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.5.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/protocol": "3.
|
|
43
|
-
"@workerdeck/
|
|
44
|
-
"@workerdeck/
|
|
42
|
+
"@workerdeck/protocol": "3.5.0",
|
|
43
|
+
"@workerdeck/client": "3.5.0",
|
|
44
|
+
"@workerdeck/react": "3.5.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"monaco-editor": "^0.56.0",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
import type { AgentRef, SessionRow, SessionState } from '@workerdeck/protocol'
|
|
3
|
+
import { EngineIcon, vendorMarkClass } from './EngineIcon.tsx'
|
|
4
|
+
import { SessionStatusIcon } from './SessionStatusIcon.tsx'
|
|
5
|
+
import { cn } from '../../lib/utils.ts'
|
|
6
|
+
|
|
7
|
+
export type AgentAvatarImage = { still: string; busy?: { src: string; durations: number[] } }
|
|
8
|
+
|
|
9
|
+
// Keyed by `AgentRef.avatar`, the gateway path: the host fetches with its own credentials and hands over data URLs.
|
|
10
|
+
export type AgentAvatars = Record<string, AgentAvatarImage>
|
|
11
|
+
|
|
12
|
+
export function avatarOf(avatars: AgentAvatars | undefined, agent: AgentRef | undefined): AgentAvatarImage | undefined {
|
|
13
|
+
return agent?.avatar === undefined ? undefined : avatars?.[agent.avatar]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AgentAvatarProps {
|
|
17
|
+
row: SessionRow
|
|
18
|
+
image?: AgentAvatarImage
|
|
19
|
+
size?: number
|
|
20
|
+
// The state the corner badge draws; a folded lead passes its team's most urgent one.
|
|
21
|
+
state?: SessionState
|
|
22
|
+
badge?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AgentAvatar({ row, image, size = 32, state, badge = true, className }: AgentAvatarProps) {
|
|
27
|
+
const { info } = row
|
|
28
|
+
const shown = state ?? row.state
|
|
29
|
+
const busy = shown === 'working' && image?.busy && image.busy.durations.length > 1 ? image.busy : undefined
|
|
30
|
+
const asleep = info.engineAsleep === true
|
|
31
|
+
const engine = info.engine ?? 'claude'
|
|
32
|
+
const badgeSize = size >= 28 ? 14 : 11
|
|
33
|
+
return (
|
|
34
|
+
<span
|
|
35
|
+
data-slot="agent-avatar"
|
|
36
|
+
className={cn('relative inline-flex shrink-0', className)}
|
|
37
|
+
style={{ width: size, height: size }}
|
|
38
|
+
title={info.agent?.name}
|
|
39
|
+
>
|
|
40
|
+
{busy ? (
|
|
41
|
+
<span
|
|
42
|
+
aria-hidden
|
|
43
|
+
className="wd-avatar-strip size-full rounded-[4px]"
|
|
44
|
+
style={
|
|
45
|
+
{
|
|
46
|
+
backgroundImage: `url(${busy.src})`,
|
|
47
|
+
backgroundSize: `${busy.durations.length * size}px ${size}px`,
|
|
48
|
+
'--wd-strip-end': `-${busy.durations.length * size}px`,
|
|
49
|
+
'--wd-strip-steps': busy.durations.length,
|
|
50
|
+
animationDuration: `${busy.durations.reduce((sum, ms) => sum + ms, 0)}ms`,
|
|
51
|
+
} as CSSProperties
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
) : image ? (
|
|
55
|
+
<img
|
|
56
|
+
src={image.still}
|
|
57
|
+
alt=""
|
|
58
|
+
draggable={false}
|
|
59
|
+
className={cn('size-full rounded-[4px] [image-rendering:pixelated]', asleep && 'opacity-60 grayscale')}
|
|
60
|
+
/>
|
|
61
|
+
) : (
|
|
62
|
+
<span className="flex size-full items-center justify-center rounded-[4px] border border-dashed border-border">
|
|
63
|
+
<EngineIcon engine={engine} model={info.model} className={cn('size-1/2', vendorMarkClass(engine, info.model))} />
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
{badge && (shown !== 'idle' || asleep) ? (
|
|
67
|
+
<span
|
|
68
|
+
className="absolute -right-1 -bottom-1 flex items-center justify-center rounded-full bg-bg"
|
|
69
|
+
style={{ width: badgeSize, height: badgeSize }}
|
|
70
|
+
>
|
|
71
|
+
<SessionStatusIcon row={{ ...row, state: shown }} className="size-full p-px" />
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
74
|
+
</span>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function LeadChip({ className }: { className?: string }) {
|
|
79
|
+
return (
|
|
80
|
+
<span
|
|
81
|
+
className={cn(
|
|
82
|
+
'shrink-0 rounded-[3px] border border-accent/50 px-1 text-[0.625rem] leading-3.5 font-medium tracking-wide text-accent',
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
LEAD
|
|
87
|
+
</span>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AgentHeadingProps {
|
|
92
|
+
row: SessionRow
|
|
93
|
+
image?: AgentAvatarImage
|
|
94
|
+
// Overrides `AgentRef.conversation`, which a gateway sends from the second conversation on.
|
|
95
|
+
conversation?: number
|
|
96
|
+
className?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A session panel's title for an agent: avatar, name, its place in a team. Plain sessions keep the host's own title.
|
|
100
|
+
export function AgentHeading({ row, image, conversation, className }: AgentHeadingProps) {
|
|
101
|
+
const agent = row.info.agent
|
|
102
|
+
if (!agent) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
const n = conversation ?? agent.conversation ?? 1
|
|
106
|
+
const detail = [agent.lead !== undefined && agent.team ? `${agent.team} team` : undefined, `conversation ${n}`]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join(' · ')
|
|
109
|
+
return (
|
|
110
|
+
<span data-slot="agent-heading" className={cn('flex min-w-0 items-center gap-2', className)}>
|
|
111
|
+
<AgentAvatar row={row} image={image} size={24} />
|
|
112
|
+
<span className="truncate text-body-sm font-medium text-fg-1">{agent.name}</span>
|
|
113
|
+
{agent.leads ? <LeadChip /> : null}
|
|
114
|
+
{detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
|
|
115
|
+
</span>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -39,6 +39,8 @@ export type ComposerHandle = {
|
|
|
39
39
|
|
|
40
40
|
export interface ComposerProps {
|
|
41
41
|
onSend: (text: string, attachmentIds: string[]) => void
|
|
42
|
+
// Present while a foreground command the engine can background is running: ⌥↵ sends and moves it to the background.
|
|
43
|
+
onSendNow?: (text: string, attachmentIds: string[]) => void
|
|
42
44
|
onInterrupt: () => void
|
|
43
45
|
busy: boolean
|
|
44
46
|
disabled?: boolean
|
|
@@ -70,6 +72,7 @@ export interface ComposerProps {
|
|
|
70
72
|
|
|
71
73
|
export function Composer({
|
|
72
74
|
onSend,
|
|
75
|
+
onSendNow,
|
|
73
76
|
onInterrupt,
|
|
74
77
|
busy,
|
|
75
78
|
disabled,
|
|
@@ -184,7 +187,7 @@ export function Composer({
|
|
|
184
187
|
rankPeerSessions(query, peers).map((peer) => ({
|
|
185
188
|
value: peer.slug,
|
|
186
189
|
label: peer.label,
|
|
187
|
-
description: [peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
190
|
+
description: [peer.gateway, peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
188
191
|
icon: <Users className="size-3.5 text-fg-3" />,
|
|
189
192
|
})),
|
|
190
193
|
onSelect: (suggestion) => suggestion.value,
|
|
@@ -214,7 +217,7 @@ export function Composer({
|
|
|
214
217
|
const staged = attachments?.items ?? []
|
|
215
218
|
const canSend = !disabled && (!isEmpty || staged.length > 0) && !attachments?.uploading && !attachments?.hasFailure
|
|
216
219
|
|
|
217
|
-
const submit = () => {
|
|
220
|
+
const submit = (now = false) => {
|
|
218
221
|
if (!canSend) {
|
|
219
222
|
return
|
|
220
223
|
}
|
|
@@ -230,7 +233,8 @@ export function Composer({
|
|
|
230
233
|
focus()
|
|
231
234
|
return
|
|
232
235
|
}
|
|
233
|
-
|
|
236
|
+
const deliver = now && onSendNow ? onSendNow : onSend
|
|
237
|
+
deliver(plainText.trim(), attachments?.readyIds ?? [])
|
|
234
238
|
attachments?.clear()
|
|
235
239
|
draft?.clear()
|
|
236
240
|
clear()
|
|
@@ -275,6 +279,11 @@ export function Composer({
|
|
|
275
279
|
setHelpOpen(true)
|
|
276
280
|
return
|
|
277
281
|
}
|
|
282
|
+
if (e.key === 'Enter' && e.altKey && onSendNow && canSend) {
|
|
283
|
+
e.preventDefault()
|
|
284
|
+
submit(true)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
278
287
|
if (helpOpen) {
|
|
279
288
|
setHelpOpen(false)
|
|
280
289
|
}
|
|
@@ -351,9 +360,16 @@ export function Composer({
|
|
|
351
360
|
</span>
|
|
352
361
|
)
|
|
353
362
|
|
|
363
|
+
const sendNow =
|
|
364
|
+
onSendNow && canSend && !shellMode ? (
|
|
365
|
+
<button type="button" className="term-press term-link text-label" onClick={() => submit(true)}>
|
|
366
|
+
⌥↵ send now - moves the running command to the background
|
|
367
|
+
</button>
|
|
368
|
+
) : null
|
|
369
|
+
|
|
354
370
|
const interrupting = busy && !canSend
|
|
355
371
|
const submitButton = terminal ? (
|
|
356
|
-
<GlyphButton label="Send" disabled={!canSend} onClick={submit} tone={canSend ? 'blue' : undefined}>
|
|
372
|
+
<GlyphButton label="Send" disabled={!canSend} onClick={() => submit()} tone={canSend ? 'blue' : undefined}>
|
|
357
373
|
↵
|
|
358
374
|
</GlyphButton>
|
|
359
375
|
) : interrupting ? (
|
|
@@ -361,7 +377,7 @@ export function Composer({
|
|
|
361
377
|
<Square className="size-3" />
|
|
362
378
|
</Button>
|
|
363
379
|
) : (
|
|
364
|
-
<Button size="icon-sm" aria-label="Send" className="rounded-full" disabled={!canSend} onClick={submit}>
|
|
380
|
+
<Button size="icon-sm" aria-label="Send" className="rounded-full" disabled={!canSend} onClick={() => submit()}>
|
|
365
381
|
<ArrowUp className="size-4" />
|
|
366
382
|
</Button>
|
|
367
383
|
)
|
|
@@ -402,7 +418,7 @@ export function Composer({
|
|
|
402
418
|
triggers={triggers}
|
|
403
419
|
markdown={false}
|
|
404
420
|
normalizeBullets={false}
|
|
405
|
-
onSubmit={submit}
|
|
421
|
+
onSubmit={() => submit()}
|
|
406
422
|
disabled={disabled}
|
|
407
423
|
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
408
424
|
minHeight={minHeight}
|
|
@@ -428,6 +444,12 @@ export function Composer({
|
|
|
428
444
|
className={cn('term-composer', shellMode && 'term-composer-shell', disabled && 'opacity-60')}
|
|
429
445
|
>
|
|
430
446
|
<div className="term-composer-body">
|
|
447
|
+
{sendNow ? (
|
|
448
|
+
<div className="term-row">
|
|
449
|
+
<span aria-hidden className="term-gutter" />
|
|
450
|
+
{sendNow}
|
|
451
|
+
</div>
|
|
452
|
+
) : null}
|
|
431
453
|
{stagedStrip}
|
|
432
454
|
<div className="term-row">
|
|
433
455
|
{canAttach && !shellMode ? fileField : null}
|
|
@@ -464,6 +486,7 @@ export function Composer({
|
|
|
464
486
|
return (
|
|
465
487
|
<div data-slot="composer" className={cn('px-[var(--wd-composer-padding)] pb-[var(--wd-composer-padding)]', className)}>
|
|
466
488
|
{helpRow}
|
|
489
|
+
{sendNow ? <div className="mx-auto mb-1 w-full max-w-[var(--wd-transcript-max-width)] text-text-muted">{sendNow}</div> : null}
|
|
467
490
|
<div
|
|
468
491
|
{...dropHandlers}
|
|
469
492
|
className={cn(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EngineCapabilities, ModelOption } from '@workerdeck/protocol'
|
|
2
|
+
import { Brain } from 'lucide-react'
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
4
|
+
import { cn } from '../../lib/utils.ts'
|
|
5
|
+
import { matchModel } from './ModelSelect.tsx'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_VALUE = 'default'
|
|
8
|
+
|
|
9
|
+
export function effortChoices(
|
|
10
|
+
models: readonly ModelOption[],
|
|
11
|
+
model: string | undefined,
|
|
12
|
+
capabilities: Pick<EngineCapabilities, 'reasoningEfforts'>,
|
|
13
|
+
): readonly string[] {
|
|
14
|
+
return matchModel(models, model)?.reasoningEfforts ?? capabilities.reasoningEfforts ?? []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EffortSelectProps {
|
|
18
|
+
efforts: readonly string[]
|
|
19
|
+
effort?: string | null
|
|
20
|
+
onEffortChange: (effort?: string) => void
|
|
21
|
+
variant?: 'toolbar' | 'form'
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function EffortSelect({ efforts, effort, onEffortChange, variant = 'toolbar', disabled, className }: EffortSelectProps) {
|
|
27
|
+
const items = [{ value: DEFAULT_VALUE, label: 'Default' }, ...efforts.map((value) => ({ value, label: value }))]
|
|
28
|
+
return (
|
|
29
|
+
<Select
|
|
30
|
+
items={items}
|
|
31
|
+
value={effort ?? null}
|
|
32
|
+
onValueChange={(value) => {
|
|
33
|
+
if (typeof value !== 'string' || value === effort) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
onEffortChange(value === DEFAULT_VALUE ? undefined : value)
|
|
37
|
+
}}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
>
|
|
40
|
+
<SelectTrigger
|
|
41
|
+
aria-label="Reasoning effort"
|
|
42
|
+
className={cn(
|
|
43
|
+
variant === 'toolbar' && 'h-6 max-w-32 border-transparent bg-transparent text-fg-3 hover:bg-surface-hover',
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<span className={cn('flex items-center gap-1 truncate', variant === 'toolbar' && 'font-mono text-label')}>
|
|
48
|
+
<Brain className="size-3 shrink-0" />
|
|
49
|
+
<SelectValue placeholder="effort" />
|
|
50
|
+
</span>
|
|
51
|
+
</SelectTrigger>
|
|
52
|
+
<SelectContent className="min-w-40">
|
|
53
|
+
{items.map((item) => (
|
|
54
|
+
<SelectItem key={item.value} value={item.value}>
|
|
55
|
+
<SelectItemText>
|
|
56
|
+
<span className="font-medium">{item.label}</span>
|
|
57
|
+
</SelectItemText>
|
|
58
|
+
{item.value === DEFAULT_VALUE ? <span className="text-label text-fg-4">The model's configured default</span> : null}
|
|
59
|
+
</SelectItem>
|
|
60
|
+
))}
|
|
61
|
+
</SelectContent>
|
|
62
|
+
</Select>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -39,7 +39,7 @@ function optionMatches(option: ModelOption, model: string): boolean {
|
|
|
39
39
|
return token !== '' && token === family(dropVariant(option.value))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function matchModel(models: ModelOption[], model?: string): ModelOption | undefined {
|
|
42
|
+
export function matchModel(models: readonly ModelOption[], model?: string): ModelOption | undefined {
|
|
43
43
|
if (!model) {
|
|
44
44
|
return undefined
|
|
45
45
|
}
|