file T-308: surface Claude's task list docked above the input pane

Claude's TodoWrite/TaskCreate task list is currently neither parsed
(transcript_reader skips the event) nor rendered — invisible to the user.
Ticket covers modelling it from the transcript and docking a compact,
collapsible, display-only list directly above the composer in the Claude
conversation view. Filed under the UI epic T-276.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 11:28:11 +02:00
co-authored by Claude Opus 4.8
parent c53e53be91
commit 10a714e2c5
3 changed files with 50 additions and 0 deletions
@@ -2443,3 +2443,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM531VE0C6N36AT12SR', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'dd13cc3149df6c1dc2d27e53fce878c1', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'status', 'in_progress', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'f4a1697c1c839dc92d6bedc2cd86d997', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'fe6d00feb488f12fd0931852b2e66236', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'status', 'backlog', 'in_progress', NULL, '2026-06-10 09:13:08', '2026-06-10 09:13:08', '2026-06-10 09:13:08', NULL, '56fbf20da154b3465d3d2e7dcc87dfc9', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -132,3 +132,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'T-305', '2026-06-10 08:15:39', '2026-06-10 08:15:39', NULL, '288b0c9d74e98c2ac42ee25d3369f689', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16YM1Y910T07Y1Y22MGTEM', 'T-306', '2026-06-10 08:17:42', '2026-06-10 08:17:42', NULL, 'af36af77224b697a61cb5447955605e5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB170DJA02EH2E8HMH7X7SS4', 'T-307', '2026-06-10 08:17:57', '2026-06-10 08:17:57', NULL, '882cca1753c2ede7bfe2aef142545b76', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1Q1Y3CYJHD7W5F68VDB6T4', 'T-308', '2026-06-10 09:28:04', '2026-06-10 09:28:04', NULL, '5050ff0e69b24a93f5b7e96c8a9fa47e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+48
View File
@@ -1252,3 +1252,51 @@ Acceptance: pick a theme (incl. the High-contrast toggle) -> restart -> same the
Tests: SettingsStore round-trips ''project.theme''; opening a project applies the saved theme to the controller; select() persists; unknown saved theme falls back. Refs: T-234 (theme switcher), T-288/D-88 (picker), SettingsStore, ProjectManager.
FIXED 2026-06-09 (this commit). Added lib/kernel/src/theme/theme_persistence.dart + wireThemePersistence() in facade. Decision on the open question: persist BOTH project.theme (per-repo, in .clide/settings.yaml) AND app.theme (global default) so a themed repo keeps its choice, an unthemed/new repo inherits the last global choice, and the HC variant persists (name encodes -hc). Restore prefers project over app; unknown theme is ignored (no throw). Tests: test/kernel/src/theme_persistence_test.dart (6 cases persist app/project, HC, restore-on-open, boot restore, unknown fallback). All green.', 'done', 'medium', NULL, NULL, NULL, '2026-06-09 15:12:15', '2026-06-10 08:57:51', NULL, 'f71e40ac7f3e0a8dd2e52366b1ff5cb9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Unify collapser cards onto one ClideCollapserCard primitive', 'Unify the conversation-panel collapsible cards onto one ClideCollapserCard primitive, and share card spacing across all card categories. Supersedes ClideHolderCard (T-266) and the collapse logic in ConversationCard (T-262) for the tool path.
## Conversation-panel card model (agreed 2026-06-10)
### Shared spacing constants, NOT a shared wrapper
All card categories share a small set of spacing CONSTANTS (inter-card bottom gap, inner padding, corner radius). NOT a forced common wrapper widget each category is its own widget; they just pull the same spacing tokens so the stream reads as one consistent rhythm.
### Three card categories
1. **Dialog cards** carry the side stripe marking who is speaking (user / Claude / agent). Prose / attribution. Not collapsible. (Today: ConversationCard stripe variant.)
2. **Simple cards** a single item shown fully open in the stream, never collapses (e.g. the image-show card; more to come). Standalone display: no chevron, no status chrome.
3. **Collapsibles** the unified collapser. Covers edits, bash, task updates, runs every tool use. Behaves like the bash card should:
- The whole card is clickable to collapse/expand.
- Collapsed: title = the echoed last content line (like bash now) + an item count + aggregate status (spinner / check / cross).
- Expanded: an inner canvas holding the nested item card(s), each item in its own inner card.
- A single item still gets its own inner card inside the collapser when open, and pushes its status / count / last-line up to the collapser header.
- Inner item cards ALSO show their own per-item status (check / cross / spinner) when expanded; the collapser header carries the aggregate.
- Chrome (per the wireframe): `color` (outer border + chevron / label / text), fixed-width counter slot, status icon hard against the right edge, chevron hard against the left edge.
## Scope
- New `ClideCollapserCard` in lib/widgets/ (exported from widgets.dart) the category-3 primitive: a list of 1..N inner item cards, collapsed ticker <-> expanded inner canvas, color / fixed-counter / edge-status / edge-chevron chrome, background + caret toggle (D-78 tail-follow), aggregate status + count + echoed-title computed from the items.
- Shared card-spacing constants consumed by all three categories.
- ALL tool uses render as collapsers single ones as a 1-item list (Bash, Read, Edit, Task, edits runs, activity runs, etc.).
- Inner item cards: content + their own per-item status; no own collapse; no stripe.
- Dialog cards (1) and simple cards (2) are NOT pulled into the collapser they only adopt the shared spacing constants (keep stripe / inner config).
## Verify
- Goldens regenerated (holder_card, conversation_card_merged) + a11y (expanded / collapsed semantics, focusable toggle).
- Wireframe: docs/design/wireframes/cards/collapser-card.{json,png}.
- After landing: update the ui-design skill''s conversation-panel guidance to describe the three card categories + the collapser.
', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 08:15:39', '2026-06-10 09:13:08', NULL, 'f15f786bf4e7f70a427368d3dd5b96aa', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1Q1Y3CYJHD7W5F68VDB6T4', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Surface Claude''s task list docked above the bottom input pane', 'Claude maintains a task/todo list during a session (the TodoWrite / TaskCreate checklist), but clide never shows it — it is completely invisible to the user. The transcript reader (lib/builtin/claude/src/transcript_reader.dart:205-212) skips the relevant event types and there is no model class for task items; the conversation view (conversation_view.dart) renders none. So the user has no view of what Claude is tracking or how far along a multi-step task is.
Surface it as a compact, always-current task list DOCKED to the bottom input pane of the Claude conversation view. Per the Explore: the pane is a Column in claude_pane.dart (_ClaudePaneState.build, ~line 474-542) with ConversationView (Expanded) on top and the ToolPromptCard/ClaudeComposer at the bottom (~line 519-525); the task list should sit between them directly above the composer (claude_composer.dart), non-expanded so it stays pinned and visible as Claude works.
Scope:
- PARSE: model Claude''s task list from the transcript. Find the event type carrying the todo/task items (currently among the skipped types in transcript_reader.dart) and add a TaskList model + items (text + status: pending/in_progress/completed). Latest-write-wins the list reflects the most recent todo state, not an append log.
- RENDER: a docked task-list widget above the composer. Compact: collapsed/summary by default (e.g. ''N tasks · M done'' + current in_progress item), expandable to the full checklist. Per-item status glyph (pending/in-progress/done) using existing tokens + Phosphor icons (ui-design skill). Hide entirely when there are no tasks.
- Keep it display-only and de-emphasised so it never competes with the composer for attention; honour D-78 (interaction zone) this is a display surface, not an interactive control.
- a11y: list semantics, status announced per item.
Acceptance:
1. When Claude has an active task list, it shows docked directly above the input pane and updates live as items change state.
2. Empty/no-task sessions show nothing (no empty chrome).
3. Collapsed by default with a one-line summary; expandable to the full list.
4. Display-only; the composer keeps full function and focus behaviour.
5. Tests: transcript_reader parses the task-list event into the model (incl. status transitions + latest-wins); widget renders states + collapse/expand + hidden-when-empty.
Refs: conversation card patterns (conversation_card.dart), D-78 (interaction zone), ui-design skill (tokens/icons).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 09:28:04', '2026-06-10 09:28:04', NULL, 'aa6646cde3277aabd22519d1b35e48c0', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);