Summary
Claude Code records the live TodoWrite task list at ~/.claude/tasks/<sessionId>/*.json, but the session viewer currently has no way to surface those records. Add first-class read support to core, expose them via the web API, and render them next to the existing todo files so users can see the agent's in-flight plan and check progress (pending → in_progress → completed) at a glance.
This complements the existing Todo integration (packages/core/src/todos.ts, ~/.claude/todos/*.json) but targets a different data source: tasks are stateful, ordered, and carry blocking relationships, whereas todos are flat checklists.
Storage layout (Claude Code, observed)
~/.claude/tasks/
<sessionId>/
.lock
.highwatermark # single number: highest task id assigned
<N>.json # one file per task, integer N matching the id
Sample task JSON:
{
"id": "11",
"subject": "Implement search endpoint",
"description": "Add /api/search returning paginated results.",
"status": "pending",
"blocks": [],
"blockedBy": []
}
Notes from observation:
id is a stringified integer assigned monotonically; .highwatermark mirrors the largest id used.
status observed values: pending, in_progress, completed. Treat any other value as pending defensively.
blocks and blockedBy are arrays of task ids (strings). Cycles should not be possible but the renderer should handle them gracefully.
- Files may appear or disappear mid-session as the agent reorganises; reads must tolerate missing entries.
Core changes (packages/core)
paths.ts: add getTasksDir() returning ~/.claude/tasks.
types.ts: add
export interface TaskItem {
id: string
subject: string
description: string
status: 'pending' | 'in_progress' | 'completed'
blocks: string[]
blockedBy: string[]
}
export interface SessionTasks {
sessionId: string
tasks: TaskItem[]
highWatermark: number
hasTasks: boolean
}
and extend SessionData (around line 377) with tasks: SessionTasks next to todos: SessionTodos.
- New
tasks.ts mirroring the surface of todos.ts:
findSessionTasks(sessionId) → Effect<SessionTasks>. Lists <id>.json files, parses defensively, sorts by numeric id ascending.
sessionHasTasks(sessionId) → Effect<boolean> quick check.
findOrphanTaskDirs() → Effect<string[]> returning session ids with task directories whose session no longer exists. Mirror findOrphanTodos semantics.
deleteOrphanTaskDirs() → Effect<{ success: boolean; deletedCount: number }> moving orphaned dirs into ~/.claude/tasks/.bak/<sessionId>/.
- Tests in
packages/core/src/__tests__/tasks.test.ts covering: empty dir, mixed valid/invalid JSON, status normalisation, orphan detection, backup move.
Web changes (packages/web)
- API: extend the existing session detail endpoint (or add
GET /api/session/[id]/tasks) to return SessionTasks so the viewer can render lazily.
- UI:
- Add a Tasks panel to
SessionViewer (collapsed by default if hasTasks === false).
- Render a flat list grouped by status (
in_progress first, then pending, then completed).
- Show
subject as the primary text, description truncated as secondary text.
- Render
blockedBy ids as small chips that link to the corresponding task row (in-page anchor).
- Storybook story for the panel:
Empty, Mixed statuses, With blockedBy chains.
- Playwright e2e: load a fixture session that has tasks, assert the panel renders and status grouping is correct.
VSCode extension changes (packages/vscode-extension)
treeProvider.ts: when a session has hasTasks, expose a child node group Tasks under the session node. Each task becomes a leaf with subject as label and status as description (in_progress/pending/completed).
- Tree icons: reuse Codicon
circle-outline/sync~spin/check for pending/in_progress/completed.
- Hover: include
description and blockedBy ids.
- No commands required for v0.5.0 — read-only display only.
Storybook
Required stories under packages/web/src/lib/components/:
TaskList.stories.svelte
Default — three tasks, one of each status
Empty — hasTasks === false, panel hidden
BlockedByChain — task B blockedBy: ['A'], task C blockedBy: ['B']
Verification / Test plan
| Feature |
Procedure |
Expected Result |
findSessionTasks parsing |
Run pnpm --filter core test tasks against fixture dir with valid/invalid JSON |
Valid tasks parsed and sorted by numeric id; invalid entries skipped silently |
| Status normalisation |
Feed task JSON with status: "unknown" |
Returned TaskItem.status === 'pending' |
| Orphan detection / backup |
Create task dir for a missing session id, run deleteOrphanTaskDirs() |
Returns deletedCount: 1; dir moved under ~/.claude/tasks/.bak/<sessionId>/ |
| Web API endpoint |
curl GET /api/session/<id>/tasks against dev server with fixture session |
200 with SessionTasks JSON shape (tasks[], highWatermark, hasTasks) |
| Web UI rendering |
Playwright loads fixture session in SessionViewer |
Tasks panel visible, three groups in order in_progress → pending → completed, blockedBy chips link to anchors |
| Storybook stories |
pnpm --filter web storybook then visit TaskList |
Default, Empty, BlockedByChain all render without console errors |
| VSCode tree |
Open fixture session in extension dev host |
Session node has child group Tasks with leaves matching task subjects + status icons |
Out of scope
- Editing tasks from within
claude-code-sessions (read-only display only).
- Cross-session task aggregation (e.g. global Tasks dashboard).
- Notifying on status change (live updates can be a follow-up — initial implementation can re-read on viewer mount).
References
- Existing parallel surface:
packages/core/src/todos.ts, packages/core/src/types.ts (TodoItem, SessionTodos), packages/core/src/paths.ts (getTodosDir).
- Storage layout reference:
~/.claude/tasks/<sessionId>/{<N>.json,.highwatermark,.lock}.
Summary
Claude Code records the live
TodoWritetask list at~/.claude/tasks/<sessionId>/*.json, but the session viewer currently has no way to surface those records. Add first-class read support tocore, expose them via the web API, and render them next to the existing todo files so users can see the agent's in-flight plan and check progress (pending → in_progress → completed) at a glance.This complements the existing
Todointegration (packages/core/src/todos.ts,~/.claude/todos/*.json) but targets a different data source: tasks are stateful, ordered, and carry blocking relationships, whereas todos are flat checklists.Storage layout (Claude Code, observed)
Sample task JSON:
{ "id": "11", "subject": "Implement search endpoint", "description": "Add /api/search returning paginated results.", "status": "pending", "blocks": [], "blockedBy": [] }Notes from observation:
idis a stringified integer assigned monotonically;.highwatermarkmirrors the largest id used.statusobserved values:pending,in_progress,completed. Treat any other value aspendingdefensively.blocksandblockedByare arrays of task ids (strings). Cycles should not be possible but the renderer should handle them gracefully.Core changes (
packages/core)paths.ts: addgetTasksDir()returning~/.claude/tasks.types.ts: addSessionData(around line 377) withtasks: SessionTasksnext totodos: SessionTodos.tasks.tsmirroring the surface oftodos.ts:findSessionTasks(sessionId)→Effect<SessionTasks>. Lists<id>.jsonfiles, parses defensively, sorts by numeric id ascending.sessionHasTasks(sessionId)→Effect<boolean>quick check.findOrphanTaskDirs()→Effect<string[]>returning session ids with task directories whose session no longer exists. MirrorfindOrphanTodossemantics.deleteOrphanTaskDirs()→Effect<{ success: boolean; deletedCount: number }>moving orphaned dirs into~/.claude/tasks/.bak/<sessionId>/.packages/core/src/__tests__/tasks.test.tscovering: empty dir, mixed valid/invalid JSON, status normalisation, orphan detection, backup move.Web changes (
packages/web)GET /api/session/[id]/tasks) to returnSessionTasksso the viewer can render lazily.SessionViewer(collapsed by default ifhasTasks === false).in_progressfirst, thenpending, thencompleted).subjectas the primary text,descriptiontruncated as secondary text.blockedByids as small chips that link to the corresponding task row (in-page anchor).Empty,Mixed statuses,With blockedBy chains.VSCode extension changes (
packages/vscode-extension)treeProvider.ts: when a session hashasTasks, expose a child node group Tasks under the session node. Each task becomes a leaf withsubjectas label and status as description (in_progress/pending/completed).circle-outline/sync~spin/checkfor pending/in_progress/completed.descriptionandblockedByids.Storybook
Required stories under
packages/web/src/lib/components/:TaskList.stories.svelteDefault— three tasks, one of each statusEmpty—hasTasks === false, panel hiddenBlockedByChain— task BblockedBy: ['A'], task CblockedBy: ['B']Verification / Test plan
findSessionTasksparsingpnpm --filter core test tasksagainst fixture dir with valid/invalid JSONstatus: "unknown"TaskItem.status === 'pending'deleteOrphanTaskDirs()deletedCount: 1; dir moved under~/.claude/tasks/.bak/<sessionId>/curl GET /api/session/<id>/tasksagainst dev server with fixture sessionSessionTasksJSON shape (tasks[],highWatermark,hasTasks)SessionViewerin_progress → pending → completed,blockedBychips link to anchorspnpm --filter web storybookthen visitTaskListDefault,Empty,BlockedByChainall render without console errorsOut of scope
claude-code-sessions(read-only display only).References
packages/core/src/todos.ts,packages/core/src/types.ts(TodoItem,SessionTodos),packages/core/src/paths.ts(getTodosDir).~/.claude/tasks/<sessionId>/{<N>.json,.highwatermark,.lock}.