@workerdeck/ui 1.3.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.
- package/build/{SessionPanel-DQp2oStr.mjs → SessionPanel-CE5AVkg6.mjs} +147 -36
- package/build/SessionPanel-CE5AVkg6.mjs.map +1 -0
- package/build/{SessionPanel-BSwUJDwg.d.mts → SessionPanel-CUpq2yO-.d.mts} +6 -4
- package/build/index.d.mts +32 -11
- package/build/index.mjs +24 -41
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +7 -0
- package/build/workspace.d.mts +3 -2
- package/build/workspace.mjs +3 -2
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/SessionBrowser.tsx +0 -6
- package/src/components/agent/SessionItem.tsx +2 -9
- package/src/components/agent/SessionPanel.tsx +33 -2
- package/src/components/agent/SessionSteps.tsx +18 -32
- package/src/components/agent/SessionWorkspace.tsx +3 -0
- package/src/components/agent/StatusBar.tsx +14 -1
- package/src/components/agent/TaskList.tsx +89 -0
- package/src/components/agent/TasksDialog.tsx +46 -0
- package/src/components/terminal/todos.ts +7 -38
- package/src/index.ts +2 -0
- package/build/SessionPanel-DQp2oStr.mjs.map +0 -1
|
@@ -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
|
-
|
|
1
|
+
import type { ChecklistItem, ChecklistStatus } from '@workerdeck/protocol'
|
|
2
|
+
import { parseTodoWriteInput } from '@workerdeck/protocol'
|
|
2
3
|
|
|
3
|
-
export type
|
|
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 =
|
|
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'
|