diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..3407af2 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(git add:*)", + "Bash(git commit:*)" + ] + } +} diff --git a/.superpowers/brainstorm/1955-1776152447/content/color-green-teal.html b/.superpowers/brainstorm/1955-1776152447/content/color-green-teal.html new file mode 100644 index 0000000..b7e22ca --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/content/color-green-teal.html @@ -0,0 +1,106 @@ +

Green-Teal Variations

+

Steel Teal shifted greener. Pick the one that feels right.

+ +
+
+
+
+
+ + My Project +
+
+
+
+
Fix login bug
+
agent
+
+
+
+
+ +
+
+
Setup CI
+
Done
+
+
+
+ + Add a task... +
+
+
+
+

Forest Teal

+

Accent: #3d9474. Distinctly greener, still muted. Earthy.

+
+
+ +
+
+
+
+ + My Project +
+
+
+
+
Fix login bug
+
agent
+
+
+
+
+ +
+
+
Setup CI
+
Done
+
+
+
+ + Add a task... +
+
+
+
+

Jade

+

Accent: #4a9880. Balanced green-teal midpoint. Calm but not cold.

+
+
+ +
+
+
+
+ + My Project +
+
+
+
+
Fix login bug
+
agent
+
+
+
+
+ +
+
+
Setup CI
+
Done
+
+
+
+ + Add a task... +
+
+
+
+

Sage

+

Accent: #5a9a7a. Most green of the three. Softer, natural tone.

+
+
+
diff --git a/.superpowers/brainstorm/1955-1776152447/content/color-theme.html b/.superpowers/brainstorm/1955-1776152447/content/color-theme.html new file mode 100644 index 0000000..2408cee --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/content/color-theme.html @@ -0,0 +1,107 @@ +

Accent Color: Which tone?

+

You want something dimmer than the indigo (#6366f1) I showed. Here are darker, more muted options — each shown on a task list mockup.

+ +
+
+
+
+ +
+ + My Project +
+ +
+
+
+
Fix login bug
+
agent
+
+
+ +
+ + Add a task... +
+
+
+
+

Slate Blue

+

Muted blue-gray. Accent: #4b5ea8. Very subdued, professional. Close to VS Code's dark theme feel.

+
+
+ +
+
+
+
+ + My Project +
+
+
+
+
Fix login bug
+
agent
+
+
+
+ + Add a task... +
+
+
+
+

Dim Violet

+

Muted purple. Accent: #7c6aad. Slightly warmer, still understated. Has a subtle "Claude" vibe.

+
+
+ +
+
+
+
+ + My Project +
+
+
+
+
Fix login bug
+
agent
+
+
+
+ + Add a task... +
+
+
+
+

Steel Teal

+

Muted teal-green. Accent: #4a8c8c. Cool and calm. Distinct from typical blue-heavy dark UIs.

+
+
+ +
+
+
+
+ + My Project +
+
+
+
+
Fix login bug
+
agent
+
+
+
+ + Add a task... +
+
+
+
+

Charcoal Blue

+

Desaturated steel blue. Accent: #5571a1. Very close to Microsoft To Do's dark mode accent but dimmer.

+
+
+
diff --git a/.superpowers/brainstorm/1955-1776152447/content/layout-design.html b/.superpowers/brainstorm/1955-1776152447/content/layout-design.html new file mode 100644 index 0000000..19c82de --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/content/layout-design.html @@ -0,0 +1,169 @@ +

Layout & Visual Design Direction

+

Your current UI has button toolbars and minimal spacing. Which direction should we take?

+ +
+
+
Current: Toolbar Style
+
+
+ +
+
Lists
+
+
My Project
+
Backend Work
+
UI Polish
+
+
+ + + +
+
+ +
+
Tasks
+
+
+
+
Fix login bug
+
agent
+
+
+ Running + +
+
+
+
Add dark mode
+
+ Manual + +
+
+
+
+ + + +
+
+
+
+
+ +
+
Proposed: To Do Style
+
+
+ +
+
Lists
+
+
+ + My Project +
+
+ + Backend Work +
+
+ + UI Polish +
+
+
+
+ + New List +
+
+
+ +
+
My Project
+
+
+
+
+
+
+
Fix login bug
+
agent · Running
+
+
+
+
+
+
Add dark mode
+
manual
+
+
+
+
+ +
+
+
Setup CI pipeline
+
agent · Done
+
+
+
+ +
+
+ + Add a task... +
+
+
+
+
+
+
+ +

Key Changes in the Proposed Design

+
+
+
1
+
+

Circular Checkboxes

+

Replace status badges with circular checkboxes on the left. Border color reflects status (orange=running, green=done, gray=manual). Click to toggle done.

+
+
+
+
2
+
+

Inline "Add a task" Input

+

Dashed border text field pinned at the bottom of the task list. Always visible. Enter to create, Escape to cancel.

+
+
+
+
3
+
+

List Name as Tasks Header

+

Replace generic "Tasks" header with the selected list name in larger text. Matches To Do's pattern.

+
+
+
+
4
+
+

Sidebar Polish

+

Colored dots per list, subtle highlight on selected, "+ New List" link at bottom instead of +/E/- buttons.

+
+
+
+
5
+
+

Remove Button Toolbars

+

Eliminate the bottom button bars from both panes. All actions via context menu, keyboard shortcuts, or inline controls.

+
+
+
+
6
+
+

Completed Task Styling

+

Done tasks get strikethrough text, reduced opacity, green checkmark. Keeps them visible but visually subordinate.

+
+
+
+

This is multi-select — pick all the changes you'd like to include. I recommend all 6.

diff --git a/.superpowers/brainstorm/1955-1776152447/content/task-creation-flow.html b/.superpowers/brainstorm/1955-1776152447/content/task-creation-flow.html new file mode 100644 index 0000000..b26220e --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/content/task-creation-flow.html @@ -0,0 +1,61 @@ +

Task Creation: How should adding tasks work?

+

Microsoft To Do uses an inline text field at the bottom of the task list. Currently ClaudeDo opens a modal dialog. Which approach fits best?

+ +
+
+
A
+
+

Inline Add (To Do style)

+

A text field always visible at the bottom of the task list. Press Enter to create a quick task with just a title. Tab or click to expand for more fields (description, tags, status).

+
+

Pros

    +
  • Fastest for rapid task entry
  • +
  • Keyboard-driven — never leave the list
  • +
  • Feels natural and lightweight
  • +
+

Cons

    +
  • Limited space for advanced fields
  • +
  • Need separate flow for setting tags/status on creation
  • +
+
+
+
+ +
+
B
+
+

Inline Add + Detail Pane Editing

+

Same inline text field for quick creation. After pressing Enter, the new task is selected and the detail pane on the right becomes editable — add description, tags, commit type there. Like To Do's "click task → edit in sidebar" pattern.

+
+

Pros

    +
  • Quick entry AND full editing without modals
  • +
  • Uses existing detail pane real estate
  • +
  • Closest to Microsoft To Do's actual flow
  • +
+

Cons

    +
  • Detail pane needs to become editable (currently read-only)
  • +
  • More complex state management
  • +
+
+
+
+ +
+
C
+
+

Keep Modal Dialog + Keyboard Shortcut

+

Keep the existing modal editor but add Ctrl+N / Enter shortcut to open it instantly. Add keyboard navigation within the dialog (Tab between fields, Enter to save).

+
+

Pros

    +
  • Minimal code changes
  • +
  • All fields visible at once
  • +
  • Modal keeps focus clear
  • +
+

Cons

    +
  • Still interrupts flow with a window
  • +
  • Feels heavier than To Do
  • +
+
+
+
+
diff --git a/.superpowers/brainstorm/1955-1776152447/content/waiting.html b/.superpowers/brainstorm/1955-1776152447/content/waiting.html new file mode 100644 index 0000000..ef07652 --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/content/waiting.html @@ -0,0 +1,3 @@ +
+

Continuing in terminal...

+
diff --git a/.superpowers/brainstorm/1955-1776152447/state/server-stopped b/.superpowers/brainstorm/1955-1776152447/state/server-stopped new file mode 100644 index 0000000..00b0c44 --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1776154800894} diff --git a/.superpowers/brainstorm/1955-1776152447/state/server.pid b/.superpowers/brainstorm/1955-1776152447/state/server.pid new file mode 100644 index 0000000..141d29f --- /dev/null +++ b/.superpowers/brainstorm/1955-1776152447/state/server.pid @@ -0,0 +1 @@ +1955 diff --git a/.superpowers/brainstorm/3761-1776156321/content/island-colors.html b/.superpowers/brainstorm/3761-1776156321/content/island-colors.html new file mode 100644 index 0000000..c8dec88 --- /dev/null +++ b/.superpowers/brainstorm/3761-1776156321/content/island-colors.html @@ -0,0 +1,79 @@ +

Island-Farben: weniger grün

+

Gleiche Struktur, neutralere Grautöne mit nur einem Hauch Grün

+ +
+
+
+
+
+
Lists
+
My Project
+
+
+
My Project
+
+
+ Fix login bug +
+
+
+
Detail
+
+
+
+
+

Neutral Slate

+

Base: #1b1e23 · Islands: #252a30
Fast kein Grün — kühl, neutral, wie VS Code Dark+

+
+
+ +
+
+
+
+
Lists
+
My Project
+
+
+
My Project
+
+
+ Fix login bug +
+
+
+
Detail
+
+
+
+
+

Warm Charcoal

+

Base: #1c1e21 · Islands: #272a2e
Minimal warm, komplett neutral. Wie Rider's New UI Dark.

+
+
+ +
+
+
+
+
Lists
+
My Project
+
+
+
My Project
+
+
+ Fix login bug +
+
+
+
Detail
+
+
+
+
+

Subtle Tint

+

Base: #1b1f22 · Islands: #262b2d
Ganz leichter kühler Ton — zwischen den anderen beiden

+
+
+
diff --git a/.superpowers/brainstorm/3761-1776156321/content/island-layout.html b/.superpowers/brainstorm/3761-1776156321/content/island-layout.html new file mode 100644 index 0000000..c7ba7fe --- /dev/null +++ b/.superpowers/brainstorm/3761-1776156321/content/island-layout.html @@ -0,0 +1,77 @@ +

Island Layout (Rider-Style)

+

Dark greenish-gray base, rounded card panels floating on top

+ +
+
+
Current: Flat columns, black gaps
+
+
+
+
Lists
+
My Project
+
Backend
+
+
+
+
My Project
+
Fix login bug
+
+
+
+
Detail
+
+
+
+
+ +
+
Proposed: Floating islands on tinted base
+
+
+
+
Lists
+
My Project
+
Backend
+
+
+
My Project
+
+
+
+
Fix login bug
+
agent · manual
+
+
+
+
+ +
+
+
Setup CI
+
done
+
+
+
+
+
Fix login bug
+
Status
+
Manual
+
Tags
+
+ agent +
+
+
+
+
+
+ +

Die Änderungen

+ diff --git a/.superpowers/brainstorm/3761-1776156321/state/server-stopped b/.superpowers/brainstorm/3761-1776156321/state/server-stopped new file mode 100644 index 0000000..429ce2f --- /dev/null +++ b/.superpowers/brainstorm/3761-1776156321/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1776158304159} diff --git a/.superpowers/brainstorm/3761-1776156321/state/server.pid b/.superpowers/brainstorm/3761-1776156321/state/server.pid new file mode 100644 index 0000000..1a4b4d0 --- /dev/null +++ b/.superpowers/brainstorm/3761-1776156321/state/server.pid @@ -0,0 +1 @@ +3761 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1e8a0c4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# ClaudeDo + +A desktop task management app that executes tasks autonomously via Claude CLI in isolated git worktrees. + +## Architecture + +Two-process system communicating over SignalR (`127.0.0.1:47821`): + +- **ClaudeDo.App** — Avalonia desktop entry point, DI container setup +- **ClaudeDo.Ui** — Views, ViewModels, SignalR client (MVVM with CommunityToolkit.Mvvm) +- **ClaudeDo.Data** — SQLite data layer, repositories, models, GitService +- **ClaudeDo.Worker** — ASP.NET Core hosted service, task queue, Claude CLI runner +- **ClaudeDo.Worker.Tests** — xUnit integration tests with real SQLite and real git + +## Tech Stack + +- .NET 8.0, Avalonia 12.0.0 (Fluent theme) +- SQLite (WAL mode) via Microsoft.Data.Sqlite — raw ADO.NET, no ORM +- SignalR for real-time IPC +- CommunityToolkit.Mvvm (`[ObservableProperty]`, `[RelayCommand]`) +- Git worktrees for task isolation + +## Key Paths + +- DB: `~/.todo-app/todo.db` +- UI config: `~/.todo-app/ui.config.json` +- Worker config: `~/.todo-app/worker.config.json` +- Logs: `~/.todo-app/logs/` +- Worktrees: configured per worker (sibling or central strategy) +- Schema: `schema/schema.sql` (embedded resource in ClaudeDo.Data) + +## Conventions + +- Repository pattern — each entity has its own async repository +- All data operations are async with CancellationToken support +- Task status flow: Manual | Queued -> Running -> Done | Failed +- Worktree state flow: Active -> Merged | Discarded | Kept +- Tags "agent" and "manual" are seeded; "agent" tag marks tasks for automated queue pickup +- Commit messages use conventional format: `{commitType}(slug): title` +- Views use compiled bindings (`x:DataType`) +- ViewModels use `[ObservableProperty]` and `[RelayCommand]` source generators + +## Building & Testing + +```bash +dotnet build ClaudeDo.slnx +dotnet test tests/ClaudeDo.Worker.Tests +``` + +## Docs + +- `docs/plan.md` — full architecture and design spec +- `docs/open.md` — verification checklist and improvement backlog +- `docs/improvement-plan.md` — prioritized improvement items diff --git a/ClaudeDo.slnx b/ClaudeDo.slnx index e680914..785aab8 100644 --- a/ClaudeDo.slnx +++ b/ClaudeDo.slnx @@ -6,6 +6,7 @@ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7b36be --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# ClaudeDo + +A desktop task management app that executes tasks autonomously via [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) in isolated git worktrees. + +Queue up coding tasks, and ClaudeDo picks them up one by one — each running in its own worktree so your main branch stays clean. + +## Architecture + +Two-process system communicating over SignalR: + +| Project | Role | +|---|---| +| **ClaudeDo.App** | Avalonia desktop entry point, DI container setup | +| **ClaudeDo.Ui** | Views, ViewModels, SignalR client (MVVM) | +| **ClaudeDo.Data** | SQLite data layer, repositories, models, GitService | +| **ClaudeDo.Worker** | ASP.NET Core hosted service, task queue, Claude CLI runner | + +``` +┌──────────────┐ SignalR ┌──────────────┐ +│ ClaudeDo.App│◄──────────►│ClaudeDo.Worker│ +│ (Avalonia) │ 127.0.0.1 │ (ASP.NET) │ +│ │ :47821 │ │ +│ ┌──────────┐│ │ ┌──────────┐ │ +│ │ Ui ││ │ │ TaskQueue│ │ +│ │(ViewModels)│ │ │ Claude CLI│ │ +│ └──────────┘│ │ └──────────┘ │ +└──────┬───────┘ └──────┬───────┘ + │ │ + └───────────┬───────────────┘ + │ + ┌──────┴──────┐ + │ ClaudeDo.Data│ + │ (SQLite) │ + └─────────────┘ +``` + +## Tech Stack + +- .NET 8.0 +- Avalonia 12.0.0 (Fluent theme) +- SQLite (WAL mode) via Microsoft.Data.Sqlite — raw ADO.NET, no ORM +- SignalR for real-time IPC between UI and Worker +- CommunityToolkit.Mvvm for source-generated MVVM +- Git worktrees for task isolation + +## Prerequisites + +- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated +- Git + +## Getting Started + +```bash +# Build +dotnet build ClaudeDo.slnx + +# Run tests +dotnet test tests/ClaudeDo.Worker.Tests + +# Run the app +dotnet run --project src/ClaudeDo.App +``` + +## How It Works + +1. Create a task in the UI and tag it with **"agent"** to mark it for automated execution. +2. The Worker picks up queued tasks and runs each one via Claude CLI in an isolated git worktree. +3. When done, the worktree can be merged, kept for review, or discarded. + +**Task status flow:** `Manual | Queued → Running → Done | Failed` + +**Worktree state flow:** `Active → Merged | Discarded | Kept` + +## Configuration + +All data and config lives under `~/.todo-app/`: + +| File | Purpose | +|---|---| +| `todo.db` | SQLite database | +| `ui.config.json` | UI settings | +| `worker.config.json` | Worker settings (worktree strategy, etc.) | +| `logs/` | Application logs | + +## Project Structure + +``` +ClaudeDo.slnx +├── src/ +│ ├── ClaudeDo.App/ # Desktop entry point +│ ├── ClaudeDo.Ui/ # Views & ViewModels +│ ├── ClaudeDo.Data/ # Data access layer +│ └── ClaudeDo.Worker/ # Background task runner +├── tests/ +│ └── ClaudeDo.Worker.Tests/ +├── schema/ +│ └── schema.sql # Database schema +└── docs/ + ├── plan.md # Architecture & design spec + ├── open.md # Verification checklist & backlog + └── improvement-plan.md # Prioritized improvements +``` + +## License + +Private — not licensed for redistribution. diff --git a/docs/improvement-plan.md b/docs/improvement-plan.md new file mode 100644 index 0000000..261e8f3 --- /dev/null +++ b/docs/improvement-plan.md @@ -0,0 +1,92 @@ +# ClaudeDo — Improvement Plan (Session 2026-04-13) + +Erfasst während manuellem Walkthrough der App. Priorisiert nach Schmerz/Aufwand. + +--- + +## P1 — UX-Blocker (sollten zuerst) + +### IP-1: UI ↔ Worker Auto-Reconnect +**Symptom:** Wenn UI vor Worker startet, bleibt die Verbindung tot. Manueller UI-Restart nötig. +**Soll:** SignalR-Client mit `WithAutomaticReconnect()` + Reconnect-Versuche im Hintergrund (exponential backoff). Status-Bar zeigt "verbinde…" während Retry. +**Dateien:** `src/ClaudeDo.Ui/Services/WorkerClient.cs` (oder wo `HubConnection` gebaut wird) +**Aufwand:** klein (~30 Zeilen, primär `HubConnectionBuilder`-Konfig + Reconnect-Handler) +**Risiko:** klein + +### IP-2: Listen-Modus „Notes" (non-autonomous) +**Symptom:** Jede Liste ist Agent-gesteuert. Keine reine Notiz-Liste möglich. +**Soll:** Neues Feld `lists.kind` (`agent` | `notes`). +- `agent`: aktuelles Verhalten (Worker pickt Tasks) +- `notes`: Worker ignoriert die Liste komplett, UI versteckt Run-/Schedule-/Worktree-Felder, Tasks haben nur Title + Description + done-Checkbox. +**Dateien:** +- Schema: neue Spalte + Migration (siehe IP-9) +- `Data/Entities/TaskList.cs`, `Repositories/ListRepository.cs` +- `Worker/Queue/QueueService.cs` (Filter `WHERE list.kind = 'agent'`) +- UI: `ListEditorView` (Radio/ComboBox), `TaskListView` (conditional Columns), `TaskDetailView` (verstecken) +**Aufwand:** mittel (~Schema + Repo + UI an mehreren Stellen) +**Risiko:** mittel — bestehende Listen müssen Default `agent` bekommen + +### IP-3: Doppelklick öffnet Edit-Dialog +**Symptom:** Edit nur über separaten Button/Menüpunkt. +**Soll:** `DoubleTapped`-Handler auf ListBox-Items (Listen-Pane) und auf TaskRows (Task-Pane) → öffnet jeweiligen Editor. +**Dateien:** `Views/MainWindow.axaml(.cs)`, `Views/TaskListView.axaml(.cs)` +**Aufwand:** klein (~10–15 Zeilen pro Stelle) +**Risiko:** klein + +### IP-4: Tag-Multi-Select statt Freitext +**Symptom:** Tags müssen getippt werden, keine Auto-Vervollständigung, Typos möglich. +**Soll:** Multi-Select-Control: +- Zeigt alle in DB existierenden Tags (DISTINCT aus `lists.tags` ∪ `tasks.tags`) +- Erlaubt Anlegen neuer Tags (Free-Text-Add) +- Chip/Token-Darstellung der ausgewählten Tags +**Dateien:** +- *neu* `Views/Controls/TagPicker.axaml` (wiederverwendbar) +- `ListEditorView`, `TaskEditorView` einbinden +- Repo-Methode `GetAllKnownTagsAsync()` +**Aufwand:** mittel (Custom-Control lohnt sich, da 2× verwendet) +**Risiko:** klein + +### IP-5: Rechtsklick-Kontextmenü +**Symptom:** Quick-Actions nur über Buttons im Detail-Pane oder Toolbar. +**Soll:** +- **Liste:** Edit, Delete, New Task, ggf. „Mark all done" (für Notes-Listen aus IP-2) +- **Task:** Edit, Delete, Run Now, Show Diff, Merge, Cancel (je nach Status) +- Items kontext-sensitiv enabled/disabled je nach Task-Status & List-Kind +**Dateien:** `Views/MainWindow.axaml` (List-Pane), `Views/TaskListView.axaml` (Task-Pane) +**Aufwand:** klein–mittel — Avalonia `ContextMenu` + Command-Bindings +**Risiko:** klein + +--- + +## P2 — Folge-Arbeiten (durch P1 ausgelöst) + +### IP-6: Schema-Migration-Mechanismus +**Trigger:** IP-2 fügt eine Spalte zu `lists` hinzu. Aktuell `schema.sql` ist Drop-and-Create-Style. +**Soll:** Mini-Migrations-System: `migrations/0001_initial.sql`, `0002_lists_kind.sql`, … + `_schema_version` Tabelle. +**Aufwand:** klein–mittel +**Querverweis:** `open.md` Sektion 7 (Schulden-Tabelle: „Embedded schema.sql ohne Versionierung") + +### IP-7: Status-Bar zeigt Reconnect-State +**Trigger:** IP-1 — User soll sehen, dass Verbindung gerade aufgebaut wird (statt nur „offline"). +**Soll:** States: `connected` | `connecting` | `reconnecting` | `offline`. Farb-codiert. +**Datei:** `ViewModels/StatusBarViewModel.cs` +**Aufwand:** klein + +### IP-8: Tag-Repository für `GetAllKnownTagsAsync` +**Trigger:** IP-4 braucht eine Quelle aller bekannten Tags. +**Soll:** Methode in `ListRepository`/`TaskRepository` ODER neuer `TagRepository`. SQL: `SELECT DISTINCT trim(value) FROM lists, json_each(lists.tags) UNION ...`. +**Aufwand:** klein + +--- + +## Empfohlene Reihenfolge + +1. **IP-1** (Auto-Reconnect) — sofortiger UX-Win, isoliert, klein +2. **IP-3** (Doppelklick) — trivial, sofort spürbar +3. **IP-5** (Kontextmenü) — kompakt, hebt Bedienkomfort deutlich +4. **IP-6** (Migrations) — Voraussetzung für IP-2 +5. **IP-2** (Notes-Mode) — größerer Brocken, braucht Schema-Migration +6. **IP-8 → IP-4** (Tag-Repo, dann Multi-Select-Control) +7. **IP-7** (Reconnect-Status in StatusBar) — Polish nach IP-1 + +Block 1 (IP-1, IP-3, IP-5) ist ein realistischer Session-Block. diff --git a/docs/open.md b/docs/open.md new file mode 100644 index 0000000..06cb836 --- /dev/null +++ b/docs/open.md @@ -0,0 +1,193 @@ +# ClaudeDo — Offene Punkte + +Stand: 2026-04-13 nach Slice F. Branch `main` @ `48e4aab`. Alle Tests grün (38/38), Build 0 Warnings. + +Dieses Dokument listet alles, was noch fehlt — gruppiert nach Aufwand/Risiko und mit konkreten Datei-Pointern, damit wir es in der IDE der Reihe nach durchgehen können. + +--- + +## 1. Verification (vor allem anderen) + +Die in `plan.md` definierten Verification-Steps sind teilweise nur durch Build/Tests abgedeckt. Diese sollten manuell einmal durchlaufen werden, BEVOR wir Polish bauen — damit wir wissen, was tatsächlich kaputt ist. + +| # | Plan | Status | Was tun | +|---|------|--------|---------| +| 1 | Schema-Init | Auto verifiziert (Worker startet ohne Crash, WAL-Files entstehen) | OK | +| 1a | SignalR-Endpoint | Manuell verifiziert (HTTP 400 auf `/hub` ohne Handshake) | OK | +| 1b | Hub-Roundtrip `Ping` | **Nicht getestet** | Test-Client schreiben oder UI starten und im Log nach "pong" schauen | +| 2 | `claude --version` Preflight | **Nicht implementiert** | `Worker/Program.cs`: vor `app.Run()` einmal `claude --version` shellen und bei Exit≠0 abbrechen | +| 3 | Smoke-Spawn (`claude -p` mit Prompt "ping") | **Nicht getestet** | Integrationstest schreiben oder einmal manuell laufen lassen | +| 4 | E2E Happy Path (Non-Worktree) | **Nicht getestet** | UI starten → Liste "Test" anlegen → Task mit Tag `agent` + Status `queued` + Description "Schreibe ein Haiku über Intralogistik" → Run abwarten → Result prüfen | +| 5 | Worktree Happy Path | **Nicht getestet** | Manueller Test mit echtem Repo (z.B. einem temp-Repo) | +| 6 | No-Changes-Run | **Nicht getestet** | Prompt der nichts ändert → `head_commit` bleibt NULL | +| 7 | Kein Git-Repo | **Nicht getestet** | working_dir auf `C:\Temp` → Task `failed`, keine `worktrees`-Row | +| 8 | Merge-UI | **Nicht getestet** (UI ruft `GitService.MergeFfOnlyAsync`, aber nie ausgeführt) | Manuell | +| 9 | Override-Parallelität | Tests vorhanden für Slot-Logik, **End-to-End nicht** | UI: zwei Tasks queuen, `Run Now` auf der zweiten → beide laufen parallel | +| 10 | Schedule | Logik per Test abgedeckt, **End-to-End nicht** | Task mit `scheduled_for = now+2min` | +| 11 | Worker-Offline-Erkennung | UI hat Status-Bar, aber **nicht visuell verifiziert** | Worker killen, schauen ob Status auf "offline" wechselt | +| 12 | Live-Stream | **Nicht getestet** | Während Run TaskDetail öffnen, beobachten ob ndjson-Zeilen erscheinen | +| 13 | Wake-up (UI ruft `WakeQueue` nach Anlage) | Implementiert in `TaskListViewModel`, **nicht visuell verifiziert** | Tasks nach Anlage in <1s gepickert | + +**Vorschlag:** Wir machen einmal Step 4 (Haiku-Happy-Path) gemeinsam — wenn das läuft, ist die ganze Pipeline lebendig. + +--- + +## 2. UI-Polish (kritisch für Benutzbarkeit) + +Im aktuellen Stand kompiliert die UI, aber mehrere Stellen sind als `// TODO` markiert. Reihenfolge nach Schmerz: + +### 2.1 Folder-Picker für `Working Directory` +- **Datei:** `src/ClaudeDo.Ui/Views/ListEditorView.axaml` + `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs` +- **Aktuell:** plain `TextBox` — Pfad muss getippt werden. +- **Soll:** Button "…" daneben → öffnet `IStorageProvider.OpenFolderPickerAsync`, schreibt Pfad ins Feld. +- **Aufwand:** klein, ~30 Zeilen. + +### 2.2 Delete-Confirmation +- **Dateien:** `MainWindowViewModel.DeleteList`, `TaskListViewModel.DeleteTask` +- **Aktuell:** löscht direkt ohne Rückfrage. Datenverlust-Risiko. +- **Soll:** Mini-Dialog "Wirklich löschen?" mit Ja/Nein. +- **Aufwand:** klein, generisches `ConfirmDialog` lohnt sich (1× bauen, mehrfach nutzen). + +### 2.3 Markdown-Rendering für Result + Description +- **Datei:** `src/ClaudeDo.Ui/Views/TaskDetailView.axaml` +- **Aktuell:** `TextBox IsReadOnly="True"` mit Plaintext. +- **Soll:** `Markdown.Avalonia` Package einbinden und auf `MarkdownScrollViewer` umstellen. +- **Aufwand:** mittel — Package + ein paar XAML-Anpassungen. Theme-Integration kann nerven. + +### 2.4 Live-Log Auto-Scroll +- **Datei:** `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` (oder im VM) +- **Aktuell:** ndjson-Zeilen werden angehängt, aber Scrollposition bleibt stehen. +- **Soll:** Bei jeder neuen Zeile `ScrollViewer.ScrollToEnd()` solange User nicht manuell hochgescrollt hat (Sticky-Bottom-Pattern). +- **Aufwand:** klein, ein attached behavior reicht. + +### 2.5 Diff-Viewer +- **Datei:** `TaskDetailViewModel.ShowDiffAsync` +- **Aktuell:** `Process.Start("cmd", "/k git diff …")` — separates Konsolenfenster, hässlich. +- **Soll:** entweder unified-diff inline anzeigen (`git diff` Output in `TextBox` mit Mono-Font + Color für +/-) oder einen externen Diff-Tool-Hook (`git difftool`). +- **Aufwand:** mittel. MVP: einfach nur den Diff-Output in einem Modal. + +### 2.6 Status-Bar Active-Tasks Live-Update +- **Datei:** `StatusBarViewModel` +- **Risiko:** das Slot-State-Update kommt vom WorkerClient, aber `RunNowCommand.NotifyCanExecuteChanged` triggert nicht pro Item bei `IsConnected`-Wechsel (vom Slice-F-Agent dokumentiert). +- **Soll:** Über `WeakReferenceMessenger` (CommunityToolkit.Mvvm) eine Connection-Change-Message verteilen, an die alle `TaskItemViewModel` lauschen. +- **Aufwand:** klein, aber muss sauber gemacht werden. + +### 2.7 Settings-Dialog +- **Datei:** *neu* — `Views/SettingsDialog.axaml` + VM +- **Aktuell:** `~/.todo-app/ui.config.json` muss von Hand editiert werden. +- **Soll:** Dialog mit Feldern: DB-Pfad, SignalR-Port, Default-Tags. Persistiert zurück in JSON. +- **Aufwand:** mittel. Achtung: Port-Wechsel braucht Worker-Restart. + +--- + +## 3. Worker-Robustheit + +### 3.1 CLI-Preflight beim Worker-Start +- **Datei:** `src/ClaudeDo.Worker/Program.cs` +- **Soll:** vor `app.Run()` `claude --version` ausführen; bei Fehler `app.Logger.LogCritical` + `Environment.Exit(1)`. +- **Aufwand:** klein, ~20 Zeilen. Liefert Verification Step 2. + +### 3.2 Worktree-Cleanup beim Anlege-Failed +- **Datei:** `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` +- **Aktuell:** Wenn `WorktreeAddAsync` zwischen `CreateAsync`-Schritten failt (z.B. Branch existiert schon), bleibt evtl. ein halbangelegter Worktree-Dir auf der Platte. +- **Soll:** try/finally — bei Fehler `git worktree remove --force` als Best-Effort-Cleanup. +- **Aufwand:** klein. + +### 3.3 Logging über `Microsoft.Extensions.Logging` strukturieren +- **Datei:** alle Worker-Komponenten +- **Aktuell:** ILogger wird benutzt, aber kein File-Sink konfiguriert. +- **Soll:** Optional Serilog oder einfach `AddFile` (Karambolage.Extensions.Logging.File) — Service-Modus braucht persistente Logs außerhalb der Console. +- **Aufwand:** klein. + +### 3.4 Tag-Negation / Exclusion (Plan-TODO) +- **Plan-Sektion:** "Tag-Modell" +- **Aktuell:** Tags sind rein additiv (`list_tags ∪ task_tags`). +- **Soll:** Mechanismus, um auf Task-Ebene einen List-Tag auszuschließen. Z.B. neue Tabelle `task_tag_exclusions` ODER ein Prefix `!tag` im task_tags-Eintrag. +- **Aufwand:** mittel — Schema + Repo + Tests + UI. + +--- + +## 4. Service-Deployment (Plan-Sektion „Worker als Windows-Service") + +### 4.1 Windows-Service-Hosting in Code +- **Datei:** `src/ClaudeDo.Worker/Program.cs` +- **Pakete:** `Microsoft.Extensions.Hosting.WindowsServices` +- **Soll:** + ```csharp + builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker"); + builder.Logging.AddEventLog(...); + ``` +- **Aufwand:** klein. + +### 4.2 Pfad-Auflösung absolut machen +- Bereits in `WorkerConfig.Load` per `Paths.Expand` gemacht — verifizieren, dass auch `cfg.ClaudeBin` ggf. in Service-PATH gefunden wird. + +### 4.3 Install-Skripte / Doku +- **Datei:** *neu* — `docs/install-service.md` oder `scripts/install-service.cmd` +- **Inhalt:** `dotnet publish` + `sc.exe create` + `sc.exe failure` + Hinweis auf `obj=` (User-Account) wegen Claude-CLI-Session. +- **Aufwand:** klein. + +### 4.4 (später) Installer-Projekt +- WiX/MSIX, registriert Service + UI-Shortcut. Plan-Sektion „Offene Punkte". + +--- + +## 5. Tests / CI + +### 5.1 GitHub-Actions / Gitea-Actions Pipeline +- **Datei:** *neu* — `.gitea/workflows/ci.yml` (oder `.github/workflows/ci.yml`) +- **Inhalt:** `dotnet restore` → `dotnet build --no-restore` → `dotnet test --no-build`. Auf Push + PR. +- **Aufwand:** klein. + +### 5.2 Echter SignalR-Roundtrip-Test +- **Datei:** *neu* — `tests/ClaudeDo.Worker.Tests/Hub/WorkerHubTests.cs` +- **Soll:** mit `WebApplicationFactory` + `HubConnectionBuilder` testen, dass `Ping`, `GetActive`, `RunNow`-Throw-Verhalten korrekt sind. Plan-Verification 1b + 9. +- **Aufwand:** mittel. + +### 5.3 Smoke-Test gegen echten `claude` +- **Datei:** *neu* — `tests/ClaudeDo.Worker.Tests/Runner/ClaudeProcessSmokeTest.cs` +- **Soll:** Real-CLI-Test, der mit `[Fact(Skip="..."]` ausgegraut bleibt und nur lokal aktiviert wird, wenn `CLAUDE_AUTHENTICATED=1` Env-Var gesetzt ist. +- **Aufwand:** klein. + +--- + +## 6. Dokumentation + +### 6.1 README.md +- Komplett fehlt. Mind. 1× kurz: was ist es, wie starten (Worker + UI), wo Config. +- **Aufwand:** klein. + +### 6.2 `docs/architecture.md` +- In `plan.md` schon teilweise enthalten — kann entweder konsolidiert oder explizit ausgegliedert werden. + +### 6.3 ADRs für die getroffenen Entscheidungen +- Z.B. „SignalR vs. SQLite-Polling für IPC", „Worktree pro Task", „SignalR über Loopback ohne Auth". +- **Aufwand:** klein, hilfreich für später. + +--- + +## 7. Bekannte Code-Schulden / Smells + +| Stelle | Issue | +|---|---| +| `WorkerHub.GetActive` returnt `IReadOnlyList` mit anonymen Typen | Sollte ein expliziter DTO sein (`ActiveTaskDto`), den Worker UND Ui teilen. Aktuell duplizieren beide das Schema. | +| `TaskRunner` führt eine `if (list.WorkingDir != null)` Verzweigung mitten in der Methode | Strategy-Pattern (`IRunStrategy`: SandboxStrategy, WorktreeStrategy) wenn die Methode wächst. Aktuell noch klein genug. | +| `App.Services` als public static `ServiceProvider` | Service-Locator-Antipattern. Toleriert, weil nur in `App.OnFrameworkInitializationCompleted` verwendet. Falls mehr Code drauf zugreift → echtes DI durchziehen. | +| Embedded `schema.sql` ohne Versionierung | Solange das Schema nicht in Production läuft, OK. Sobald User-Daten existieren → `migrations/` Folder + Version-Tabelle. | +| CRLF-Warnings beim Commit | `.gitattributes` mit `* text=auto eol=lf` (oder explizit pro Sprache) wäre sauberer. | + +--- + +## Empfohlene Reihenfolge für die nächste Session + +1. **Verification Step 4** zusammen durchspielen → falls etwas grundlegend kaputt ist, jetzt finden, nicht später. +2. **CLI-Preflight (3.1)** + **Folder-Picker (2.1)** + **Delete-Confirm (2.2)** — kleine, isolierte Wins. +3. **Auto-Scroll (2.4)** + **Active-Tasks Live-Update (2.6)** — User-Experience im Detail-Pane. +4. **Markdown-Rendering (2.3)** — größer, lohnt sich aber für Lesbarkeit. +5. **Worktree-Cleanup (3.2)** — Robustheit, bevor wir Worktrees ernsthaft nutzen. +6. **CI-Pipeline (5.1)** — automatisches Sicherheitsnetz für alles weitere. +7. **Service-Deployment (4)** — wenn die App lokal stabil läuft. +8. **Settings-Dialog (2.7)** + **Diff-Viewer (2.5)** — Polish. +9. **Tag-Negation (3.4)** — wenn der Bedarf konkret wird. + +Punkte 1–3 sind ein realistischer Block für eine Session. diff --git a/docs/superpowers/plans/2026-04-14-ui-fixes.md b/docs/superpowers/plans/2026-04-14-ui-fixes.md new file mode 100644 index 0000000..72179b7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-ui-fixes.md @@ -0,0 +1,1550 @@ +# UI Fixes 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:** Fix four post-integration issues: raw NDJSON display, missing start feedback, lost live output, and missing config editors + modal theming. + +**Architecture:** A new `StreamLineFormatter` in the UI layer parses NDJSON for display. `TaskDetailViewModel` switches from `ObservableCollection` to a single `string` property. Optimistic UI feedback via a local `RunNowRequestedEvent`. Existing editor dialogs get config sections and proper theming. + +**Tech Stack:** .NET 8, Avalonia 12 (Fluent dark theme), CommunityToolkit.Mvvm, System.Text.Json, xUnit + +--- + +## File Structure + +### New Files +- `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` — NDJSON-to-text parser for UI display +- `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` — test project for UI helpers +- `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` — formatter unit tests + +### Modified Files +- `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` — LiveText, formatter, start feedback, log reload +- `src/ClaudeDo.Ui/Views/TaskDetailView.axaml` — TextBox replaces ItemsControl, auto-scroll +- `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` — auto-scroll handler +- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — RunNowRequestedEvent, GetAgentsAsync, AgentInfo DTO +- `src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs` — IsStarting property +- `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` — wire RunNowRequestedEvent +- `src/ClaudeDo.Ui/Views/TaskListView.axaml` — starting state visual +- `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs` — config fields, agent loading +- `src/ClaudeDo.Ui/Views/ListEditorView.axaml` — config section, theming +- `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs` — config override fields +- `src/ClaudeDo.Ui/Views/TaskEditorView.axaml` — config section, theming +- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — default model fallback +- `ClaudeDo.slnx` — add new test project + +--- + +### Task 1: Create UI test project + +**Files:** +- Create: `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` + +- [ ] **Step 1: Create the test project** + +```xml + + + + net8.0 + enable + false + + + + + + + + + + +``` + +- [ ] **Step 2: Add to solution** + +Run: `dotnet sln ClaudeDo.slnx add tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` +Expected: Project added successfully. + +- [ ] **Step 3: Verify build** + +Run: `dotnet build tests/ClaudeDo.Ui.Tests` +Expected: Build succeeded. + +- [ ] **Step 4: Commit** + +```bash +git add tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj ClaudeDo.slnx +git commit -m "chore(tests): add ClaudeDo.Ui.Tests project" +``` + +--- + +### Task 2: StreamLineFormatter — text deltas (TDD) + +**Files:** +- Create: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` +- Create: `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` + +- [ ] **Step 1: Write failing tests for text delta extraction** + +```csharp +// tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +using ClaudeDo.Ui.Helpers; + +namespace ClaudeDo.Ui.Tests.Helpers; + +public class StreamLineFormatterTests +{ + private readonly StreamLineFormatter _sut = new(); + + [Fact] + public void FormatLine_TextDelta_ReturnsTextContent() + { + var line = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello world"}}}"""; + var result = _sut.FormatLine(line); + Assert.Equal("Hello world", result); + } + + [Fact] + public void FormatLine_ConsecutiveTextDeltas_ReturnEachDelta() + { + var line1 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello "}}}"""; + var line2 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"world"}}}"""; + Assert.Equal("Hello ", _sut.FormatLine(line1)); + Assert.Equal("world", _sut.FormatLine(line2)); + } + + [Fact] + public void FormatLine_ContentBlockStop_ReturnsNewline() + { + var delta = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}}"""; + var stop = """{"type":"stream_event","event":{"type":"content_block_stop","index":0}}"""; + _sut.FormatLine(delta); + Assert.Equal("\n", _sut.FormatLine(stop)); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: Build error — `StreamLineFormatter` does not exist. + +- [ ] **Step 3: Write minimal StreamLineFormatter** + +```csharp +// src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs +using System.Text.Json; + +namespace ClaudeDo.Ui.Helpers; + +public sealed class StreamLineFormatter +{ + public string? FormatLine(string ndjsonLine) + { + if (string.IsNullOrWhiteSpace(ndjsonLine)) return null; + + try + { + using var doc = JsonDocument.Parse(ndjsonLine); + var root = doc.RootElement; + + if (!root.TryGetProperty("type", out var typeProp)) return null; + var type = typeProp.GetString(); + + return type switch + { + "stream_event" => HandleStreamEvent(root), + _ => null, + }; + } + catch (JsonException) + { + return ndjsonLine; // Fallback: show raw line + } + } + + private static string? HandleStreamEvent(JsonElement root) + { + if (!root.TryGetProperty("event", out var evt)) return null; + if (!evt.TryGetProperty("type", out var evtTypeProp)) return null; + var evtType = evtTypeProp.GetString(); + + return evtType switch + { + "content_block_delta" => HandleDelta(evt), + "content_block_stop" => "\n", + _ => null, + }; + } + + private static string? HandleDelta(JsonElement evt) + { + if (!evt.TryGetProperty("delta", out var delta)) return null; + if (!delta.TryGetProperty("type", out var deltaType)) return null; + + return deltaType.GetString() switch + { + "text_delta" => delta.TryGetProperty("text", out var text) ? text.GetString() : null, + _ => null, // input_json_delta etc. — skip + }; + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: 3 passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +git commit -m "feat(ui): add StreamLineFormatter with text delta parsing (TDD)" +``` + +--- + +### Task 3: StreamLineFormatter — tool use, result, system, fallback (TDD) + +**Files:** +- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` +- Modify: `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` + +- [ ] **Step 1: Write failing tests for remaining event types** + +Append to `StreamLineFormatterTests.cs`: + +```csharp +[Fact] +public void FormatLine_ToolUseStart_ReturnsToolNameLine() +{ + var line = """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_xxx","name":"Read","input":{}}}}"""; + var result = _sut.FormatLine(line); + Assert.Equal("\n[Tool: Read]\n", result); +} + +[Fact] +public void FormatLine_InputJsonDelta_ReturnsNull() +{ + var line = """{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file\":"}}}"""; + Assert.Null(_sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_Result_ReturnsFormattedResult() +{ + var line = """{"type":"result","result":"Task completed successfully.","session_id":"sess_123"}"""; + var result = _sut.FormatLine(line); + Assert.Equal("\n--- Result ---\nTask completed successfully.\n", result); +} + +[Fact] +public void FormatLine_ApiRetry_ReturnsRetryNotice() +{ + var line = """{"type":"system","subtype":"api_retry","message":"Retrying..."}"""; + var result = _sut.FormatLine(line); + Assert.Equal("\n[Retrying API call...]\n", result); +} + +[Fact] +public void FormatLine_SystemNonRetry_ReturnsNull() +{ + var line = """{"type":"system","subtype":"init"}"""; + Assert.Null(_sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_AssistantType_ReturnsNull() +{ + var line = """{"type":"assistant","message":{"role":"assistant","content":[]}}"""; + Assert.Null(_sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_MalformedJson_ReturnsRawLine() +{ + var line = "this is not json"; + Assert.Equal("this is not json", _sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_MessageStartAndDelta_ReturnsNull() +{ + var start = """{"type":"stream_event","event":{"type":"message_start","message":{"id":"msg_xxx"}}}"""; + var delta = """{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":50}}}"""; + Assert.Null(_sut.FormatLine(start)); + Assert.Null(_sut.FormatLine(delta)); +} +``` + +- [ ] **Step 2: Run tests to verify new tests fail** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: Several failures — `FormatLine_ToolUseStart`, `FormatLine_Result`, `FormatLine_ApiRetry` return null instead of expected strings. + +- [ ] **Step 3: Extend StreamLineFormatter to handle all event types** + +Update the `FormatLine` method's switch expression in `StreamLineFormatter.cs`: + +```csharp +return type switch +{ + "stream_event" => HandleStreamEvent(root), + "result" => HandleResult(root), + "system" => HandleSystem(root), + "assistant" => null, + _ => null, +}; +``` + +Add `HandleStreamEvent` case for `content_block_start`: + +```csharp +private static string? HandleStreamEvent(JsonElement root) +{ + if (!root.TryGetProperty("event", out var evt)) return null; + if (!evt.TryGetProperty("type", out var evtTypeProp)) return null; + var evtType = evtTypeProp.GetString(); + + return evtType switch + { + "content_block_start" => HandleBlockStart(evt), + "content_block_delta" => HandleDelta(evt), + "content_block_stop" => "\n", + _ => null, + }; +} +``` + +Add new methods: + +```csharp +private static string? HandleBlockStart(JsonElement evt) +{ + if (!evt.TryGetProperty("content_block", out var block)) return null; + if (!block.TryGetProperty("type", out var blockType)) return null; + + if (blockType.GetString() == "tool_use" && + block.TryGetProperty("name", out var name)) + { + return $"\n[Tool: {name.GetString()}]\n"; + } + return null; +} + +private static string? HandleResult(JsonElement root) +{ + if (root.TryGetProperty("result", out var resultProp)) + { + var text = resultProp.GetString(); + if (text is not null) + return $"\n--- Result ---\n{text}\n"; + } + return null; +} + +private static string? HandleSystem(JsonElement root) +{ + if (root.TryGetProperty("subtype", out var subtype) && + subtype.GetString() == "api_retry") + { + return "\n[Retrying API call...]\n"; + } + return null; +} +``` + +- [ ] **Step 4: Run tests to verify all pass** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: 11 passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +git commit -m "feat(ui): complete StreamLineFormatter with tool use, result, system events" +``` + +--- + +### Task 4: StreamLineFormatter — FormatFile method (TDD) + +**Files:** +- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` +- Modify: `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` + +- [ ] **Step 1: Write failing test for FormatFile** + +Append to `StreamLineFormatterTests.cs`: + +```csharp +[Fact] +public void FormatFile_ParsesAllLinesAndReturnsFormattedText() +{ + var dir = Path.Combine(Path.GetTempPath(), "claudedo_test_" + Guid.NewGuid().ToString("N")[..8]); + Directory.CreateDirectory(dir); + var filePath = Path.Combine(dir, "test.ndjson"); + try + { + var lines = new[] + { + """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}""", + """{"type":"stream_event","event":{"type":"content_block_stop","index":0}}""", + """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"t1","name":"Edit","input":{}}}}""", + """{"type":"stream_event","event":{"type":"content_block_stop","index":1}}""", + """{"type":"result","result":"Done.","session_id":"s1"}""", + }; + File.WriteAllLines(filePath, lines); + + var result = _sut.FormatFile(filePath); + Assert.Contains("Hello", result); + Assert.Contains("[Tool: Edit]", result); + Assert.Contains("--- Result ---", result); + Assert.Contains("Done.", result); + } + finally + { + Directory.Delete(dir, true); + } +} + +[Fact] +public void FormatFile_TrimsLargeContent() +{ + var dir = Path.Combine(Path.GetTempPath(), "claudedo_test_" + Guid.NewGuid().ToString("N")[..8]); + Directory.CreateDirectory(dir); + var filePath = Path.Combine(dir, "large.ndjson"); + try + { + // Generate enough text deltas to exceed 50k chars + var lines = new List(); + for (int i = 0; i < 600; i++) + { + var chunk = new string('x', 100); + lines.Add($$"""{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"{{chunk}}\n"}}}"""); + } + File.WriteAllLines(filePath, lines); + + var result = _sut.FormatFile(filePath); + Assert.True(result.Length <= 50_000 + 200); // some tolerance for trimming at newline boundary + } + finally + { + Directory.Delete(dir, true); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "FormatFile" -v quiet` +Expected: Build error — `FormatFile` method does not exist. + +- [ ] **Step 3: Implement FormatFile and trimming** + +Add to `StreamLineFormatter.cs`: + +```csharp +private const int MaxLength = 50_000; + +public string FormatFile(string filePath) +{ + var sb = new System.Text.StringBuilder(); + foreach (var line in File.ReadLines(filePath)) + { + var formatted = FormatLine(line); + if (formatted is not null) + sb.Append(formatted); + } + return Trim(sb.ToString()); +} + +public static string Trim(string text) +{ + if (text.Length <= MaxLength) return text; + var trimStart = text.Length - MaxLength; + var newlineAfter = text.IndexOf('\n', trimStart); + if (newlineAfter >= 0 && newlineAfter < trimStart + 200) + trimStart = newlineAfter + 1; + return text[trimStart..]; +} +``` + +Add `using System.Text;` to the top of the file. + +- [ ] **Step 4: Run tests to verify all pass** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: 13 passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +git commit -m "feat(ui): add FormatFile and text trimming to StreamLineFormatter" +``` + +--- + +### Task 5: TaskDetailViewModel — replace LiveLines with LiveText + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` + +- [ ] **Step 1: Replace LiveLines with LiveText and wire formatter** + +In `TaskDetailViewModel.cs`, make these changes: + +1. Add using at top: +```csharp +using ClaudeDo.Ui.Helpers; +``` + +2. Replace the LiveLines property (line 40) and MaxLiveLines constant (line 47): + +Remove: +```csharp +public ObservableCollection LiveLines { get; } = new(); +``` +and: +```csharp +private const int MaxLiveLines = 500; +``` + +Add: +```csharp +[ObservableProperty] private string _liveText = ""; +private StreamLineFormatter _formatter = new(); +``` + +3. Update `LoadAsync` (line 69) — replace `LiveLines.Clear()` with: +```csharp +LiveText = ""; +_formatter = new StreamLineFormatter(); +``` + +4. Update `Clear` method — replace `LiveLines.Clear()` with: +```csharp +LiveText = ""; +_formatter = new StreamLineFormatter(); +``` + +5. Update `OnTaskMessage` (lines 259-265): + +Replace entire method: +```csharp +private void OnTaskMessage(string taskId, string line) +{ + if (taskId != _taskId) return; + var formatted = _formatter.FormatLine(line); + if (formatted is not null) + { + LiveText += formatted; + if (LiveText.Length > 50_000) + LiveText = StreamLineFormatter.Trim(LiveText); + } +} +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +git commit -m "refactor(ui): replace LiveLines with LiveText + StreamLineFormatter" +``` + +--- + +### Task 6: TaskDetailView — TextBox replaces ItemsControl + auto-scroll + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/TaskDetailView.axaml` +- Modify: `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` + +- [ ] **Step 1: Replace ItemsControl with TextBox in TaskDetailView.axaml** + +Replace lines 107-122 (the "Live Output" section heading through the Border/ItemsControl): + +Old: +```xml + + + + + + + + + + + + +``` + +New: +```xml + + + + + + +``` + +- [ ] **Step 2: Add auto-scroll in code-behind** + +In `TaskDetailView.axaml.cs`, add an `OnDataContextChanged` override and property-change handler: + +Add using: +```csharp +using System.ComponentModel; +``` + +Add after the `FocusTitle` method: + +```csharp +protected override void OnDataContextChanged(EventArgs e) +{ + base.OnDataContextChanged(e); + if (DataContext is TaskDetailViewModel vm) + { + vm.PropertyChanged += OnViewModelPropertyChanged; + } +} + +private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) +{ + if (e.PropertyName == nameof(TaskDetailViewModel.LiveText)) + { + var scroll = this.FindControl("LiveOutputScroll"); + scroll?.ScrollToEnd(); + } +} +``` + +- [ ] **Step 3: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/TaskDetailView.axaml src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs +git commit -m "feat(ui): replace ItemsControl with TextBox for formatted live output" +``` + +--- + +### Task 7: Log reload from disk + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` + +- [ ] **Step 1: Add log reload to LoadAsync** + +In `TaskDetailViewModel.cs`, in `LoadAsync`, after `LogPath = task.LogPath;` (around line 81), add: + +```csharp +// Load historical log for completed tasks +if (task.LogPath is not null + && task.Status is Data.Models.TaskStatus.Done or Data.Models.TaskStatus.Failed + && File.Exists(task.LogPath)) +{ + _formatter = new StreamLineFormatter(); + LiveText = _formatter.FormatFile(task.LogPath); +} +``` + +Add `using System.IO;` at the top if not already present. + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +git commit -m "feat(ui): reload formatted log from disk for completed tasks" +``` + +--- + +### Task 8: WorkerClient — RunNowRequestedEvent + +**Files:** +- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` + +- [ ] **Step 1: Add RunNowRequestedEvent and fire it in RunNowAsync** + +In `WorkerClient.cs`: + +Add event declaration after the existing events (after line 44): +```csharp +public event Action? RunNowRequestedEvent; +``` + +Update `RunNowAsync` method (lines 163-166): +```csharp +public async Task RunNowAsync(string taskId) +{ + RunNowRequestedEvent?.Invoke(taskId); + await _hub.InvokeAsync("RunNow", taskId); +} +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/Services/WorkerClient.cs +git commit -m "feat(ui): add RunNowRequestedEvent for optimistic UI feedback" +``` + +--- + +### Task 9: TaskItemViewModel — IsStarting state + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs` + +- [ ] **Step 1: Add IsStarting property and update CanRunNow** + +In `TaskItemViewModel.cs`: + +Add property after line 16: +```csharp +[ObservableProperty] private bool _isStarting; +``` + +Update `CanRunNow` (line 83-84): +```csharp +private bool CanRunNow() => + _canRunNow() && Status != TaskStatus.Running && !IsStarting; +``` + +Add method to set starting state (after `Refresh` method): +```csharp +public void SetStarting() +{ + IsStarting = true; + StatusText = "starting..."; + RunNowCommand.NotifyCanExecuteChanged(); +} + +public void ClearStarting() +{ + IsStarting = false; + RunNowCommand.NotifyCanExecuteChanged(); +} +``` + +Update `Refresh` method — add after `OnPropertyChanged(nameof(IsRunning))` (line 68): +```csharp +IsStarting = false; +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs +git commit -m "feat(ui): add IsStarting state to TaskItemViewModel" +``` + +--- + +### Task 10: TaskListViewModel — wire RunNowRequested to TaskItemViewModels + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` + +- [ ] **Step 1: Subscribe to RunNowRequestedEvent and TaskStartedEvent** + +In `TaskListViewModel.cs` constructor, after the existing `worker.PropertyChanged` subscription (after line 57): + +```csharp +worker.RunNowRequestedEvent += taskId => +{ + var item = Tasks.FirstOrDefault(t => t.Id == taskId); + item?.SetStarting(); +}; + +worker.TaskStartedEvent += (_, taskId, _) => +{ + var item = Tasks.FirstOrDefault(t => t.Id == taskId); + item?.ClearStarting(); +}; +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs +git commit -m "feat(ui): wire RunNowRequested to TaskItemViewModel starting state" +``` + +--- + +### Task 11: TaskDetailViewModel — start feedback + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` + +- [ ] **Step 1: Subscribe to RunNowRequestedEvent and TaskStartedEvent** + +In `TaskDetailViewModel.cs` constructor, after `worker.TaskUpdatedEvent += OnTaskUpdated;` (line 63): + +```csharp +worker.RunNowRequestedEvent += OnRunNowRequested; +worker.TaskStartedEvent += OnTaskStarted; +``` + +Add the handler methods before `OnTaskMessage`: + +```csharp +private void OnRunNowRequested(string taskId) +{ + if (taskId != _taskId) return; + StatusText = "starting..."; + LiveText = ""; + _formatter = new StreamLineFormatter(); +} + +private void OnTaskStarted(string slot, string taskId, DateTime startedAt) +{ + if (taskId != _taskId) return; + StatusText = "running"; +} +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +git commit -m "feat(ui): add optimistic start feedback to TaskDetailViewModel" +``` + +--- + +### Task 12: TaskListView — starting state visual + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/TaskListView.axaml` + +- [ ] **Step 1: Add starting indicator next to running indicator** + +In `TaskListView.axaml`, find the running indicator Ellipse (the orange dot visible when `IsRunning`). After it, add a similar indicator for the starting state. The exact location depends on the layout, but it should be adjacent to the existing status indicators. + +Find the `IsRunning` Ellipse and add a sibling for `IsStarting`: + +```xml + + +``` + +Use a gold/yellow color (`#FFD700`) to distinguish "starting" from "running" (orange). + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/TaskListView.axaml +git commit -m "feat(ui): add starting state indicator in task list" +``` + +--- + +### Task 13: Modal theming — ListEditorView + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/ListEditorView.axaml` + +- [ ] **Step 1: Apply theme resources to ListEditorView** + +In `ListEditorView.axaml`, update the `` element — add `Background`: + +```xml + +``` + +Add `Foreground="{StaticResource TextSecondaryBrush}"` to each label TextBlock ("Name", "Working Directory", "Default Commit Type"). + +Style the Save button with accent: +```xml +