dock Claude's task list above the composer (T-308)

Claude's TodoWrite checklist was invisible. Add a TaskItem/TaskStatus
model + a latest-wins parser (taskListFrom) that reads the most recent
TodoWrite tool call (it replaces the whole list each time), and a compact
display-only ClaudeTaskDock pinned between the conversation and the
composer: collapsed to "N tasks · M done" + the current in-progress item,
expandable to the full checklist with per-item status glyphs + a11y
labels. Hidden when there are no tasks. Parser + widget tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:31:35 +02:00
co-authored by Claude Opus 4.8
parent 2c8e9f2821
commit 4f632d3cdd
8 changed files with 348 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
/// Claude's working task list, modelled from the conversation for the docked
/// task view (T-308).
///
/// Claude tracks tasks with the `TodoWrite` tool, which **replaces the whole
/// list** on each call — so the current state is simply the todos of the most
/// recent `TodoWrite`. This is a latest-wins snapshot, not an append log; older
/// `TodoWrite` calls are superseded.
library;
import 'package:clide/builtin/claude/src/transcript_reader.dart';
enum TaskStatus { pending, inProgress, completed }
class TaskItem {
const TaskItem({required this.text, required this.status});
final String text;
final TaskStatus status;
@override
bool operator ==(Object other) => other is TaskItem && other.text == text && other.status == status;
@override
int get hashCode => Object.hash(text, status);
}
/// The current task list — the todos of the most recent `TodoWrite` tool call,
/// or empty if Claude hasn't written one this session.
List<TaskItem> taskListFrom(List<ConversationItem> items) {
for (var i = items.length - 1; i >= 0; i--) {
final it = items[i];
if (it is! AssistantToolUse || it.name != 'TodoWrite') continue;
final raw = it.input['todos'];
if (raw is! List) return const [];
return [
for (final t in raw)
if (t is Map)
TaskItem(
// `content` is the canonical label; `activeForm` is the present-tense
// variant TodoWrite also carries — fall back to it, then to empty.
text: (t['content'] ?? t['activeForm'] ?? '').toString(),
status: _statusFrom(t['status']),
),
];
}
return const [];
}
TaskStatus _statusFrom(Object? raw) => switch (raw) {
'in_progress' => TaskStatus.inProgress,
'completed' => TaskStatus.completed,
_ => TaskStatus.pending,
};