docs: add planning UX spec/plan and prompts/mailbox proposals
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
docs/mailbox-proposal.md
Normal file
98
docs/mailbox-proposal.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Task Mailbox — Push Messages Into Running Sessions
|
||||||
|
|
||||||
|
**Status:** proposal
|
||||||
|
**Context:** the user runs parallel Claude sessions (e.g. backend + frontend) and wants to push messages into a session while it's busy inside a subagent. A shared folder works for one-offs; this turns it into a first-class ClaudeDo feature so every future parallel-session project gets it for free.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Claude CLI processes one turn at a time. While a subagent (or any long tool) runs, no new user input can be injected. The harness offers no mid-execution interrupt. The workable window is *between* tool calls — so we need a cheap "inbox check" the agent can poll at natural checkpoints, plus a UI affordance and a cross-session sender.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. Data
|
||||||
|
|
||||||
|
New table `task_messages`:
|
||||||
|
|
||||||
|
| col | type | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | INTEGER PK | |
|
||||||
|
| `task_id` | TEXT FK → tasks.id | recipient |
|
||||||
|
| `sender` | TEXT | `'user'` \| `'task:<id>'` (for cross-session) |
|
||||||
|
| `body` | TEXT | markdown |
|
||||||
|
| `created_at` | TEXT | ISO |
|
||||||
|
| `delivered_at` | TEXT NULL | set when inbox pulls it |
|
||||||
|
|
||||||
|
EF Core migration + repository. Async, CancellationToken, matches existing conventions.
|
||||||
|
|
||||||
|
### 2. Worker MCP tools (extend existing `mcp__claudedo__*` server)
|
||||||
|
|
||||||
|
- **`check_inbox(task_id)`** → returns undelivered messages for this task and marks them delivered. Idempotent. Empty array if nothing pending.
|
||||||
|
- **`send_to_task(task_id, body)`** → inserts a row. Callable from *any* session — this is how the frontend session tells the backend session something.
|
||||||
|
- **`inbox_status(task_id)`** → `{ pending: int }` for a cheap "is there anything?" poll.
|
||||||
|
|
||||||
|
All three run in-proc in the Worker, go through the existing repository layer.
|
||||||
|
|
||||||
|
### 3. SignalR additions on `WorkerHub`
|
||||||
|
|
||||||
|
Server methods (UI → Worker):
|
||||||
|
- `SendTaskMessage(taskId, body)` — UI calls this; worker inserts the row and fires `TaskMessageQueued`.
|
||||||
|
|
||||||
|
Client events (Worker → UI):
|
||||||
|
- `TaskMessageQueued(taskId, pendingCount)` — so the UI can show an unread badge.
|
||||||
|
- `TaskMessageDelivered(taskId, pendingCount)` — when the agent pulls it, badge clears.
|
||||||
|
|
||||||
|
### 4. UI
|
||||||
|
|
||||||
|
On every `Running` task row + detail pane:
|
||||||
|
- "Send to session" textarea + Enter to submit → `SendTaskMessage`.
|
||||||
|
- Unread badge showing `pendingCount`.
|
||||||
|
- Read-only message timeline (who sent what, when delivered).
|
||||||
|
|
||||||
|
### 5. Agent-side poll discipline
|
||||||
|
|
||||||
|
Two complementary mechanisms so it's robust whether or not the agent remembers:
|
||||||
|
|
||||||
|
**a) CLAUDE.md instruction** (seeded by worker into each worktree's `CLAUDE.md`):
|
||||||
|
> After every subagent completes and before starting the next step, call `mcp__claudedo__check_inbox`. Treat returned messages as user input with priority over the current plan.
|
||||||
|
|
||||||
|
**b) PostToolUse hook on `Agent`** (written into the worktree's `.claude/settings.json` by the Worker when it creates the tree):
|
||||||
|
- Runs `mcp__claudedo__inbox_status` via a tiny CLI shim the worker ships.
|
||||||
|
- If `pending > 0`, the hook emits a system reminder: "Inbox has N pending messages — call `mcp__claudedo__check_inbox` now."
|
||||||
|
- Keeps the burden off the agent's memory. Belt + suspenders.
|
||||||
|
|
||||||
|
### 6. Cross-session pattern
|
||||||
|
|
||||||
|
Backend session and frontend session are just two tasks with known IDs. Either can call `send_to_task(other_id, body)` via the MCP server. No shared folder needed — the DB is already the shared channel.
|
||||||
|
|
||||||
|
To make this ergonomic:
|
||||||
|
- A "linked tasks" concept: tag two tasks as peers at creation time. The Worker exposes `send_to_peer(body)` as sugar around `send_to_task` so neither session needs to hardcode the other's UUID.
|
||||||
|
|
||||||
|
## Limits (honest)
|
||||||
|
|
||||||
|
- Messages arrive *between* tool calls, not mid-tool. A 20-minute subagent still blocks 20 minutes. Splitting work into shorter subagents is still the right discipline.
|
||||||
|
- If the agent ignores the CLAUDE.md instruction, the hook catches it next tool call — but we can't force immediate consumption.
|
||||||
|
- `-p` (print) mode with stdin prompt is one-shot and can't be extended. This design targets *interactive* sessions (Planning Sessions already use this mode). For queued `-p` runs, the mailbox is effectively a post-run instruction carrier.
|
||||||
|
|
||||||
|
## Why this is the repeatable "Grundgerüst"
|
||||||
|
|
||||||
|
Once this lands in ClaudeDo, the workflow becomes:
|
||||||
|
1. Create two linked tasks (`backend`, `frontend`) with `working_dir` set.
|
||||||
|
2. Start each — each gets its own worktree, its own Planning Session terminal, its own inbox with `check_inbox` + `send_to_peer`.
|
||||||
|
3. Push messages from the UI or from the other session. No per-project scaffolding, no custom hooks, no shared folder.
|
||||||
|
|
||||||
|
Every future parallel-session project inherits the mailbox.
|
||||||
|
|
||||||
|
## Build order (suggested)
|
||||||
|
|
||||||
|
1. Migration + repo + model. Tests first.
|
||||||
|
2. MCP tools (`check_inbox`, `send_to_task`, `inbox_status`) + unit tests.
|
||||||
|
3. SignalR method + events + UI textarea/badge.
|
||||||
|
4. Worker writes CLAUDE.md addendum + `.claude/settings.json` hook into each new worktree.
|
||||||
|
5. Linked-tasks sugar (`send_to_peer`).
|
||||||
|
6. Manual verification: queue a long subagent, send a message, confirm it's picked up at the next tool boundary.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should messages be deleted or soft-kept after delivery? Leaning soft-kept for the timeline UI.
|
||||||
|
- Priority / interrupt semantics — do we want a "high priority" flag that the agent should surface immediately vs. batch?
|
||||||
|
- Should `send_to_peer` also work when the peer is `Queued` (i.e. not yet running)? Probably yes — deliver on start.
|
||||||
198
docs/prompts-inventory.md
Normal file
198
docs/prompts-inventory.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# ClaudeDo — Prompt & CLI Inventory
|
||||||
|
|
||||||
|
Snapshot of every string ClaudeDo sends to Claude CLI, plus the CLI-flag surface that shapes each run. Intended as a working doc for tomorrow's prompt-tuning pass.
|
||||||
|
|
||||||
|
Date: 2026-04-24
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Task-execution prompts (agent-tagged tasks → Claude CLI)
|
||||||
|
|
||||||
|
Used for every "agent" task that the queue picks up or that `RunNow` dispatches.
|
||||||
|
Orchestration lives in `src/ClaudeDo.Worker/Runner/TaskRunner.cs` and `ClaudeArgsBuilder.cs`.
|
||||||
|
|
||||||
|
### 1.1 User prompt (stdin) — `TaskRunner.RunAsync` ~L101–L110
|
||||||
|
|
||||||
|
Plain text, no template around it:
|
||||||
|
|
||||||
|
```
|
||||||
|
{task.Title}
|
||||||
|
|
||||||
|
{task.Description?.Trim()} ← only if non-empty
|
||||||
|
|
||||||
|
## Sub-Tasks ← only if subtasks exist
|
||||||
|
- [ ] {subtask.Title} ← "[x]" if completed
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Title is included verbatim — no leading `#` heading.
|
||||||
|
- No role tags, no XML, no delimiters between title and description — just blank lines.
|
||||||
|
- Sub-Tasks section uses markdown checkboxes. This is the only structural scaffolding.
|
||||||
|
- No context about the project, working dir, or git state is added here.
|
||||||
|
|
||||||
|
### 1.2 Retry prompt (on failure, when a session ID exists) — `TaskRunner` ~L126
|
||||||
|
|
||||||
|
```
|
||||||
|
The previous attempt failed with:
|
||||||
|
|
||||||
|
{result.ErrorMarkdown}
|
||||||
|
|
||||||
|
Try again and fix the issues.
|
||||||
|
```
|
||||||
|
|
||||||
|
Fired once per task via `--resume <session_id>`; if the retry also fails, the task is marked Failed.
|
||||||
|
|
||||||
|
### 1.3 Follow-up prompt (multi-turn `ContinueAsync`) — `TaskRunner.ContinueAsync` L159
|
||||||
|
|
||||||
|
The UI/hub supplies `followUpPrompt` as-is; no wrapping. The session is resumed via `--resume`. So the effective "prompt template" is whatever the user types in the Continue textbox.
|
||||||
|
|
||||||
|
### 1.4 System prompt — merged in `TaskRunner` ~L413–L418
|
||||||
|
|
||||||
|
Built by `TaskRunner.MergeInstructions(global, list, task)` which concatenates three optional strings with `\n\n`:
|
||||||
|
|
||||||
|
1. `AppSettings.DefaultClaudeInstructions` (global, set in Settings modal, default `""`)
|
||||||
|
2. `list_config.SystemPrompt` (per-list override)
|
||||||
|
3. `task.SystemPrompt` (per-task override)
|
||||||
|
|
||||||
|
The merged string is passed as `--append-system-prompt <instructions>` to the CLI. Empty/whitespace → flag is omitted entirely.
|
||||||
|
|
||||||
|
**Currently the global `DefaultClaudeInstructions` ships as empty string** (see `AppSettingsEntity.cs` L9). Anything in the system prompt today is whatever the user typed into Settings / List-Settings / Task-Settings.
|
||||||
|
|
||||||
|
### 1.5 CLI args — `ClaudeArgsBuilder.Build` (`ClaudeArgsBuilder.cs`)
|
||||||
|
|
||||||
|
Always on:
|
||||||
|
- `-p`
|
||||||
|
- `--output-format stream-json`
|
||||||
|
- `--verbose`
|
||||||
|
- `--permission-mode {auto|acceptEdits|plan|default}` (legacy `bypassPermissions` → `auto`)
|
||||||
|
|
||||||
|
Conditional:
|
||||||
|
- `--model {sonnet|opus|haiku|...}` — from `task.Model ?? list.Model ?? AppSettings.DefaultModel` (default `sonnet`)
|
||||||
|
- `--max-turns {n}` — `AppSettings.DefaultMaxTurns` (default `100`)
|
||||||
|
- `--append-system-prompt "{merged instructions}"` — see 1.4
|
||||||
|
- `--agents '[{"file":"{path}"}]'` — from task or list override, points at an agent `.md`
|
||||||
|
- `--resume {session_id}` — for retries and `ContinueAsync`
|
||||||
|
|
||||||
|
Unused but pre-declared:
|
||||||
|
- `ResultSchema` — a `{summary, files_changed, commit_type}` JSON schema is serialized but **never attached** to args in `Build`. Dead code today; relevant if we turn on `--output-schema`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Planning-agent prompts (`/plan` / Planning session)
|
||||||
|
|
||||||
|
Used by the Planning feature, which spawns a Claude session inside a git worktree with MCP tools so the agent can create Subtasks under the parent.
|
||||||
|
Source: `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs`.
|
||||||
|
|
||||||
|
### 2.1 System prompt — `BuildSystemPrompt()` L290–L308
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a planning assistant for ClaudeDo.
|
||||||
|
Your role is to help break down a task into smaller, actionable subtasks.
|
||||||
|
Your final goal WILL ALWAYS be the creation of Subtasks
|
||||||
|
|
||||||
|
ALWAYS invoke the `superpowers:brainstorming` skill via the Skill tool at the
|
||||||
|
start of every planning session, and follow its process end-to-end. It guides
|
||||||
|
you through clarifying questions, approach exploration, and design approval
|
||||||
|
BEFORE any subtasks are created. Do not create child tasks until the user has
|
||||||
|
approved a design.
|
||||||
|
|
||||||
|
NEVER Change files yourself.
|
||||||
|
|
||||||
|
ALWAYS Use the available MCP tools (mcp__claudedo__*) to create child tasks once the
|
||||||
|
design is approved. When you are done planning, finalize the session.
|
||||||
|
|
||||||
|
Be concise and focused. Each subtask should be independently executable.
|
||||||
|
```
|
||||||
|
|
||||||
|
Written to `{session-dir}/system-prompt.md` at session start and fed via `--append-system-prompt`.
|
||||||
|
|
||||||
|
Notes / known oddities
|
||||||
|
- Trailing space on "NEVER Change files yourself. " and on the blank line above the ALWAYS/MCP block.
|
||||||
|
- Mixes voice ("Your role is", "ALWAYS invoke") — could be tightened.
|
||||||
|
- Implicitly relies on the `superpowers:brainstorming` skill being installed in the worktree's Claude config.
|
||||||
|
- Does not name the MCP tools explicitly (the `mcp__claudedo__*` wildcard assumes the agent discovers them via tool listing).
|
||||||
|
|
||||||
|
### 2.2 Initial prompt — `BuildInitialPrompt(task)` L310–L323
|
||||||
|
|
||||||
|
```
|
||||||
|
# Task: {task.Title}
|
||||||
|
|
||||||
|
{task.Description} ← only if non-empty
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Please analyze this task and break it down into concrete subtasks.
|
||||||
|
```
|
||||||
|
|
||||||
|
Written to `{session-dir}/initial-prompt.txt`; the Windows Terminal launcher pipes it to the Claude CLI on start.
|
||||||
|
|
||||||
|
### 2.3 Planning session CLI flags
|
||||||
|
|
||||||
|
`PlanningSessionManager` itself does not build CLI args — the `WindowsTerminalPlanningLauncher` does. Relevant facts:
|
||||||
|
- Permission mode: **plan** (per recent commit `8e9f09a` "run planning agent in plan permission mode and enforce brainstorming skill").
|
||||||
|
- Runs with an `.mcp.json` that points at our local MCP server (`http://127.0.0.1:{port}/mcp`) with a per-session bearer token.
|
||||||
|
- `.claude/settings.local.json` sets `"enableAllProjectMcpServers": true` so the MCP tools auto-activate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Commit-message template (not a prompt, but agent-visible)
|
||||||
|
|
||||||
|
Built by `CommitMessageBuilder.Build` (`CommitMessageBuilder.cs`). Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
{commitType}({listSlug}): {title ≤60 chars}
|
||||||
|
|
||||||
|
{description ≤400 chars} ← only if set
|
||||||
|
|
||||||
|
ClaudeDo-Task: {taskId}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `commitType` comes from `task.CommitType` (default `chore`, list default configurable).
|
||||||
|
- Slug = lowercased list name with non-alphanumerics stripped, runs collapsed to `-`.
|
||||||
|
- The agent sees the resulting commit in `git log` during retries and follow-ups, so phrasing here bleeds into model behavior on multi-turn work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Where each prompt is edited (UI surface)
|
||||||
|
|
||||||
|
| Prompt slot | Edited in | Stored as |
|
||||||
|
|-------------------------------------|--------------------------------------------|--------------------------------------------|
|
||||||
|
| Global `DefaultClaudeInstructions` | Settings modal (`SettingsModalViewModel`) | `app_settings.DefaultClaudeInstructions` |
|
||||||
|
| Per-list system prompt | List-Settings modal | `list_config.SystemPrompt` |
|
||||||
|
| Per-task system prompt | Details island / task agent settings | `tasks.system_prompt` |
|
||||||
|
| Per-task agent file | Details island | `tasks.agent_path` (absolute `.md` path) |
|
||||||
|
| Default model / max turns / perms | Settings modal | `app_settings.*` |
|
||||||
|
| Planning system prompt | **Hard-coded** in `PlanningSessionManager` | not editable from UI |
|
||||||
|
| Planning initial prompt template | **Hard-coded** in `PlanningSessionManager` | not editable from UI |
|
||||||
|
| Retry prompt | **Hard-coded** in `TaskRunner` | not editable |
|
||||||
|
| Task prompt structure (title/desc) | **Hard-coded** in `TaskRunner` | not editable |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Things worth reviewing tomorrow
|
||||||
|
|
||||||
|
1. **Task-execution prompt has no frame at all.** Just title + description. Consider whether a thin wrapper (goal / constraints / done-criteria) improves agent focus without bloating small tasks.
|
||||||
|
2. **Global DefaultClaudeInstructions is empty out of the box.** This is the cleanest place to put project-wide guardrails (commit format, branch etiquette, verify-before-done, no force push). Right now nothing is there.
|
||||||
|
3. **Planning system prompt**:
|
||||||
|
- Typo-level: trailing spaces, inconsistent capitalization ("ALWAYS"/"NEVER"/"Always").
|
||||||
|
- "Your final goal WILL ALWAYS be the creation of Subtasks" conflicts slightly with "Do not create child tasks until the user has approved a design" — rewordable.
|
||||||
|
- Does not state how many subtasks is reasonable, nor how granular.
|
||||||
|
- Does not describe the MCP tool surface; the agent has to discover `mcp__claudedo__*` tools.
|
||||||
|
4. **Retry prompt is minimal.** `"Try again and fix the issues."` — could be firmer about not repeating the same failure mode.
|
||||||
|
5. **Sub-Tasks block** is dumped as plain checkboxes with no instruction ("please complete all open items", "do them in order", etc.). If the user relies on subtasks for ordering, that intent isn't conveyed.
|
||||||
|
6. **ResultSchema is defined but unused.** Decide: drop it, or wire it up (`--output-schema`) and start asking for structured summaries.
|
||||||
|
7. **Commit-message template** never tells the agent what `commit_type` to pick when it has flexibility — the value is hard-coded per task. Consider exposing as a prompt hint or inferring from diffs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. File pointers
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — user/retry/follow-up prompts, MergeInstructions
|
||||||
|
- `src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs` — CLI args + ResultSchema
|
||||||
|
- `src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs` — commit template
|
||||||
|
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — planning system + initial prompts
|
||||||
|
- `src/ClaudeDo.Worker/Planning/WindowsTerminalPlanningLauncher.cs` — planning CLI invocation
|
||||||
|
- `src/ClaudeDo.Data/Models/AppSettingsEntity.cs` — global defaults
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` — UI for global defaults
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` — UI for per-list overrides
|
||||||
@@ -0,0 +1,799 @@
|
|||||||
|
# Planning UX Polish + Sequential Subtask Queue — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add sequential execution of planning subtasks (new `Waiting` status, context-menu trigger, worker-side chain advancement) plus three small UX changes (auto-collapse done planning parents in the task list, collapsible Description in the Details pane, narrower island GridSplitters).
|
||||||
|
|
||||||
|
**Architecture:** Foundation first — add the new `Waiting` enum value and its surface in the UI (chip, virtual-queued filter, row plumbing). Then ship the three UI polish items independently. Finally build the worker-side chain coordinator behind TDD and wire up the SignalR method + context-menu entry.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm, EF Core (Sqlite), SignalR, xUnit.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-24-planning-ux-and-sequential-subtasks-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Add `Waiting` status to the enum
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs` (TaskStatus enum)
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` (chip class switch)
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (virtual-queued match predicate)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `Waiting` to the enum**
|
||||||
|
|
||||||
|
Append `Waiting` as the last value (keeps existing numeric slots stable for any int-serialized rows).
|
||||||
|
|
||||||
|
`src/ClaudeDo.Data/Models/TaskEntity.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum TaskStatus
|
||||||
|
{
|
||||||
|
Manual,
|
||||||
|
Queued,
|
||||||
|
Running,
|
||||||
|
Done,
|
||||||
|
Failed,
|
||||||
|
Planning,
|
||||||
|
Planned,
|
||||||
|
Draft,
|
||||||
|
Waiting,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Extend `StatusChipClass` switch in TaskRowViewModel**
|
||||||
|
|
||||||
|
`src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — update the switch:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public string StatusChipClass => Status switch
|
||||||
|
{
|
||||||
|
TaskStatus.Running => "running",
|
||||||
|
TaskStatus.Failed => "error",
|
||||||
|
TaskStatus.Done => "review",
|
||||||
|
TaskStatus.Queued => "queued",
|
||||||
|
TaskStatus.Waiting => "waiting",
|
||||||
|
_ => "idle",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `IsWaiting` and include it in virtual-queued matching**
|
||||||
|
|
||||||
|
In the same `TaskRowViewModel.cs`, add alongside `IsQueued`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool IsWaiting => Status == TaskStatus.Waiting;
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`, find the `TaskMatchesList` static method and update the `virtual:queued` branch so tasks in `Waiting` also match. Locate the existing match for `ListKind.Virtual when list.Id == "virtual:queued"` and change it to match `t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting`. If the existing line reads `t.Status == TaskStatus.Queued` exactly, replace it with `t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj
|
||||||
|
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both build with 0 errors. Existing warnings OK.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Data/Models/TaskEntity.cs \
|
||||||
|
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \
|
||||||
|
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
|
||||||
|
git commit -m "feat(data): add Waiting task status and include it in virtual:queued"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Narrower island GridSplitters
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml` (lines 158 and 170)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Halve the splitter width**
|
||||||
|
|
||||||
|
Both `GridSplitter` elements currently use `Width="5"`. Change both to `Width="3"`. Leave all other attributes untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Views/MainWindow.axaml
|
||||||
|
git commit -m "style(ui): narrow island GridSplitters from 5 to 3"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Collapsible Description section in Details pane
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add observable flag + toggle command**
|
||||||
|
|
||||||
|
In `DetailsIslandViewModel.cs`, add beside the existing editable fields:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private bool _isDescriptionExpanded = true;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Reset flag when a new task is loaded**
|
||||||
|
|
||||||
|
Find the method that handles a new `Task` being bound (the existing `OnTaskChanged` / `Bind` path — it's the spot that already sets `EditableTitle`, `EditableDescription`, etc.). At the start of the load path where fields get reset, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IsDescriptionExpanded = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
(If the reset is scattered, put it next to the `EditableDescription = ""` assignment.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Wrap the description TextBox in a collapsible section**
|
||||||
|
|
||||||
|
In `DetailsIslandView.axaml`, locate the description TextBox. Wrap it so it looks like:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<Button Classes="flat"
|
||||||
|
Command="{Binding ToggleDescriptionExpandedCommand}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left"
|
||||||
|
Padding="0">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<PathIcon Width="10" Height="10"
|
||||||
|
Data="{StaticResource Icon.ChevronDown}"
|
||||||
|
IsVisible="{Binding IsDescriptionExpanded}"/>
|
||||||
|
<PathIcon Width="10" Height="10"
|
||||||
|
Data="{StaticResource Icon.ChevronRight}"
|
||||||
|
IsVisible="{Binding !IsDescriptionExpanded}"/>
|
||||||
|
<TextBlock Classes="eyebrow" Text="DESCRIPTION"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- existing description TextBox goes here unchanged, but add: -->
|
||||||
|
<TextBox ...existing attributes...
|
||||||
|
IsVisible="{Binding IsDescriptionExpanded}"/>
|
||||||
|
</StackPanel>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the existing `Icon.ChevronDown` / `Icon.ChevronRight` static resources don't exist, inspect `App.axaml` (or wherever `StaticResource Icon.*` icons live) and pick the closest existing chevron pair. If only one direction exists, use a simple `▾` / `▸` TextBlock substitute:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TextBlock Text="▾" IsVisible="{Binding IsDescriptionExpanded}"/>
|
||||||
|
<TextBlock Text="▸" IsVisible="{Binding !IsDescriptionExpanded}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Manual verify**
|
||||||
|
|
||||||
|
Launch the app (`dotnet run --project src/ClaudeDo.App`), open a task with a description, click the chevron. Verify the body collapses/expands; verify opening a different task restores the expanded default.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs \
|
||||||
|
src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
|
||||||
|
git commit -m "feat(ui): collapsible description section in details pane"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Auto-collapse done planning parents in task list
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add expansion state + "all children done" flag to `TaskRowViewModel`**
|
||||||
|
|
||||||
|
In `TaskRowViewModel.cs`, add below the existing observable properties:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private bool _areChildrenExpanded = true;
|
||||||
|
[ObservableProperty] private bool _allChildrenDone;
|
||||||
|
|
||||||
|
partial void OnAllChildrenDoneChanged(bool value)
|
||||||
|
{
|
||||||
|
// Default children to collapsed once the planning parent is fully done.
|
||||||
|
if (value) AreChildrenExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleChildrenExpanded() => AreChildrenExpanded = !AreChildrenExpanded;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Compute `AllChildrenDone` during Regroup in `TasksIslandViewModel`**
|
||||||
|
|
||||||
|
In `TasksIslandViewModel.cs`, locate the `Regroup()` method (the one that clears and repopulates `OverdueItems`/`OpenItems`/`CompletedItems`). Before it distributes rows, build a lookup of children by parent id:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var childrenByParent = Items
|
||||||
|
.Where(r => r.IsChild && r.ParentTaskId is not null)
|
||||||
|
.GroupBy(r => r.ParentTaskId!)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild))
|
||||||
|
{
|
||||||
|
if (childrenByParent.TryGetValue(parent.Id, out var kids) && kids.Count > 0)
|
||||||
|
parent.AllChildrenDone = kids.All(c => c.Status == TaskStatus.Done);
|
||||||
|
else
|
||||||
|
parent.AllChildrenDone = false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then inside the existing distribution loop, skip child rows whose parent row has `AreChildrenExpanded == false`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
foreach (var row in Items)
|
||||||
|
{
|
||||||
|
if (row.IsChild && row.ParentTaskId is not null)
|
||||||
|
{
|
||||||
|
var parentRow = Items.FirstOrDefault(p => p.Id == row.ParentTaskId);
|
||||||
|
if (parentRow is not null && !parentRow.AreChildrenExpanded) continue;
|
||||||
|
}
|
||||||
|
// ... existing distribution into Overdue/Open/Completed ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `Regroup()` currently uses LINQ expressions instead of a loop, split them out into explicit foreach so the skip is clear. Keep the overdue/completed logic intact — children of a collapsed parent are excluded from every bucket.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Re-run Regroup when a row's expansion flag toggles**
|
||||||
|
|
||||||
|
In `TasksIslandViewModel.cs`, in the constructor (after `Items` is created), subscribe to changes so toggling one row triggers a regroup:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Items.CollectionChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.NewItems is not null)
|
||||||
|
foreach (TaskRowViewModel r in e.NewItems)
|
||||||
|
r.PropertyChanged += OnItemPropertyChanged;
|
||||||
|
if (e.OldItems is not null)
|
||||||
|
foreach (TaskRowViewModel r in e.OldItems)
|
||||||
|
r.PropertyChanged -= OnItemPropertyChanged;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the handler:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void OnItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(TaskRowViewModel.AreChildrenExpanded))
|
||||||
|
Regroup();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add chevron toggle button to the planning-parent row**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, inside the main task card where the title/eyebrow row lives (co-located with `PlanningBadge`), add a chevron button visible only when `IsPlanningParent && HasPlanningChildren`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button Classes="flat"
|
||||||
|
Command="{Binding ToggleChildrenExpandedCommand}"
|
||||||
|
IsVisible="{Binding HasPlanningChildren}"
|
||||||
|
Padding="0" Margin="0,0,6,0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock FontSize="10"
|
||||||
|
Text="▾"
|
||||||
|
IsVisible="{Binding AreChildrenExpanded}"/>
|
||||||
|
<TextBlock FontSize="10"
|
||||||
|
Text="▸"
|
||||||
|
IsVisible="{Binding !AreChildrenExpanded}"/>
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Place it immediately before the title TextBlock in the parent-row layout. Leave child rows untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Manual verify**
|
||||||
|
|
||||||
|
Create a planning parent with ≥2 children. Mark both children `Done` (manually via DB if needed, or via a full planning run). Reload the list — the children should be hidden by default. Click the chevron on the parent — children appear. Click again — collapse.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \
|
||||||
|
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs \
|
||||||
|
src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
|
||||||
|
git commit -m "feat(ui): auto-collapse done planning parents in task list"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: PlanningChainCoordinator — worker-side chain advancement (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs`
|
||||||
|
- Create: `tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the first failing test — queueing sets first child Queued, rest Waiting**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Planning;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Xunit;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Planning;
|
||||||
|
|
||||||
|
public class PlanningChainCoordinatorTests
|
||||||
|
{
|
||||||
|
private static DbContextOptions<ClaudeDoDbContext> InMemoryOptions() =>
|
||||||
|
new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||||
|
.UseSqlite("DataSource=:memory:;Cache=Shared")
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
private static async Task<(ClaudeDoDbContext ctx, TaskRepository repo)> NewDbAsync()
|
||||||
|
{
|
||||||
|
var ctx = new ClaudeDoDbContext(InMemoryOptions());
|
||||||
|
await ctx.Database.OpenConnectionAsync();
|
||||||
|
await ctx.Database.EnsureCreatedAsync();
|
||||||
|
return (ctx, new TaskRepository(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedPlanningFamily(TaskRepository repo, string parentId, int childCount)
|
||||||
|
{
|
||||||
|
await repo.AddAsync(new TaskEntity
|
||||||
|
{
|
||||||
|
Id = parentId, ListId = "L1", Title = "Parent",
|
||||||
|
CreatedAt = System.DateTime.UtcNow, Status = TaskStatus.Planned,
|
||||||
|
});
|
||||||
|
for (int i = 0; i < childCount; i++)
|
||||||
|
{
|
||||||
|
await repo.AddAsync(new TaskEntity
|
||||||
|
{
|
||||||
|
Id = $"{parentId}-c{i}", ListId = "L1", Title = $"Child {i}",
|
||||||
|
CreatedAt = System.DateTime.UtcNow, Status = TaskStatus.Manual,
|
||||||
|
ParentTaskId = parentId, SortOrder = i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task QueueSubtasksSequentially_SetsFirstQueued_RestWaiting()
|
||||||
|
{
|
||||||
|
var (ctx, repo) = await NewDbAsync();
|
||||||
|
await using var _ = ctx;
|
||||||
|
await SeedPlanningFamily(repo, "P", 3);
|
||||||
|
|
||||||
|
var coord = new PlanningChainCoordinator(repo);
|
||||||
|
await coord.QueueSubtasksSequentiallyAsync("P", default);
|
||||||
|
|
||||||
|
var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync();
|
||||||
|
Assert.Equal(TaskStatus.Queued, kids[0].Status);
|
||||||
|
Assert.Equal(TaskStatus.Waiting, kids[1].Status);
|
||||||
|
Assert.Equal(TaskStatus.Waiting, kids[2].Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test — expect failure (class doesn't exist)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: compile error "PlanningChainCoordinator not found".
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create the coordinator with the minimum to pass**
|
||||||
|
|
||||||
|
`src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Planning;
|
||||||
|
|
||||||
|
public sealed class PlanningChainCoordinator
|
||||||
|
{
|
||||||
|
private readonly TaskRepository _tasks;
|
||||||
|
|
||||||
|
public PlanningChainCoordinator(TaskRepository tasks) => _tasks = tasks;
|
||||||
|
|
||||||
|
public async Task QueueSubtasksSequentiallyAsync(string parentTaskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var parent = await _tasks.GetByIdAsync(parentTaskId, ct)
|
||||||
|
?? throw new InvalidOperationException($"Task {parentTaskId} not found.");
|
||||||
|
|
||||||
|
var children = (await _tasks.GetChildrenAsync(parentTaskId, ct))
|
||||||
|
.OrderBy(t => t.SortOrder)
|
||||||
|
.ToList();
|
||||||
|
if (children.Count == 0)
|
||||||
|
throw new InvalidOperationException("Parent has no subtasks.");
|
||||||
|
|
||||||
|
var bad = children.FirstOrDefault(c => c.Status is not (TaskStatus.Manual or TaskStatus.Planned));
|
||||||
|
if (bad is not null)
|
||||||
|
throw new InvalidOperationException($"Child {bad.Id} is in status {bad.Status}; expected Manual or Planned.");
|
||||||
|
|
||||||
|
for (int i = 0; i < children.Count; i++)
|
||||||
|
{
|
||||||
|
children[i].Status = i == 0 ? TaskStatus.Queued : TaskStatus.Waiting;
|
||||||
|
await _tasks.UpdateAsync(children[i], ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `TaskRepository.GetChildrenAsync` does not yet exist, add it:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// in src/ClaudeDo.Data/Repositories/TaskRepository.cs
|
||||||
|
public Task<List<TaskEntity>> GetChildrenAsync(string parentTaskId, CancellationToken ct = default) =>
|
||||||
|
_ctx.Tasks.Where(t => t.ParentTaskId == parentTaskId).ToListAsync(ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
(If the repo uses `AsNoTracking()` elsewhere for reads, match that pattern. For this method we want tracked entities so `UpdateAsync` works without extra attach.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test — expect pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 1 passed.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add failing test — on child Done, next Waiting sibling flips to Queued**
|
||||||
|
|
||||||
|
Append to `PlanningChainCoordinatorTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task OnChildDone_FlipsNextWaitingToQueued()
|
||||||
|
{
|
||||||
|
var (ctx, repo) = await NewDbAsync();
|
||||||
|
await using var _ = ctx;
|
||||||
|
await SeedPlanningFamily(repo, "P", 3);
|
||||||
|
|
||||||
|
var coord = new PlanningChainCoordinator(repo);
|
||||||
|
await coord.QueueSubtasksSequentiallyAsync("P", default);
|
||||||
|
|
||||||
|
// Simulate first child finishing Done.
|
||||||
|
var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
|
||||||
|
first.Status = TaskStatus.Done;
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var advanced = await coord.OnChildFinishedAsync("P-c0", TaskStatus.Done, default);
|
||||||
|
|
||||||
|
Assert.Equal("P-c1", advanced);
|
||||||
|
var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync();
|
||||||
|
Assert.Equal(TaskStatus.Done, kids[0].Status);
|
||||||
|
Assert.Equal(TaskStatus.Queued, kids[1].Status);
|
||||||
|
Assert.Equal(TaskStatus.Waiting, kids[2].Status);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run — expect failure**
|
||||||
|
|
||||||
|
Expected: compile error "OnChildFinishedAsync does not exist".
|
||||||
|
|
||||||
|
- [ ] **Step 7: Implement `OnChildFinishedAsync`**
|
||||||
|
|
||||||
|
In `PlanningChainCoordinator.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Call after a child task transitions to a terminal status.
|
||||||
|
/// Returns the id of the newly-queued sibling (if any), else null.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string?> OnChildFinishedAsync(string childTaskId, TaskStatus finalStatus, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (finalStatus != TaskStatus.Done) return null;
|
||||||
|
|
||||||
|
var child = await _tasks.GetByIdAsync(childTaskId, ct);
|
||||||
|
if (child?.ParentTaskId is null) return null;
|
||||||
|
|
||||||
|
var siblings = (await _tasks.GetChildrenAsync(child.ParentTaskId, ct))
|
||||||
|
.OrderBy(t => t.SortOrder)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var next = siblings
|
||||||
|
.Where(s => s.SortOrder > child.SortOrder && s.Status == TaskStatus.Waiting)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (next is null) return null;
|
||||||
|
|
||||||
|
next.Status = TaskStatus.Queued;
|
||||||
|
await _tasks.UpdateAsync(next, ct);
|
||||||
|
return next.Id;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run — expect pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 2 passed.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Add failing test — on Failed, chain stops**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task OnChildFailed_DoesNotAdvanceChain()
|
||||||
|
{
|
||||||
|
var (ctx, repo) = await NewDbAsync();
|
||||||
|
await using var _ = ctx;
|
||||||
|
await SeedPlanningFamily(repo, "P", 3);
|
||||||
|
|
||||||
|
var coord = new PlanningChainCoordinator(repo);
|
||||||
|
await coord.QueueSubtasksSequentiallyAsync("P", default);
|
||||||
|
|
||||||
|
var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
|
||||||
|
first.Status = TaskStatus.Failed;
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var advanced = await coord.OnChildFinishedAsync("P-c0", TaskStatus.Failed, default);
|
||||||
|
|
||||||
|
Assert.Null(advanced);
|
||||||
|
var kids = await ctx.Tasks.Where(t => t.ParentTaskId == "P").OrderBy(t => t.SortOrder).ToListAsync();
|
||||||
|
Assert.Equal(TaskStatus.Failed, kids[0].Status);
|
||||||
|
Assert.Equal(TaskStatus.Waiting, kids[1].Status);
|
||||||
|
Assert.Equal(TaskStatus.Waiting, kids[2].Status);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 10: Run — expect pass (existing guard handles it)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~PlanningChainCoordinatorTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 3 passed.
|
||||||
|
|
||||||
|
- [ ] **Step 11: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs \
|
||||||
|
src/ClaudeDo.Data/Repositories/TaskRepository.cs \
|
||||||
|
tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs
|
||||||
|
git commit -m "feat(worker): add PlanningChainCoordinator with sequential subtask advancement"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Hook chain advancement into TaskRunner finish path
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Program.cs` (DI registration)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Register `PlanningChainCoordinator` in DI**
|
||||||
|
|
||||||
|
Locate `src/ClaudeDo.Worker/Program.cs` where other services are registered (look for `services.AddSingleton<PlanningSessionManager>` or similar). Add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddScoped<PlanningChainCoordinator>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `AddScoped` if `TaskRepository` is scoped (check how it's registered — match its lifetime). If `TaskRepository` is constructed ad-hoc inside the worker, add a constructor overload on `PlanningChainCoordinator` that takes `IDbContextFactory<ClaudeDoDbContext>` and builds its own `TaskRepository` per call, then register as Singleton. Mirror the pattern used by `PlanningSessionManager`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Inject coordinator into `TaskRunner`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add `PlanningChainCoordinator` to the constructor parameter list and store it in a readonly field (match the style used for `_broadcaster`).
|
||||||
|
|
||||||
|
If `TaskRunner` is not a good fit for direct injection (e.g., it's used in contexts without DI), instead inject `IServiceProvider` / `IDbContextFactory<ClaudeDoDbContext>` and new-up a coordinator inside the finish handler. Pick whichever matches existing `TaskRunner` patterns.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Call coordinator after Done/Failed emission**
|
||||||
|
|
||||||
|
Immediately after each `await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);` on line ~338 and the two failed emissions on lines ~355 and ~372, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (task.ParentTaskId is not null)
|
||||||
|
{
|
||||||
|
var advancedId = await _chainCoordinator.OnChildFinishedAsync(
|
||||||
|
task.Id,
|
||||||
|
/* Done or Failed based on path */,
|
||||||
|
CancellationToken.None);
|
||||||
|
if (advancedId is not null)
|
||||||
|
await _broadcaster.TaskUpdated(advancedId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `TaskStatus.Done` in the done-path call site and `TaskStatus.Failed` in the failed-path call sites. For the failed paths that use `justFailed` rather than `task`, read `justFailed?.ParentTaskId` and `justFailed?.Id` to stay consistent with the surrounding code.
|
||||||
|
|
||||||
|
After this call the existing queue-pickup loop will see the newly-Queued sibling and dispatch it on its next tick.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run full test suite**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pre-existing tests + 3 new ones pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs \
|
||||||
|
src/ClaudeDo.Worker/Program.cs
|
||||||
|
git commit -m "feat(worker): advance planning subtask chain on child finish"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Hub method + client + context menu entry
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/IWorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add hub method**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, add (match the style of other planning methods):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task QueuePlanningSubtasks(string parentTaskId)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var repo = new TaskRepository(ctx);
|
||||||
|
var coord = new PlanningChainCoordinator(repo);
|
||||||
|
await coord.QueueSubtasksSequentiallyAsync(parentTaskId, CancellationToken.None);
|
||||||
|
|
||||||
|
// Broadcast updates for the parent and all its children so the UI refreshes.
|
||||||
|
var children = await ctx.Tasks
|
||||||
|
.Where(t => t.ParentTaskId == parentTaskId)
|
||||||
|
.Select(t => t.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
await _broadcaster.TaskUpdated(parentTaskId);
|
||||||
|
foreach (var id in children)
|
||||||
|
await _broadcaster.TaskUpdated(id);
|
||||||
|
|
||||||
|
// Make sure the queue picks up the now-Queued first child immediately.
|
||||||
|
_queueSignal.Wake();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the existing hub constructs `PlanningSessionManager` via DI directly, inject `PlanningChainCoordinator` the same way and call `_chainCoordinator.QueueSubtasksSequentiallyAsync(...)` instead of newing one up. If the hub exposes a queue-wakeup via a different name than `_queueSignal`, use that (search the file for `WakeQueue` or `.Wake()`).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add method to `IWorkerClient`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/IWorkerClient.cs`, add next to the other planning methods:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement in `WorkerClient`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add (match the pattern of `StartPlanningSessionAsync` etc.):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) =>
|
||||||
|
_connection.InvokeAsync("QueuePlanningSubtasks", parentTaskId, ct);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add `CanQueueSubtasksSequentially` + `HasPlanningChildren` observable to `TaskRowViewModel`**
|
||||||
|
|
||||||
|
Confirm `HasPlanningChildren` exists (it's referenced in the spec). If not, add it as `[ObservableProperty] bool _hasPlanningChildren;` and ensure `TasksIslandViewModel.Regroup()` already sets it (there should be a parent-side "has children" pass similar to the `AllChildrenDone` one added in Task 4 — if not, set it there).
|
||||||
|
|
||||||
|
Then add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool CanQueueSubtasksSequentially =>
|
||||||
|
IsPlanningParent && HasPlanningChildren && !IsChild;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `OnPropertyChanged(nameof(CanQueueSubtasksSequentially))` inside `OnStatusChanged` and `OnHasPlanningChildrenChanged` so the flag refreshes when status or children change.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add context-menu entry**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, inside the existing `<ContextMenu>`, directly after the "Discard planning session" item:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Separator IsVisible="{Binding CanQueueSubtasksSequentially}"/>
|
||||||
|
<MenuItem Header="Queue subtasks sequentially"
|
||||||
|
IsVisible="{Binding CanQueueSubtasksSequentially}"
|
||||||
|
Click="OnQueueSubtasksSequentiallyClick"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Add click handler in code-behind**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs`, add (match the other `On*Click` handlers — they pull the `TaskRowViewModel` from `DataContext` and call the shell / worker):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async void OnQueueSubtasksSequentiallyClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not TaskRowViewModel row) return;
|
||||||
|
var worker = App.Services.GetRequiredService<IWorkerClient>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await worker.QueuePlanningSubtasksAsync(row.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Match the toast/log pattern used by OnSendToQueueClick et al.
|
||||||
|
System.Diagnostics.Debug.WriteLine($"QueuePlanningSubtasks failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the same `App.Services` / `IWorkerClient` lookup pattern as `OnSendToQueueClick` — do not introduce a new DI pattern. If the existing handlers use a shell/mediator indirection, use that instead.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Manual verify end-to-end**
|
||||||
|
|
||||||
|
1. Launch app: `dotnet run --project src/ClaudeDo.App`.
|
||||||
|
2. Open a planning task with ≥2 subtasks (all in `Manual`/`Planned`).
|
||||||
|
3. Right-click parent → **Queue subtasks sequentially**.
|
||||||
|
4. Confirm in the task list: first child shows `Queued` chip, others show `Waiting` chip.
|
||||||
|
5. Let the first run to completion (or, for a quick smoke test, edit the DB to mark it `Done` and emit `TaskUpdated` via a restart).
|
||||||
|
6. Confirm the next child's status flips `Waiting → Queued` without user interaction.
|
||||||
|
7. Force-fail a child (cancel it mid-run) — confirm remaining `Waiting` children stay `Waiting`.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs \
|
||||||
|
src/ClaudeDo.Ui/Services/IWorkerClient.cs \
|
||||||
|
src/ClaudeDo.Ui/Services/WorkerClient.cs \
|
||||||
|
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs \
|
||||||
|
src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml \
|
||||||
|
src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs
|
||||||
|
git commit -m "feat(ui+worker): context menu to queue planning subtasks sequentially"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review checklist (for the plan author before handing off)
|
||||||
|
|
||||||
|
- All four spec items mapped: auto-collapse (Task 4), collapsible description (Task 3), narrower splitters (Task 2), sequential subtask queue (Tasks 1, 5, 6, 7).
|
||||||
|
- `Waiting` enum touches: enum, chip class, virtual:queued filter — covered in Task 1.
|
||||||
|
- TDD applied where it pays off (the coordinator); UI tasks rely on manual verification (correct for this codebase).
|
||||||
|
- No placeholders. Every code step shows the code to paste.
|
||||||
|
- Type names consistent: `PlanningChainCoordinator`, `QueueSubtasksSequentiallyAsync`, `OnChildFinishedAsync`, `QueuePlanningSubtasksAsync`, `AreChildrenExpanded`, `AllChildrenDone`, `IsDescriptionExpanded` — used the same across tasks.
|
||||||
|
- Commits are small and conventional.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Planning UX Polish + Sequential Subtask Queue
|
||||||
|
|
||||||
|
**Status:** design
|
||||||
|
**Date:** 2026-04-24
|
||||||
|
**Scope:** three small UX changes + one feature — sequential execution of planning subtasks triggered from the context menu.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. Collapse the children of a finished planning-parent row in the task list by default.
|
||||||
|
2. Allow the user to collapse the Description section in the Details pane.
|
||||||
|
3. Halve the width of the GridSplitters between islands.
|
||||||
|
4. Let the user queue all subtasks of a planning parent so they run one after another, with a new `Waiting` status for pending siblings.
|
||||||
|
|
||||||
|
## 1. Auto-collapse done planning parents
|
||||||
|
|
||||||
|
**Rule for "done":** a planning parent is "done" when every one of its children has `Status == Done`.
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- `TaskRowViewModel`: add UI-only `[ObservableProperty] bool _areChildrenExpanded`. Default computed from status — `false` when the row is a done planning parent, else `true`. Not persisted.
|
||||||
|
- Add `[RelayCommand] void ToggleChildrenExpanded()`.
|
||||||
|
- `TasksIslandView.axaml` (or `TaskRowView.axaml`): chevron button on the planning-parent row, visible only when `IsPlanningParent && HasPlanningChildren`. Bound to the toggle command.
|
||||||
|
- `TasksIslandViewModel.Regroup()`: before adding child rows to `OpenItems`/`CompletedItems`, check each child's parent row in `Items`. If the parent's `AreChildrenExpanded == false`, skip the child.
|
||||||
|
- When a planning parent flips from "not done" → "done" in `OnWorkerTaskUpdated`, call `Regroup()` so the collapse takes effect.
|
||||||
|
|
||||||
|
No DB changes.
|
||||||
|
|
||||||
|
## 2. Collapsible description in Details pane
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- `DetailsIslandViewModel`: `[ObservableProperty] bool _isDescriptionExpanded = true` + `[RelayCommand] void ToggleDescriptionExpanded()`.
|
||||||
|
- `DetailsIslandView.axaml`: wrap the existing description `TextBox` in a `StackPanel`; add a thin header row with the label "Description" and a chevron button. Body's `IsVisible` binds to the flag.
|
||||||
|
- State is per ViewModel instance — reset to `true` whenever a different task is loaded.
|
||||||
|
|
||||||
|
No persistence.
|
||||||
|
|
||||||
|
## 3. Narrower GridSplitters
|
||||||
|
|
||||||
|
`MainWindow.axaml` lines 158 and 170: `Width="5"` → `Width="3"` on both `GridSplitter` elements.
|
||||||
|
|
||||||
|
That's the whole change.
|
||||||
|
|
||||||
|
## 4. Sequential subtask queue
|
||||||
|
|
||||||
|
### Data
|
||||||
|
|
||||||
|
- `ClaudeDo.Data/Models/TaskStatus.cs`: add a new enum value `Waiting` (lowercase serialized form `waiting`, matching existing convention).
|
||||||
|
- Verify status is stored as string (it should be based on existing patterns). If stored as int, ensure new value gets a stable numeric slot at the end of the enum to avoid breaking existing rows. **No EF migration** beyond what the enum emits automatically.
|
||||||
|
|
||||||
|
### Worker
|
||||||
|
|
||||||
|
- New SignalR hub method: `QueuePlanningSubtasksAsync(string parentTaskId) : Task`.
|
||||||
|
- Loads all children of the parent, ordered by `SortOrder`.
|
||||||
|
- Validates: parent must be a planning parent, children must currently all be in `Manual` or `Planned` (reject if any child is already Queued/Running/Done/Failed, surface a friendly error).
|
||||||
|
- First child → `Queued`. All other children → `Waiting`. Save.
|
||||||
|
- Emit `TaskUpdated` for each affected task.
|
||||||
|
- Chain progression — hook into the existing finish/complete path that already fires `TaskFinished`:
|
||||||
|
- On a child task finishing with status `Done` **and** its parent has waiting siblings: find the next sibling by `(ParentTaskId == parent.Id && Status == Waiting)` ordered by `SortOrder`, flip to `Queued`, emit `TaskUpdated`, and let the existing queue pickup loop pick it up.
|
||||||
|
- On `Failed`: do nothing. Remaining `Waiting` siblings stay waiting. (A toast for failed tasks will be added in a later spec.)
|
||||||
|
|
||||||
|
This logic lives in a new `PlanningChainCoordinator` service (or similar) in `ClaudeDo.Worker/Planning/`, registered as a singleton and wired into whatever already emits task-finished events.
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
- `TaskRowView` — add context menu entry **"Queue subtasks sequentially"**:
|
||||||
|
- `IsVisible` bound to `IsPlanningParent && HasPlanningChildren`.
|
||||||
|
- `IsEnabled` when all children are in `Manual` / `Planned` state (new property on `TaskRowViewModel`: `CanQueueSubtasksSequentially`).
|
||||||
|
- Calls `WorkerClient.QueuePlanningSubtasksAsync(Id)`.
|
||||||
|
- `TaskRowViewModel`:
|
||||||
|
- Add `IsWaiting => Status == TaskStatus.Waiting` and extend `StatusChipClass` switch to return a new class `"waiting"`.
|
||||||
|
- Add `CanQueueSubtasksSequentially` (computed; requires access to children).
|
||||||
|
- `StatusColorConverter` — add a muted color for `Waiting` (proposed: the existing `TextMuteBrush` or a faint cyan).
|
||||||
|
- Task list — planning parent continues to appear in virtual:queued because it has a `Queued` child (existing logic). **Extend** the virtual:queued match predicate in `TasksIslandViewModel.TaskMatchesList` so a task matches when `Status == Queued || Status == Waiting`. This ensures all sibling subtasks (the queued one + the waiting ones) render under the parent in that list.
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
- `IWorkerClient` / `WorkerClient`: add `QueuePlanningSubtasksAsync(string parentTaskId)` that calls the hub method.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Toast notifications on subtask failure (separate follow-up spec).
|
||||||
|
- Retrying a stopped chain from a failed task (user does it manually via existing actions).
|
||||||
|
- Persisting the collapse state of planning parents or the Description across sessions.
|
||||||
|
- Drag-to-reorder of waiting subtasks (execution order = `SortOrder` at the moment the chain starts).
|
||||||
|
|
||||||
|
## Validation plan
|
||||||
|
|
||||||
|
Manual:
|
||||||
|
- Plan a task with 3 subtasks. Context-menu → Queue subtasks sequentially. Confirm first = Queued, others = Waiting. Watch the first run to Done, confirm the second flips Queued → Running automatically.
|
||||||
|
- Force-fail subtask 2 (cancel or make it fail). Confirm subtask 3 stays Waiting; no further dispatch.
|
||||||
|
- Once all three are Done, confirm the planning parent auto-collapses in the list.
|
||||||
|
- Toggle the Description chevron in the Details pane on an arbitrary task.
|
||||||
|
- Eyeball the narrower GridSplitter — still resizable, still hittable.
|
||||||
|
|
||||||
|
Automated (minimal — only where cheap):
|
||||||
|
- Worker-level unit test for `PlanningChainCoordinator`: happy-path chain advance on Done; no advance on Failed; correct ordering by `SortOrder`.
|
||||||
Reference in New Issue
Block a user