@workerdeck/ui 1.4.0 → 2.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.
@@ -1,4 +1,4 @@
1
- import { ArrowRight, Check, ChevronDown, ChevronRight, CircleAlert, Dot, PauseCircle } from 'lucide-react'
1
+ import { ArrowRight, Check, ChevronDown, ChevronRight, CircleAlert } from 'lucide-react'
2
2
  import { isAgentRecord, subagentLabel } from '@workerdeck/protocol'
3
3
  import type { SessionInfo, SubagentInfo } from '@workerdeck/protocol'
4
4
  import { Spinner } from '../ui/Spinner.tsx'
@@ -8,28 +8,22 @@ export type Step = {
8
8
  key: string
9
9
  label: string
10
10
  noun: string
11
- kind: 'agent' | 'task'
12
- state: 'done' | 'running' | 'pending' | 'failed'
11
+ state: 'done' | 'running' | 'failed'
13
12
  detail?: string
14
13
  title: string
15
14
  onSelect: () => void
16
15
  }
17
16
 
18
- export function sessionSteps(info: SessionInfo, onSelect: (toolUseId: string, kind: Step['kind']) => void): Step[] {
19
- const steps = (info.subagents ?? []).map((sub) => {
20
- const kind = isAgentRecord(sub) ? ('agent' as const) : ('task' as const)
21
- return {
22
- key: sub.toolUseId,
23
- label: subagentLabel(sub),
24
- noun: 'agent',
25
- kind,
26
- state: stepState(sub.status),
27
- detail: sub.toolCount > 0 ? String(sub.toolCount) : undefined,
28
- title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,
29
- onSelect: () => onSelect(sub.toolUseId, kind),
30
- }
31
- })
32
- return [...steps.filter((s) => s.kind === 'agent'), ...steps.filter((s) => s.kind === 'task')]
17
+ export function sessionSteps(info: SessionInfo, onSelect: (toolUseId: string) => void): Step[] {
18
+ return (info.subagents ?? []).filter(isAgentRecord).map((sub) => ({
19
+ key: sub.toolUseId,
20
+ label: subagentLabel(sub),
21
+ noun: 'agent',
22
+ state: stepState(sub.status),
23
+ detail: sub.toolCount > 0 ? String(sub.toolCount) : undefined,
24
+ title: `${subagentLabel(sub)} · ${sub.toolCount} tool${sub.toolCount === 1 ? '' : 's'}`,
25
+ onSelect: () => onSelect(sub.toolUseId),
26
+ }))
33
27
  }
34
28
 
35
29
  function stepState(status: SubagentInfo['status']): Step['state'] {
@@ -89,14 +83,12 @@ export function StepToggle({
89
83
  }
90
84
 
91
85
  export function StepRow({ step, active = false, onSelect }: { step: Step; active?: boolean; onSelect: () => void }) {
92
- const agent = step.kind === 'agent'
93
- const selected = active && agent
94
- const body = step.state === 'failed' ? 'text-danger' : agent ? 'text-success' : 'text-fg-4'
86
+ const body = step.state === 'failed' ? 'text-danger' : 'text-success'
95
87
  return (
96
88
  <button
97
89
  type="button"
98
90
  title={step.title}
99
- aria-current={selected || undefined}
91
+ aria-current={active || undefined}
100
92
  onClick={(e) => {
101
93
  e.stopPropagation()
102
94
  onSelect()
@@ -104,22 +96,19 @@ export function StepRow({ step, active = false, onSelect }: { step: Step; active
104
96
  className={cn(
105
97
  'flex w-full items-center gap-1.5 rounded-[4px] py-1 pr-2.5 pl-3.5',
106
98
  'text-left text-micro outline-none',
107
- selected ? 'bg-row-selected' : 'hover:bg-row-active',
99
+ active ? 'bg-row-selected' : 'hover:bg-row-active',
108
100
  body,
109
101
  )}
110
102
  >
111
- <StepIcon state={step.state} kind={step.kind} />
103
+ <StepIcon state={step.state} />
112
104
  <span className="min-w-0 flex-1 truncate">{step.label}</span>
113
105
  {step.detail ? <span className="shrink-0 tabular-nums text-fg-4">{step.detail}</span> : null}
114
- {agent ? <ArrowRight className="size-3.5 shrink-0 text-fg-4" /> : null}
106
+ <ArrowRight className="size-3.5 shrink-0 text-fg-4" />
115
107
  </button>
116
108
  )
117
109
  }
118
110
 
119
- function StepIcon({ state, kind }: { state: Step['state']; kind: Step['kind'] }) {
120
- if (kind === 'task' && state !== 'running' && state !== 'failed') {
121
- return <Dot className="size-[11px] shrink-0" />
122
- }
111
+ function StepIcon({ state }: { state: Step['state'] }) {
123
112
  switch (state) {
124
113
  case 'running': {
125
114
  return <Spinner className="size-[11px] shrink-0" />
@@ -127,9 +116,6 @@ function StepIcon({ state, kind }: { state: Step['state']; kind: Step['kind'] })
127
116
  case 'failed': {
128
117
  return <CircleAlert className="size-[11px] shrink-0" />
129
118
  }
130
- case 'pending': {
131
- return <PauseCircle className="size-[11px] shrink-0" />
132
- }
133
119
  default: {
134
120
  return <Check className="size-[11px] shrink-0" />
135
121
  }
@@ -19,13 +19,13 @@ export interface SessionWorkspaceProps {
19
19
  transcriptVariant?: SessionPanelProps['transcriptVariant']
20
20
  transcriptDensity?: SessionPanelProps['transcriptDensity']
21
21
  transcriptFont?: SessionPanelProps['transcriptFont']
22
- midTurnSend?: SessionPanelProps['midTurnSend']
23
22
  scrubber?: SessionPanelProps['scrubber']
24
23
  bookmarks?: SessionPanelProps['bookmarks']
25
24
  onToggleBookmark?: SessionPanelProps['onToggleBookmark']
26
25
  stickyPrompt?: SessionPanelProps['stickyPrompt']
27
26
  openSubagent?: SessionPanelProps['openSubagent']
28
27
  reveal?: SessionPanelProps['reveal']
28
+ subagents?: SessionPanelProps['subagents']
29
29
  onSubagentChange?: SessionPanelProps['onSubagentChange']
30
30
  statusPlacement?: SessionPanelProps['statusPlacement']
31
31
  controlsSurface?: SessionPanelProps['controlsSurface']
@@ -52,13 +52,13 @@ export function SessionWorkspace({
52
52
  transcriptVariant,
53
53
  transcriptDensity,
54
54
  transcriptFont,
55
- midTurnSend,
56
55
  scrubber,
57
56
  bookmarks,
58
57
  onToggleBookmark,
59
58
  stickyPrompt,
60
59
  openSubagent,
61
60
  reveal,
61
+ subagents,
62
62
  onSubagentChange,
63
63
  statusPlacement,
64
64
  controlsSurface,
@@ -211,13 +211,13 @@ export function SessionWorkspace({
211
211
  transcriptVariant={transcriptVariant}
212
212
  transcriptDensity={transcriptDensity}
213
213
  transcriptFont={transcriptFont}
214
- midTurnSend={midTurnSend}
215
214
  scrubber={scrubber}
216
215
  bookmarks={bookmarks}
217
216
  onToggleBookmark={onToggleBookmark}
218
217
  stickyPrompt={stickyPrompt}
219
218
  openSubagent={openSubagent}
220
219
  reveal={reveal}
220
+ subagents={subagents}
221
221
  onSubagentChange={onSubagentChange}
222
222
  controlsSurface={controlsSurface}
223
223
  fontSize={fontSize}
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState, type ReactNode } from 'react'
2
2
  import type { ConnectionState, TranscriptState } from '@workerdeck/react'
3
- import type { ContextUsage, ProfileEngine, RateLimitInfo } from '@workerdeck/protocol'
3
+ import type { ContextUsage, ProfileEngine, RateLimitInfo, SessionTask } from '@workerdeck/protocol'
4
+ import { taskCountLabel, taskSummary } from '@workerdeck/protocol'
4
5
  import { RefreshCw, WifiOff } from 'lucide-react'
5
6
  import { Badge } from '../ui/Badge.tsx'
6
7
  import { ProgressRing } from '../ui/ProgressRing.tsx'
@@ -23,6 +24,8 @@ export interface StatusBarProps {
23
24
  onOpenStatus?: () => void
24
25
  onOpenContext?: () => void
25
26
  onOpenUsage?: () => void
27
+ tasks?: readonly SessionTask[]
28
+ onOpenTasks?: () => void
26
29
  controls?: ReactNode
27
30
  actions?: ReactNode
28
31
  placement?: 'top' | 'bottom'
@@ -134,6 +137,8 @@ export function StatusBar({
134
137
  onOpenStatus,
135
138
  onOpenContext,
136
139
  onOpenUsage,
140
+ tasks,
141
+ onOpenTasks,
137
142
  controls,
138
143
  actions,
139
144
  placement = 'top',
@@ -146,6 +151,7 @@ export function StatusBar({
146
151
  const weekly = windows?.seven_day
147
152
  const link: ConnectionState = connection ?? (connected === false ? 'reconnecting' : 'live')
148
153
  const terminal = useTranscriptVariant() === 'terminal'
154
+ const taskCount = tasks && tasks.length > 0 ? taskSummary(tasks) : undefined
149
155
  return (
150
156
  <div
151
157
  data-slot="status-bar"
@@ -185,6 +191,13 @@ export function StatusBar({
185
191
  </span>
186
192
  </Slot>
187
193
  ) : null}
194
+ {taskCount ? (
195
+ <Slot onClick={onOpenTasks} hint="Tasks">
196
+ <span className="text-label text-fg-3">
197
+ <span className={cn('tabular-nums', taskCount.running > 0 ? 'text-info' : undefined)}>{taskCountLabel(taskCount)}</span> tasks
198
+ </span>
199
+ </Slot>
200
+ ) : null}
188
201
  {controls ? <span className="self-center">{controls}</span> : null}
189
202
  <span className="flex-1" />
190
203
  <span className="font-mono text-label text-fg-3">{formatCost(state.totalCostUsd)}</span>
@@ -0,0 +1,89 @@
1
+ import { ArrowRight, Check, CircleAlert, Circle } from 'lucide-react'
2
+ import { taskSummary, visibleTasks } from '@workerdeck/protocol'
3
+ import type { SessionTask } from '@workerdeck/protocol'
4
+ import { Spinner } from '../ui/Spinner.tsx'
5
+ import { cn } from '../../lib/utils.ts'
6
+
7
+ export interface TaskListProps {
8
+ tasks: readonly SessionTask[]
9
+ showCompleted: boolean
10
+ // Absent where the toggle lives outside this list — VS Code puts it in the view's title bar.
11
+ onShowCompletedChange?: (showCompleted: boolean) => void
12
+ onSelectTask?: (task: SessionTask) => void
13
+ className?: string
14
+ }
15
+
16
+ export function TaskList({ tasks, showCompleted, onShowCompletedChange, onSelectTask, className }: TaskListProps) {
17
+ const summary = taskSummary(tasks)
18
+ const shown = visibleTasks(tasks, showCompleted)
19
+ const hidden = tasks.length - shown.length
20
+ return (
21
+ <div className={cn('flex flex-col', className)}>
22
+ <div className="flex items-baseline justify-between gap-4 pb-1.5">
23
+ <span className="text-label text-fg-3">
24
+ {summary.done} of {summary.total} done
25
+ {summary.failed > 0 ? ` · ${summary.failed} failed` : ''}
26
+ </span>
27
+ {summary.done > 0 && onShowCompletedChange ? (
28
+ <button
29
+ type="button"
30
+ onClick={() => onShowCompletedChange(!showCompleted)}
31
+ className="shrink-0 rounded-[4px] px-1 py-0.5 text-label text-fg-4 outline-none hover:bg-row-hover hover:text-fg-2"
32
+ >
33
+ {showCompleted ? 'Hide completed' : `Show completed (${summary.done})`}
34
+ </button>
35
+ ) : null}
36
+ </div>
37
+ {shown.length === 0 ? (
38
+ <p className="py-6 text-center text-body-sm text-fg-4">
39
+ {tasks.length === 0 ? 'No tasks yet — a checklist appears once the agent plans one.' : `${hidden} completed, all hidden.`}
40
+ </p>
41
+ ) : (
42
+ <div className="flex flex-col">
43
+ {shown.map((task) => (
44
+ <TaskRow key={task.key} task={task} onSelect={onSelectTask} />
45
+ ))}
46
+ </div>
47
+ )}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ function TaskRow({ task, onSelect }: { task: SessionTask; onSelect?: (task: SessionTask) => void }) {
53
+ const pressable = onSelect !== undefined && task.toolUseId !== undefined
54
+ const body = task.state === 'failed' ? 'text-danger' : task.state === 'done' ? 'text-fg-4' : 'text-fg-2'
55
+ const row = (
56
+ <>
57
+ <TaskIcon state={task.state} />
58
+ <span className={cn('min-w-0 flex-1 truncate text-left', task.state === 'done' ? 'line-through' : undefined)}>{task.label}</span>
59
+ {task.detail ? <span className="shrink-0 tabular-nums text-fg-4">{task.detail}</span> : null}
60
+ {pressable ? <ArrowRight className="size-3.5 shrink-0 text-fg-4" /> : null}
61
+ </>
62
+ )
63
+ const className = cn('flex w-full items-center gap-1.5 rounded-[4px] px-1.5 py-1 text-body-sm outline-none', body)
64
+ if (!pressable) {
65
+ return <div className={className}>{row}</div>
66
+ }
67
+ return (
68
+ <button type="button" title={task.label} onClick={() => onSelect(task)} className={cn(className, 'hover:bg-row-active')}>
69
+ {row}
70
+ </button>
71
+ )
72
+ }
73
+
74
+ function TaskIcon({ state }: { state: SessionTask['state'] }) {
75
+ switch (state) {
76
+ case 'running': {
77
+ return <Spinner className="size-[13px] shrink-0" />
78
+ }
79
+ case 'failed': {
80
+ return <CircleAlert className="size-[13px] shrink-0" />
81
+ }
82
+ case 'done': {
83
+ return <Check className="size-[13px] shrink-0" />
84
+ }
85
+ default: {
86
+ return <Circle className="size-[13px] shrink-0 text-fg-4" />
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,46 @@
1
+ import type { SessionTask } from '@workerdeck/protocol'
2
+ import { Dialog, DialogBody, DialogContent, DialogHeader } from '../ui/Dialog.tsx'
3
+ import { TaskList } from './TaskList.tsx'
4
+
5
+ export interface TasksDialogProps {
6
+ tasks: readonly SessionTask[]
7
+ showCompleted: boolean
8
+ onShowCompletedChange: (showCompleted: boolean) => void
9
+ onSelectTask?: (task: SessionTask) => void
10
+ open: boolean
11
+ onOpenChange: (open: boolean) => void
12
+ className?: string
13
+ }
14
+
15
+ export function TasksDialog({
16
+ tasks,
17
+ showCompleted,
18
+ onShowCompletedChange,
19
+ onSelectTask,
20
+ open,
21
+ onOpenChange,
22
+ className,
23
+ }: TasksDialogProps) {
24
+ return (
25
+ <Dialog open={open} onOpenChange={onOpenChange}>
26
+ <DialogContent className={className}>
27
+ <DialogHeader title="Tasks" description="The agent's checklist and the tasks it has spawned" />
28
+ <DialogBody>
29
+ <TaskList
30
+ tasks={tasks}
31
+ showCompleted={showCompleted}
32
+ onShowCompletedChange={onShowCompletedChange}
33
+ onSelectTask={
34
+ onSelectTask
35
+ ? (task) => {
36
+ onOpenChange(false)
37
+ onSelectTask(task)
38
+ }
39
+ : undefined
40
+ }
41
+ />
42
+ </DialogBody>
43
+ </DialogContent>
44
+ </Dialog>
45
+ )
46
+ }
@@ -1,6 +1,9 @@
1
- export type TodoStatus = 'pending' | 'in_progress' | 'completed'
1
+ import type { ChecklistItem, ChecklistStatus } from '@workerdeck/protocol'
2
+ import { parseTodoWriteInput } from '@workerdeck/protocol'
2
3
 
3
- export type TodoEntry = { status: TodoStatus; text: string }
4
+ export type TodoStatus = ChecklistStatus
5
+
6
+ export type TodoEntry = ChecklistItem
4
7
 
5
8
  export type TodoPreview = { summary: string; shown: TodoEntry[]; more?: string }
6
9
 
@@ -8,46 +11,12 @@ const PREVIEW_TODOS = 8
8
11
 
9
12
  const GLYPH: Record<TodoStatus, string> = { pending: '☐', in_progress: '◐', completed: '☒' }
10
13
 
11
- function todoEntry(value: unknown): TodoEntry | undefined {
12
- if (typeof value !== 'object' || value === null) {
13
- return undefined
14
- }
15
- const raw = value as { content?: unknown; status?: unknown; activeForm?: unknown }
16
- const status = raw.status
17
- if (status !== 'pending' && status !== 'in_progress' && status !== 'completed') {
18
- return undefined
19
- }
20
- const content = typeof raw.content === 'string' ? raw.content.trim() : ''
21
- if (content === '') {
22
- return undefined
23
- }
24
- const active = typeof raw.activeForm === 'string' ? raw.activeForm.trim() : ''
25
- return { status, text: status === 'in_progress' && active !== '' ? active : content }
26
- }
27
-
28
- // Whole-or-nothing: a malformed entry (streaming, partial input) falls back to the generic preview rather than a half-drawn checklist.
29
- export function parseTodos(input: unknown): TodoEntry[] | undefined {
30
- const todos = (input as { todos?: unknown } | null)?.todos
31
- if (!Array.isArray(todos) || todos.length === 0) {
32
- return undefined
33
- }
34
- const out: TodoEntry[] = []
35
- for (const value of todos) {
36
- const entry = todoEntry(value)
37
- if (entry === undefined) {
38
- return undefined
39
- }
40
- out.push(entry)
41
- }
42
- return out
43
- }
44
-
45
14
  export function todoPreview(name: string, input: unknown): TodoPreview | undefined {
46
15
  if (name !== 'TodoWrite') {
47
16
  return undefined
48
17
  }
49
- const todos = parseTodos(input)
50
- if (todos === undefined) {
18
+ const todos = parseTodoWriteInput(input)
19
+ if (todos === undefined || todos.length === 0) {
51
20
  return undefined
52
21
  }
53
22
  const done = todos.reduce((n, todo) => n + (todo.status === 'completed' ? 1 : 0), 0)
package/src/index.ts CHANGED
@@ -75,7 +75,6 @@ export { Reasoning, type ReasoningProps } from './components/agent/Reasoning.tsx
75
75
  export { Loader } from './components/agent/Loader.tsx'
76
76
  export { ToolCallCard, type ToolCallCardProps, type ToolCallItem } from './components/agent/ToolCallCard.tsx'
77
77
  export { ToolTitleProvider, useToolTitle } from './components/agent/tool-titles.tsx'
78
- export { HeldSendsBar, useHeldSends, type HeldSend, type HeldSends } from './components/agent/held-sends.tsx'
79
78
  export { FileCard, type FileCardProps, type FileDeliveredItem } from './components/agent/FileCard.tsx'
80
79
  export { PermissionPrompt, type PermissionPromptProps } from './components/agent/PermissionPrompt.tsx'
81
80
  export {
@@ -99,6 +98,8 @@ export {
99
98
  export { StatusBar, type StatusBarProps } from './components/agent/StatusBar.tsx'
100
99
  export { ContextDialog, type ContextDialogProps } from './components/agent/ContextDialog.tsx'
101
100
  export { UsageDialog, type UsageDialogProps } from './components/agent/UsageDialog.tsx'
101
+ export { TaskList, type TaskListProps } from './components/agent/TaskList.tsx'
102
+ export { TasksDialog, type TasksDialogProps } from './components/agent/TasksDialog.tsx'
102
103
  export { UsageMeters, useMinuteClock } from './components/agent/UsageMeters.tsx'
103
104
  export { SessionInfoDialog, type SessionInfoDialogProps } from './components/agent/SessionInfoDialog.tsx'
104
105
  export { McpDialog, type McpDialogProps } from './components/agent/McpDialog.tsx'