@workerdeck/ui 1.4.0 → 2.0.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,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
@@ -99,6 +99,8 @@ export {
99
99
  export { StatusBar, type StatusBarProps } from './components/agent/StatusBar.tsx'
100
100
  export { ContextDialog, type ContextDialogProps } from './components/agent/ContextDialog.tsx'
101
101
  export { UsageDialog, type UsageDialogProps } from './components/agent/UsageDialog.tsx'
102
+ export { TaskList, type TaskListProps } from './components/agent/TaskList.tsx'
103
+ export { TasksDialog, type TasksDialogProps } from './components/agent/TasksDialog.tsx'
102
104
  export { UsageMeters, useMinuteClock } from './components/agent/UsageMeters.tsx'
103
105
  export { SessionInfoDialog, type SessionInfoDialogProps } from './components/agent/SessionInfoDialog.tsx'
104
106
  export { McpDialog, type McpDialogProps } from './components/agent/McpDialog.tsx'