Compare commits
111 Commits
v1.7.0
...
50c10b6e75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50c10b6e75 | ||
|
|
075b6d13af | ||
|
|
324f1d9c7c | ||
|
|
c592ca32fb | ||
|
|
7ce418d474 | ||
|
|
ab260ad0a6 | ||
|
|
b3b87df320 | ||
|
|
da73324e3a | ||
|
|
c5a4e350e9 | ||
|
|
e547921fdd | ||
|
|
f1316dfd0e | ||
|
|
cc7355eaa4 | ||
|
|
22a1ba7f30 | ||
|
|
a3f407b0e5 | ||
|
|
469e68bbc8 | ||
|
|
176b9855bf | ||
|
|
5d34f95fe0 | ||
|
|
0e130177fc | ||
|
|
5363570fb4 | ||
|
|
f60becaf06 | ||
|
|
519bfbe6b3 | ||
|
|
06e3acd5ac | ||
|
|
f3052dc5fc | ||
|
|
9d133e227b | ||
|
|
7542bc2058 | ||
|
|
ef86a8c29b | ||
|
|
da23b6cd3a | ||
|
|
c10f564265 | ||
|
|
8036de1019 | ||
|
|
7873e60095 | ||
|
|
6f4b5d5544 | ||
|
|
f25c7599bd | ||
|
|
6fdf04d6a0 | ||
|
|
ee0d1257dd | ||
|
|
204b089000 | ||
|
|
da4ab0ca5e | ||
|
|
c035720b37 | ||
|
|
4522ac906b | ||
|
|
2455eacb1f | ||
|
|
d8b86e33a3 | ||
|
|
49b9f1ffde | ||
|
|
4d52845130 | ||
|
|
9a117a5429 | ||
|
|
202e8dea49 | ||
|
|
1e547dea18 | ||
|
|
56ebc2803f | ||
|
|
cf7f0da400 | ||
|
|
ac1e9b06de | ||
|
|
79bfc79d33 | ||
|
|
1b3c6bdbb4 | ||
|
|
bd1e3db1d9 | ||
|
|
edc9f77357 | ||
|
|
883dbc6af7 | ||
|
|
9bdf99d95f | ||
|
|
c8f468f270 | ||
|
|
84fd2c11a0 | ||
|
|
30b49d1071 | ||
|
|
ad7d74820a | ||
|
|
75aa42b877 | ||
|
|
925b72ae83 | ||
|
|
cd683ba227 | ||
|
|
d0ab382973 | ||
|
|
3e3041c1c7 | ||
|
|
92cee125cc | ||
|
|
bba3c55e1c | ||
|
|
26f5936d14 | ||
|
|
b72a7888e4 | ||
|
|
beae2d639d | ||
|
|
ac137f7c1c | ||
|
|
97e38fb480 | ||
|
|
b63c78c234 | ||
|
|
37ce673a57 | ||
|
|
b9741ef38b | ||
|
|
0a0d7e8551 | ||
|
|
2dfa9956c5 | ||
|
|
773811d060 | ||
|
|
3756b81817 | ||
|
|
72a86fc173 | ||
|
|
cc46019622 | ||
|
|
71ac48162a | ||
|
|
bcf5e2f51f | ||
|
|
fb055ce740 | ||
|
|
9e7f37b5cc | ||
|
|
39fa83a0a0 | ||
|
|
15ed624d4a | ||
|
|
52e3980cd1 | ||
|
|
53d897aff4 | ||
|
|
7d743f17c6 | ||
|
|
26758b6e8a | ||
|
|
914095dc99 | ||
|
|
4d82079cac | ||
|
|
3a40e39fc8 | ||
|
|
2e73d3333d | ||
|
|
c764b2bf6e | ||
|
|
f7d1b37343 | ||
|
|
fab17720cc | ||
|
|
9470c5b10b | ||
|
|
c45f892591 | ||
|
|
a8670ee23a | ||
|
|
7676ecf0d4 | ||
|
|
fa83d7f441 | ||
|
|
e48475d6cd | ||
|
|
46f42a4d93 | ||
|
|
46ac3fc930 | ||
|
|
5e0859fbb8 | ||
|
|
2d00160283 | ||
|
|
20b3a29d08 | ||
|
|
fd7f8ac78f | ||
|
|
0bb809445e | ||
|
|
3c66d65160 | ||
|
|
ffe0fb9820 |
27
CLAUDE.md
27
CLAUDE.md
@@ -44,16 +44,35 @@ Two-process system communicating over SignalR (`127.0.0.1:47821`):
|
|||||||
- Views use compiled bindings (`x:DataType`)
|
- Views use compiled bindings (`x:DataType`)
|
||||||
- ViewModels use `[ObservableProperty]` and `[RelayCommand]` source generators
|
- ViewModels use `[ObservableProperty]` and `[RelayCommand]` source generators
|
||||||
|
|
||||||
|
## Working style (autonomous)
|
||||||
|
|
||||||
|
For any non-trivial feature, bug, or change, run this loop without hand-holding:
|
||||||
|
|
||||||
|
1. **Brainstorm first** (superpowers:brainstorming) — ask clarifying questions one at a time, propose 2–3 options with a recommendation, present a short design, get approval before building.
|
||||||
|
2. **Write it down** — a spec in `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and a step-by-step plan in `docs/superpowers/plans/` (superpowers:writing-plans). Commit the docs.
|
||||||
|
3. **Implement on main** with superpowers:subagent-driven-development — one subagent per task, TDD, build + test, commit per task with Conventional Commits. Once the plan is approved, do NOT pause for re-approval between tasks; only stop for genuine decisions or blockers.
|
||||||
|
4. **Trust but verify** — read each subagent's diff and run the build/tests yourself before marking a task done.
|
||||||
|
5. **Bugs** → superpowers:systematic-debugging (find the root cause before any fix).
|
||||||
|
6. **Never claim UI works without running it** — explicitly flag visual-verification gaps for the user to check.
|
||||||
|
|
||||||
|
Commit freely (per task + the spec/plan docs). Never push without asking.
|
||||||
|
|
||||||
## Building & Testing
|
## Building & Testing
|
||||||
|
|
||||||
`dotnet build ClaudeDo.slnx` requires .NET 9; on .NET 8 build individual projects instead.
|
`dotnet build ClaudeDo.slnx` requires .NET 9; on .NET 8 build individual projects with `-c Release` (a running Worker locks the `Debug` output).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj # pulls in Ui + Data
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release # pulls in Ui + Data
|
||||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
dotnet test tests/ClaudeDo.Worker.Tests # also: Data.Tests, Ui.Tests, Installer.Tests, Releases.Tests
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release # also: Data.Tests, Ui.Tests, Localization.Tests, Installer.Tests, Releases.Tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Gotchas
|
||||||
|
- **Subagents:** use the `sonnet` model; stage files explicitly by path — never `git add -A` (parallel sessions often leave unrelated WIP in the tree).
|
||||||
|
- **Icons:** `PathIcon` *fills* its geometry. Line-art/stroke icons must be authored as filled geometry, or rendered with a stroked `Path` — otherwise they render invisible.
|
||||||
|
- **Localization:** `locales/en.json` and `locales/de.json` keys must stay in parity (Localization.Tests enforces it).
|
||||||
|
- **Test fakes:** changing `IWorkerClient` / `WorkerHub` / ViewModel constructors breaks hand-rolled fakes in both test projects — update them.
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
- `docs/plan.md` — full architecture and design spec
|
- `docs/plan.md` — full architecture and design spec
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
|
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
|
||||||
<Project Path="src/ClaudeDo.Releases/ClaudeDo.Releases.csproj" />
|
<Project Path="src/ClaudeDo.Releases/ClaudeDo.Releases.csproj" />
|
||||||
<Project Path="src/ClaudeDo.Localization/ClaudeDo.Localization.csproj" />
|
<Project Path="src/ClaudeDo.Localization/ClaudeDo.Localization.csproj" />
|
||||||
|
<Project Path="src/ClaudeDo.Logging/ClaudeDo.Logging.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj" />
|
<Project Path="tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj" />
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# Task Mailbox — Push Messages Into Running Sessions
|
# Task Mailbox — Push Messages Into Running Sessions
|
||||||
|
|
||||||
**Status:** proposal
|
**Status:** PARKED (2026-06-04) — not building this.
|
||||||
|
**Why parked:** The generic Claude-Mailbox plugin (the `mcp__mailbox__*` tools used in normal sessions) already covers the core need — cross-session messaging, inbox checks, a sender — at the harness level for any project. Integrating it directly into ClaudeDo (task/worktree-scoped inboxes, per-worktree CLAUDE.md + hook seeding, UI badges, `send_to_peer`) is a sizable build (migration + MCP tools + SignalR + UI + hooks) for marginal gain over the plugin. Revisit only if the generic plugin proves insufficient for the parallel-session workflow. The original proposal is kept below for reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**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.
|
**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
|
## Problem
|
||||||
|
|||||||
294
docs/open.md
294
docs/open.md
@@ -1,286 +1,30 @@
|
|||||||
# ClaudeDo — Offene Punkte
|
# ClaudeDo — Offene Punkte
|
||||||
|
|
||||||
Stand: 2026-04-30. Neu erstellt nach Code-Audit gegen `plan.md`, `improvement-plan.md` und `mailbox-proposal.md`.
|
Stand: 2026-06-04. **Nur noch offene Punkte.** Was erledigt ist, steht in den Commits und im Code — nicht hier.
|
||||||
|
|
||||||
Die alte Version dieses Dokuments war auf 2026-04-13 ("nach Slice F") datiert und ignorierte die seither gelandeten Slices (Planning Sessions, Prime Claude, Self-Update, Externe MCP-Tools, editierbare Status/Tags, BlockedBy-Chains). Diese Version trennt sauber zwischen **erledigt**, **teilweise**, **offen** — und listet das, was inzwischen gebaut wurde, explizit als „shipped" auf, damit es nicht verloren geht.
|
|
||||||
|
|
||||||
Legende: ✅ DONE — 🟡 PARTIAL — ⬜ OPEN — ⛔ DROPPED
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. Was seit dem 2026-04-13 dazugekommen ist
|
## Manuelle Verifikation (offen)
|
||||||
|
|
||||||
Diese Slices gab es im alten Dokument noch nicht (oder nur als Platzhalter). Sie sind **fertig im Code**, brauchen aber jeweils noch ein paar Polish-Punkte (siehe Sektion 2/3).
|
Kein Code-Aufwand, nur Durchspielen mit explizit notiertem Pass-Kriterium. Der Großteil der Pipeline ist laut User bereits in der Praxis getestet; hier das, was noch ein falsifizierbares Observable braucht.
|
||||||
|
|
||||||
| Slice | Worker-Anker | UI-Anker | Status |
|
- **Worktree-Pipeline:**
|
||||||
|---|---|---|---|
|
- Worktree-Happy-Path → `worktrees.state='active'`, `head_commit` gesetzt, `diff_stat` non-empty, Branch `claudedo/<id>` auf Disk.
|
||||||
| **Planning Sessions** (Plan B+C) | `Planning/PlanningSessionManager`, `PlanningChainCoordinator`, `PlanningMcpService` | `Views/Planning/PlanningDiffView`, `ConflictResolutionView`, `UnfinishedPlanningModalView` | ✅ Code, manuelle Verifikation siehe §1.1 |
|
- No-Changes-Run → `status='Done'`, `head_commit IS NULL`, `diff_stat IS NULL`.
|
||||||
| **Prime Claude** (geplante Recurrence) | `Prime/PrimeScheduler`, `NextDueCalculator`, `PrimeRunner` | `ViewModels/Modals/PrimeClaudeTabViewModel`, `Views/Controls/ThemedDatePicker` | ✅ Code, manuelle Verifikation siehe §1.2 |
|
- Kein Git-Repo (`working_dir=C:\Temp`) → `status='Failed'`, **keine** `worktrees`-Row, Git-Fehler im Log.
|
||||||
| **Self-Update System** (Gitea Releases) | — | `ClaudeDo.Releases` (`ReleaseClient`, `SelfUpdater`, `ChecksumVerifier`, `VersionComparer`), `ClaudeDo.Installer` (Pages/Steps/Core) | ✅ Code, manuelle Verifikation siehe §1.3 |
|
- **Feature-Walkthroughs:** Planning-Session-Flow (Draft→Finalize→Chain), Prime/Daily-Prep-Trigger, Weekly-Report-Generierung, Self-Update (Banner → Update → „up to date").
|
||||||
| **Externes MCP-Endpoint** (11 Tools für Drittsessions) | `External/ExternalMcpService` (`ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `UpdateTask`, `UpdateTaskStatus`, `SetTaskTags`, `ListTags`, `DeleteTask`, `RunTaskNow`, `CancelTask`), `ExternalMcpAuthMiddleware` (X-ClaudeDo-Key) | — | ✅ Code, ohne Tests am Endpoint selbst |
|
- **Worker-Autostart am Gerät:** Logoff/Logon-Autostart, Update-Pfad, Uninstall entfernt die Startup-`.lnk`.
|
||||||
| **Editierbare Status & Tags** (entkoppelt vom `agent`-Tag) | `WorkerHub.SetTaskStatus`, `SetTaskTags`, `UpdateTaskAgentSettings`; Queue-Picker filtert nicht mehr nach `agent`-Tag | `DetailsIslandViewModel`, Status-/Tag-Kontextmenü in `TasksIslandView` | ✅ Code |
|
|
||||||
| **BlockedBy-Chains** (sequenzielle Subtask-Ausführung) | `TaskStateService.BlockOn`/`UnblockAsync`, `QueuePicker` filter `BlockedByTaskId IS NULL`, `PlanningChainCoordinator.OnChildFinishedAsync` | Drittes Feld neben `Status` und `PlanningPhase` | ✅ Code, Migration `20260423154708_AddPlanningSupport` |
|
## Offene Code-Punkte
|
||||||
| **Worker-State-Konsolidierung** | `TaskStateService` ist alleiniger Owner von `Status`/`PlanningPhase`/`BlockedByTaskId`-Writes; `OverrideSlotService` ausgelagert; `QueueWaker` + `QueuePicker` getrennt | — | ✅ Code |
|
|
||||||
| **MarkdownView / Tabbed Settings / About-Modal / Prime-Status-Footer / Doppelklick-Edit** | — | `Views/MarkdownView`, `SettingsModalView` als `TabControl`, `AboutModalView`, transient Prime-Status in Footer, `DoubleTapped` an List/Task-Rows | ✅ Code |
|
- **Status-Bar Live-Update:** Prüfen, ob `RunNow`-Enable/Disable pro Task-Row bei Connection-Change sauber re-evaluiert. Connection-Status lebt in `IslandsShellViewModel` / `WorkerConnectionModalViewModel` (es gibt keinen `StatusBarViewModel` mehr). Erst messen, dann ggf. fixen. Klein.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Verification (vor allem anderen)
|
## Bewusst verworfen (nicht erneut vorschlagen)
|
||||||
|
|
||||||
Der Großteil der Verification-Steps aus `plan.md` ist im Code abgedeckt — was fehlt ist die **manuelle Bestätigung mit explizit notiertem Pass-Kriterium**. Ohne falsifizierbare Observable produziert ein Manual-Run nur "sah ok aus".
|
- **CI-Build/Test-Pipeline** — push-to-main + release-on-push deckt das ab; Tests laufen am Ende jeder Session.
|
||||||
|
- **Real-`claude`-Smoke-Test als xUnit-Test** — kein Claude in `dotnet test`; bleibt manueller Check (siehe oben). Tests nutzen `FakeClaudeProcess`.
|
||||||
### 1.0 Plan-Verification 1–13
|
- **`architecture.md` / ADRs** — die per-Projekt-`CLAUDE.md`-Dateien sind die lebende Doku; ADRs lohnen solo nicht.
|
||||||
|
- **Task-Mailbox-Integration** — geparkt; das generische `mcp__mailbox__*`-Plugin reicht (Begründung in `mailbox-proposal.md`).
|
||||||
| # | Item | Status | Pass-Kriterium (was muss konkret zu sehen sein) |
|
- **Tag-Negation, Tag-Multi-Select, Notes-`lists.kind`-Switch, Install-Service-Skript** — durch die aktuelle Architektur überholt (Tag-System entfernt, Notes/Autostart anders gelöst).
|
||||||
|---|------|---|---|
|
|
||||||
| 1 | Schema-Init | ✅ | `~/.todo-app/todo.db` + `*-wal` + `*-shm` existieren; EF-Migrationsverlauf in `__EFMigrationsHistory` enthält alle 8 Migrationen; Worker-Log: „listening on …" |
|
|
||||||
| 1a | SignalR-Endpoint | ✅ | `curl http://127.0.0.1:47821/hub` → HTTP 400 (kein Handshake) |
|
|
||||||
| 1b | Hub-Roundtrip `Ping` | 🟡 | UI-Statusbar zeigt „Connected"; `WorkerClient.PingAsync()` liefert `"pong"` (UI-Test fehlt) |
|
|
||||||
| 2 | `claude --version` Preflight | ✅ | `Worker/Lifecycle/ClaudeCliPreflight.cs` + Wiring in `Program.cs`. Kaputter `claude_bin` → `LogCritical(...) + Environment.Exit(1)`. Skip via `CLAUDEDO_SKIP_CLI_PREFLIGHT=1`. Tests: `tests/.../Lifecycle/ClaudeCliPreflightTests.cs` |
|
|
||||||
| 3 | Smoke-Spawn (`claude -p` Prompt „ping") | ⬜ | `task_runs`-Row mit `session_id NOT NULL`, `result` non-empty, `output_tokens > 0` |
|
|
||||||
| 4 | E2E Happy Path (Non-Worktree) | ⬜ | Liste „Test" anlegen → Task „Schreibe ein Haiku über Intralogistik" → `tasks.status='Done'`, `tasks.result IS NOT NULL`, Logfile unter `~/.todo-app/logs/<taskId>.ndjson`, UI-Row mit Done-Badge |
|
|
||||||
| 5 | Worktree Happy Path | ⬜ | Liste mit `working_dir` auf temp-Repo, Task mit Codeänderung → `worktrees.state='active'`, `head_commit IS NOT NULL`, `diff_stat` non-empty, Branch `claudedo/<id>` auf Disk |
|
|
||||||
| 6 | No-Changes-Run | ⬜ | Prompt der nichts ändert → `tasks.status='Done'` aber `worktrees.head_commit IS NULL`, `diff_stat IS NULL` |
|
|
||||||
| 7 | Kein Git-Repo | ⬜ | `working_dir=C:\Temp` (kein Repo) → `tasks.status='Failed'`, **keine** `worktrees`-Row, Worker-Log enthält Git-Fehler |
|
|
||||||
| 8 | Merge-UI | 🟡 | `MergeTask`-Hub-Methode + `MergeModalView` vorhanden, manueller Run nicht durchgespielt → `worktrees.state='merged'`, im Ziel-Repo `git log` zeigt Commit, `git worktree list` ohne Branch |
|
|
||||||
| 9 | Override-Parallelität | 🟡 | `OverrideSlotService`-Tests grün; UI-E2E nicht durchgespielt → `WorkerHub.GetActive` ≥ 2 Einträge bei Run+RunNow |
|
|
||||||
| 10 | Schedule | 🟡 | `QueuePicker`-Tests grün; UI-E2E nicht → `scheduled_for=now+2min` bleibt Queued, dann automatisch Running, `started_at >= scheduled_for` |
|
|
||||||
| 11 | Worker-Offline-Erkennung | 🟡 | `WorkerClient.OnServerConnectionClosed` + Auto-Reconnect implementiert (`WithAutomaticReconnect`); visuell prüfen: nach `taskkill` der Worker-Exe wechselt Statusbar in ≤ 5s auf „Offline", `RunNow`-Buttons disabled |
|
|
||||||
| 12 | Live-Stream | 🟡 | `ClaudeProcess` streamt NDJSON via `TaskMessage`-Event, UI hat `LiveTail`; visuell prüfen: während Run laufen ndjson-Zeilen ein |
|
|
||||||
| 13 | Wake-up (`WakeQueue` nach Anlage) | 🟡 | `QueueWaker.Wake()` wird bei Enqueue aufgerufen; visuell prüfen: Task wechselt in ≤ 1s auf Running (statt nach `queue_backstop_interval_ms`=30s) |
|
|
||||||
|
|
||||||
**Empfohlener Sprint:** Steps 3–7 in einem Rutsch durchspielen (alles non-UI), parallel daneben 8–13 visuell beim normalen App-Lauf abhaken.
|
|
||||||
|
|
||||||
### 1.1 Planning Sessions — Manual Verification (unverändert relevant)
|
|
||||||
|
|
||||||
Bedingt durch Slice "Planning B/C". Ablauf identisch zur alten open.md:
|
|
||||||
|
|
||||||
1. Manual-Task mit Title + TODO-Description anlegen.
|
|
||||||
2. Rechtsklick → **Open planning Session** → Windows Terminal mit Claude CLI öffnet.
|
|
||||||
3. In CLI: zwei Children via `mcp__claudedo__create_child_task` anlegen.
|
|
||||||
4. UI: Drafts erscheinen eingerückt, italic, mit `DRAFT`-Badge; Parent zeigt `PLANNING`-Badge.
|
|
||||||
5. Chevron klappt ein/aus.
|
|
||||||
6. CLI `finalize` → Children werden Queued (erste) bzw. Queued+BlockedBy (Rest); Parent flippt von `Active` auf `Finalized` (`PLANNED`-Badge); erste Child startet automatisch.
|
|
||||||
7. Neuer Planning-Task, Terminal ohne Finalize schließen → Rechtsklick öffnet Resume/Finalize-now/Discard-Modal.
|
|
||||||
8. Delete-Versuch auf Parent mit Children → freundlicher Fehlerdialog, kein Delete.
|
|
||||||
|
|
||||||
**Bekannte Follow-ups (non-blocking):**
|
|
||||||
- ✅ `Border.badge.planned` (blau) wird jetzt bei `Finalized` angewendet — `TaskRowView` nutzt `Classes.planning`/`Classes.planned` gebunden an `IsPlanActive`/`IsPlanFinalized`; der Child-„PLANNED"-Badge nutzt direkt `planned`.
|
|
||||||
- ✅ Tote `Instance`-Statics auf `BoolToItalicConverter` und `BoolToDraftOpacityConverter` entfernt (Registrierung läuft über das Resource-Dictionary in `App.axaml`).
|
|
||||||
- ✅ `Ui.Tests` IWorkerClient-Fakes auf gemeinsame Basis `StubWorkerClient` rebased — kein Constructor-Drift mehr; die drei Fakes überschreiben nur ihre relevanten Member.
|
|
||||||
|
|
||||||
### 1.2 Prime Claude — Manual Verification
|
|
||||||
|
|
||||||
Slice "Prime" (Recurrence-Scheduler).
|
|
||||||
|
|
||||||
1. Settings → Prime-Claude-Tab → Schedule mit `at: 09:00`, `every: workday`, `task_template: "Daily Standup"` anlegen.
|
|
||||||
2. Test mit verschobenem `IPrimeClock` (oder Schedule mit `at: now+1min`) → bei Trigger erscheint Toast/Footer-Notification „Prime fired", neuer Task entsteht in der Ziel-Liste.
|
|
||||||
3. Worker-Restart innerhalb des Schedule-Fensters → Catch-up läuft genau einmal (kein Doppelfeuer).
|
|
||||||
4. Schedule editieren → `next_due_at` wird neu berechnet; UI-Anzeige aktualisiert.
|
|
||||||
5. Schedule löschen → keine weiteren Trigger, keine ghost-Tasks.
|
|
||||||
|
|
||||||
### 1.3 Self-Update — Manual Verification (aus alter open.md, weiterhin gültig)
|
|
||||||
|
|
||||||
Voraussetzung: funktionierendes Gitea-Release unter `git.kuns.dev/releases/ClaudeDo` mit drei Assets — `ClaudeDo-<version>-win-x64.zip`, `ClaudeDo.Installer-<version>.exe`, `checksums.txt`.
|
|
||||||
|
|
||||||
1. Baseline-Version (z.B. `0.2.x`) normal installieren.
|
|
||||||
2. Neues Release `v0.3.0` mit frischem Installer + App-Zip + Checksums veröffentlichen.
|
|
||||||
3. App starten → Banner erscheint: `Update available: v0.2.x → v0.3.0`.
|
|
||||||
4. **Update now** klicken → App schließt, Installer öffnet im Update-Mode, läuft, restartet Worker.
|
|
||||||
5. App neu starten → Banner weg; `Help → Check for updates` zeigt kurz „You're up to date (v0.3.0)".
|
|
||||||
6. `v0.2.x`-Installer manuell starten → bietet Self-Update auf v0.3.0 an. **Update** → laufende Exe wird ersetzt, Wizard öffnet auf neuer Version.
|
|
||||||
7. Schritt 6 mit **Continue anyway** → Wizard öffnet ohne Self-Update.
|
|
||||||
8. Schritt 6 mit **Cancel** → Installer beendet ohne Aktion.
|
|
||||||
9. Network-Kill in App und Installer beim Start → silent fallback (kein Error, kein Banner).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. UI-Polish
|
|
||||||
|
|
||||||
### 2.1 Folder-Picker für `Working Directory` ⬜
|
|
||||||
- **Datei:** `Views/ListSettingsModalView.axaml` + zugehöriges VM
|
|
||||||
- **Aktuell:** plain `TextBox` — Pfad muss getippt werden.
|
|
||||||
- **Soll:** Button „…" daneben → öffnet `IStorageProvider.OpenFolderPickerAsync`, schreibt Pfad ins Feld.
|
|
||||||
- **Aufwand:** klein.
|
|
||||||
|
|
||||||
### 2.2 Delete-Confirmation ⬜
|
|
||||||
- **Aktuell:** Listen/Tasks-Delete läuft direkt ohne Rückfrage. Datenverlust-Risiko.
|
|
||||||
- **Soll:** generischer `ConfirmDialog` (1× bauen, mehrfach nutzen), Mini-Dialog „Wirklich löschen?".
|
|
||||||
- **Aufwand:** klein.
|
|
||||||
|
|
||||||
### 2.3 Markdown-Rendering Result + Description ✅
|
|
||||||
- `Views/MarkdownView.axaml` + Detail-Pane verwenden Markdown.Avalonia.
|
|
||||||
|
|
||||||
### 2.4 Live-Log Auto-Scroll ⬜
|
|
||||||
- **Datei:** `Views/DetailsIslandView.axaml(.cs)` (Live-Tail-Section)
|
|
||||||
- **Aktuell:** ndjson-Zeilen werden angehängt, Scrollposition bleibt stehen.
|
|
||||||
- **Soll:** Sticky-Bottom-Pattern — bei jeder neuen Zeile `ScrollToEnd()`, solange User nicht manuell hochgescrollt hat. Attached-Behavior reicht.
|
|
||||||
|
|
||||||
### 2.5 Diff-Viewer 🟡
|
|
||||||
- `DiffModalView.axaml` + `PlanningDiffView` existieren; integriert für Planning-Merges.
|
|
||||||
- **Offen:** Task-Level-Diff (Worktree vs. main) noch nicht im Modal-Flow geprüft. Verwenden statt `Process.Start("cmd /k git diff …")`.
|
|
||||||
|
|
||||||
### 2.6 Status-Bar Active-Tasks Live-Update ⬜
|
|
||||||
- **Datei:** `ViewModels/StatusBarViewModel`
|
|
||||||
- **Risiko:** `RunNowCommand.NotifyCanExecuteChanged` triggert nicht pro Item bei Connection-Change.
|
|
||||||
- **Soll:** `WeakReferenceMessenger`-Connection-Change-Message; alle `TaskRowViewModel` lauschen.
|
|
||||||
- **Aufwand:** klein, muss sauber gemacht werden.
|
|
||||||
|
|
||||||
### 2.7 Settings-Dialog ✅
|
|
||||||
- `SettingsModalView` als `TabControl`, Tabs: General, Prime Claude, etc. Persistiert in `~/.todo-app/ui.config.json` und `worker.config.json`.
|
|
||||||
|
|
||||||
### 2.8 (NEU) Planning-Phase Badge-Farbe für `Finalized` ✅
|
|
||||||
`Finalized` zeigt jetzt den blauen `planned`-Badge (Class-Binding in `TaskRowView`).
|
|
||||||
|
|
||||||
### 2.9 (NEU) Tote Converter-Statics entfernen ✅
|
|
||||||
`BoolToItalicConverter.Instance`, `BoolToDraftOpacityConverter.Instance` entfernt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Worker-Robustheit
|
|
||||||
|
|
||||||
### 3.1 CLI-Preflight beim Worker-Start ✅
|
|
||||||
- `src/ClaudeDo.Worker/Lifecycle/ClaudeCliPreflight.cs` + Wiring in `Program.cs`. Tests: `tests/.../Lifecycle/ClaudeCliPreflightTests.cs`. Skippable via `CLAUDEDO_SKIP_CLI_PREFLIGHT=1`.
|
|
||||||
|
|
||||||
### 3.2 Worktree-Cleanup beim Anlege-Failed ⬜
|
|
||||||
- **Datei:** `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
|
|
||||||
- **Soll:** try/finally — bei Fehler zwischen `git worktree add` und DB-Insert `git worktree remove --force` als Best-Effort-Cleanup.
|
|
||||||
- **Aufwand:** klein.
|
|
||||||
|
|
||||||
### 3.3 Logging über file-Sink ⬜
|
|
||||||
- ILogger ist überall verdrahtet, aber kein File-Sink konfiguriert.
|
|
||||||
- **Soll:** Serilog oder `Karambolage.Extensions.Logging.File` — für Service-Modus zwingend, console-only ist im SCM-Fenster verloren.
|
|
||||||
- **Aufwand:** klein.
|
|
||||||
|
|
||||||
### 3.4 Tag-Negation / Exclusion ⬜
|
|
||||||
- Tags sind weiterhin rein additiv (`list_tags ∪ task_tags`). Nach Slice „Editierbare Tags" weniger dringend, aber nicht gelöst.
|
|
||||||
- **Soll:** entweder neue Tabelle `task_tag_exclusions` oder Prefix `!tag` im task_tags-Eintrag.
|
|
||||||
- **Aufwand:** mittel — Schema + Repo + Tests + UI.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Service-Deployment
|
|
||||||
|
|
||||||
### 4.1 Worker-Autostart via Startup-Shortcut ✅ (ersetzt Scheduled Task + Windows-Service)
|
|
||||||
- Der Worker läuft als `WinExe` (kein Konsolenfenster) + Serilog-File-Sink (`~/.todo-app/logs/worker-*.log`) + Single-Instance-Mutex.
|
|
||||||
- Autostart über eine **Startup-Ordner-Verknüpfung** `ClaudeDo Worker.lnk` (`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\`), die der Installer via `AutostartShortcut`/`ShortcutFactory` COM-Helper anlegt. Kein Scheduled Task, kein Windows-Service.
|
|
||||||
- `StartWorkerStep` startet den Worker per `Process.Start`; `StopWorkerStep` beendet ihn per prozessbasiertem Kill.
|
|
||||||
- Die App (`IslandsShellViewModel`) startet den Worker nicht selbst. Bei offline-Worker ~12s nach App-Start: einmaliges `WorkerConnectionModal` (Start Worker / Rerun Installer / Dismiss); Connection-Status-Pill in der Fußzeile ist ein Button zum erneuten Öffnen des Modals.
|
|
||||||
- `UninstallRunner` löscht die Startup-`.lnk`; migriert ältere Installs durch best-effort-Löschen des Legacy-Scheduled-Tasks „ClaudeDoWorker" und des Legacy-Windows-Service.
|
|
||||||
- **Manuelle E2E-Verifikation am Gerät ausstehend** (Logoff/Logon-Autostart, Update-Pfad, Uninstall).
|
|
||||||
|
|
||||||
### 4.2 Pfad-Auflösung absolut ✅
|
|
||||||
- `WorkerConfig.Load` expandiert `~`/`%USERPROFILE%` für alle Pfad-Felder.
|
|
||||||
|
|
||||||
### 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 Installer-Projekt ✅
|
|
||||||
- `ClaudeDo.Installer` (WPF) + `ClaudeDo.Releases` mit Pages/Steps/Core/Theme — Self-Update funktioniert (siehe §1.3).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Tests / CI
|
|
||||||
|
|
||||||
### 5.1 CI-Pipeline (Gitea Actions) ⬜
|
|
||||||
- **Datei (neu):** `.gitea/workflows/ci.yml`
|
|
||||||
- **Inhalt:** `dotnet restore` → `dotnet build` (csproj-weise wegen `.slnx`-Bug auf .NET 8) → `dotnet test`. Auf Push + PR.
|
|
||||||
- **Achtung:** Pipeline darf NICHT die `.slnx` als Build-Target nehmen — explizite csproj-Liste in einem checked-in Build-Skript.
|
|
||||||
- **Aufwand:** klein.
|
|
||||||
|
|
||||||
### 5.2 SignalR-Hub-Tests ✅
|
|
||||||
- `tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs`, `AgentSettingsHubTests.cs` testen Hub-Methoden via Fakes (kein realer SignalR-Roundtrip, aber alle Code-Pfade abgedeckt).
|
|
||||||
- **Optional verbleibt:** echter Roundtrip-Test mit `WebApplicationFactory<Program>` + `HubConnectionBuilder` für End-to-End-Validierung der SignalR-Pipeline. Niedriger Mehrwert solange Fakes alle Methoden treffen.
|
|
||||||
|
|
||||||
### 5.3 Smoke-Test gegen echten `claude` ⬜
|
|
||||||
- **Datei (neu):** `tests/ClaudeDo.Worker.Tests/Runner/ClaudeProcessSmokeTest.cs`
|
|
||||||
- **Soll:** Real-CLI-Test mit `[Fact(Skip="...")]` ausgegraut, nur lokal aktiviert wenn `CLAUDE_AUTHENTICATED=1` Env-Var gesetzt ist.
|
|
||||||
- **Aufwand:** klein.
|
|
||||||
|
|
||||||
### 5.4 (NEU) ExternalMcpService-Tests ⬜
|
|
||||||
- `External/ExternalMcpService` hat 11 Tools, aber nur partielle Coverage in `tests/.../External/ExternalMcpServiceTests.cs`. Für jedes Tool mindestens einen Happy-Path + einen Error-Pfad ergänzen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Dokumentation
|
|
||||||
|
|
||||||
### 6.1 README.md ⬜
|
|
||||||
- Komplett fehlt. Mind. 1× kurz: was ist es, wie starten (Worker + UI), wo Config, wie Self-Update.
|
|
||||||
|
|
||||||
### 6.2 docs/architecture.md 🟡
|
|
||||||
- In `plan.md` enthalten — entweder konsolidieren oder explizit ausgliedern. CLAUDE.md-Dateien pro Projekt sind aktuell de-facto-Architecture-Doc.
|
|
||||||
|
|
||||||
### 6.3 ADRs ⬜
|
|
||||||
- Vorschläge: „SignalR vs. SQLite-Polling für IPC", „Worktree pro Task", „TaskStateService als alleiniger State-Owner", „BlockedByTaskId statt Status='Waiting'", „External MCP als zweite WebApplication".
|
|
||||||
- **Aufwand:** klein, hilfreich für später.
|
|
||||||
|
|
||||||
### 6.4 (NEU) Mailbox-Proposal ⬜
|
|
||||||
- `docs/mailbox-proposal.md` ist als Vorschlag vorhanden, nicht implementiert. Entscheidung: bauen, droppen oder parken? Wenn droppen → Datei entfernen, sonst klare Roadmap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Bekannte Code-Schulden / Smells
|
|
||||||
|
|
||||||
| Stelle | Issue | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `WorkerHub.GetActive` returnt `IReadOnlyList<object>` mit anonymen Typen | Sollte expliziter `ActiveTaskDto` sein, den Worker UND UI teilen | ✅ (gibt bereits `IReadOnlyList<ActiveTaskDto>` zurück) |
|
|
||||||
| `TaskRunner` führt eine `if (list.WorkingDir != null)`-Verzweigung mitten in der Methode | Strategy-Pattern wenn die Methode wächst, aktuell noch klein genug | ⬜ |
|
|
||||||
| `App.Services` als public static `ServiceProvider` | Service-Locator-Antipattern, toleriert weil nur in `App.OnFrameworkInitializationCompleted` | ⬜ |
|
|
||||||
| Embedded `schema.sql` ohne Versionierung | Durch EF-Core-Migrationen ersetzt | ✅ |
|
|
||||||
| CRLF-Warnings beim Commit | `.gitattributes` mit `* text=auto eol=lf` (oder explizit pro Sprache) wäre sauberer | ✅ (`.gitattributes` angelegt) |
|
|
||||||
| Tote Converter-Instances (`BoolToItalicConverter.Instance`, `BoolToDraftOpacityConverter.Instance`) | Per Resource-Dictionary registriert, Statics ungenutzt | ✅ (entfernt) |
|
|
||||||
| 1 unausgeführter `// TODO` in `DetailsIslandViewModel` (`SendPromptAsync` ohne Hub-Methode) | Entweder Hub-Methode bauen oder TODO entfernen | ✅ (im Main-Code nicht mehr vorhanden) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Improvement Plan (improvement-plan.md, Stand 2026-04-13)
|
|
||||||
|
|
||||||
| ID | Item | Status | Bemerkung |
|
|
||||||
|---|---|---|---|
|
|
||||||
| IP-1 | UI ↔ Worker Auto-Reconnect | ✅ | `WorkerClient` mit `WithAutomaticReconnect()` + Reconnect-Handler |
|
|
||||||
| IP-2 | Listen-Modus „Notes" (non-autonomous) | ⬜ | Nach Slice „editierbare Status/Tags" weniger dringend (man kann jetzt einen Task ohne `agent`-Tag idle lassen), aber `lists.kind` als sauberer Mode-Switch fehlt. |
|
|
||||||
| IP-3 | Doppelklick öffnet Edit-Dialog | ✅ | `DoubleTapped`-Handler in `ListsIslandView`, `TasksIslandView` |
|
|
||||||
| IP-4 | Tag Multi-Select Control | ⬜ | Tags sind via Picker im Detail-Pane editierbar, aber kein dediziertes Multi-Select-Control mit Auto-Vervollständigung in Editor-Dialogen. |
|
|
||||||
| IP-5 | Rechtsklick-Kontextmenü | ✅ | Listen + Tasks haben Context-Menüs (Edit, Delete, Run Now, Show Diff, Merge, Cancel, Status, Tags) |
|
|
||||||
| IP-6 | Schema-Migration-Mechanismus | ✅ | EF-Core-Migrations + `__EFMigrationsHistory` |
|
|
||||||
| IP-7 | Status-Bar Reconnect-States | ✅ | `connected`/`connecting`/`reconnecting`/`offline` farbcodiert |
|
|
||||||
| IP-8 | Tag-Repository `GetAllKnownTagsAsync` | ✅ | `TagRepository.GetAllAsync` + `WorkerClient.GetAllTagsAsync` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Empfohlene Reihenfolge für die nächsten Sessions
|
|
||||||
|
|
||||||
**Block 1 — Verification durchspielen** (kein neuer Code, nur Beweis):
|
|
||||||
1. §1.0 Steps 3–7 manuell (Smoke + E2E + Worktree + No-Changes + Kein-Repo) — ist die Pipeline wirklich lebendig?
|
|
||||||
2. §1.1 Planning-Walkthrough — nach den uncommitted Coordinator-Änderungen einmal durchspielen.
|
|
||||||
3. §1.2 Prime-Walkthrough — Schedule-Trigger einmal beobachten.
|
|
||||||
|
|
||||||
**Block 2 — Niedrig hängende UI-Polish** (eine Session):
|
|
||||||
4. §2.1 Folder-Picker
|
|
||||||
5. §2.2 Delete-Confirmation
|
|
||||||
6. §2.4 Live-Log Auto-Scroll
|
|
||||||
7. §2.6 Status-Bar Live-Update
|
|
||||||
8. §2.8 Planning-Badge-Farbe + §2.9 tote Converter weg
|
|
||||||
|
|
||||||
**Block 3 — Robustheit & Service-Deployment**:
|
|
||||||
9. §3.2 Worktree-Cleanup
|
|
||||||
10. §3.3 File-Sink-Logging
|
|
||||||
11. §4.3 Install-Skripte/Doku
|
|
||||||
|
|
||||||
**Block 4 — Sicherheitsnetz**:
|
|
||||||
12. §5.1 Gitea-Actions CI-Pipeline (csproj-weise)
|
|
||||||
13. §5.3 Smoke-Test gegen echten claude
|
|
||||||
14. §5.4 ExternalMcpService-Tests vervollständigen
|
|
||||||
|
|
||||||
**Block 5 — Dokumentation & Aufräumen**:
|
|
||||||
15. §6.1 README
|
|
||||||
16. §6.3 ADRs (mind. die fünf wichtigsten)
|
|
||||||
17. §6.4 Mailbox-Proposal: bauen/droppen entscheiden
|
|
||||||
18. §7 Smells: `ActiveTaskDto`, `.gitattributes`, TODO-Comment
|
|
||||||
|
|
||||||
**Block 6 — Optional / wenn Bedarf konkret wird**:
|
|
||||||
19. §3.4 Tag-Negation
|
|
||||||
20. §IP-2 Notes-Modus
|
|
||||||
21. §IP-4 Tag Multi-Select Control
|
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ Snapshot of every string ClaudeDo sends to Claude CLI, plus the CLI-flag surface
|
|||||||
|
|
||||||
Date: 2026-04-24
|
Date: 2026-04-24
|
||||||
|
|
||||||
|
> **Update 2026-06-04 — prompts externalized.** All prose prompts now live as
|
||||||
|
> editable files under `~/.todo-app/prompts/`, each seeded from a bundled default in
|
||||||
|
> `src/ClaudeDo.Data/PromptFiles.cs` (read via `ReadOrDefault` / `Render`, which
|
||||||
|
> substitutes only named `{tokens}`):
|
||||||
|
> `system.md`, `planning-system.md`, `planning-initial.md` (`{title}`/`{description}`),
|
||||||
|
> `retry.md`, `daily-prep.md` (`{date}`/`{maxTasks}`), `weekly-report.md`
|
||||||
|
> (`{start}`/`{end}`; German output). The old `agent.md` and `planning.md` are
|
||||||
|
> retired — `system.md` is the single appended system prompt (the agent/manual split
|
||||||
|
> is gone), and the planning system prompt is `planning-system.md`. Daily-prep and
|
||||||
|
> retry prompts are now English; retry leans on the resumed session and appends the
|
||||||
|
> captured stderr only when it's a real error (not the generic "exited with code N").
|
||||||
|
> The system prompt instructs the agent to emit `CLAUDEDO_BLOCKED: <reason>` on its
|
||||||
|
> own line for any true blocker; `StreamAnalyzer` collects every marker, strips them
|
||||||
|
> from the result, and `TaskRunner` folds them into the review result as a
|
||||||
|
> "⚠ Roadblocks" section. All six prompt files are editable from Settings → Files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Task-execution prompts (agent-tagged tasks → Claude CLI)
|
## 1. Task-execution prompts (agent-tagged tasks → Claude CLI)
|
||||||
|
|||||||
517
docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
Normal file
517
docs/superpowers/plans/2026-06-03-daily-prep-live-view.md
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
# Daily Prep — Live Output View + Clear Day — 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:** Stream the daily-prep run's output into a live, human-readable view (a new mode in the Details island), and add a "Clear Day" button that empties MyDay.
|
||||||
|
|
||||||
|
**Architecture:** The worker broadcasts `PrepStarted/PrepLine/PrepFinished` over SignalR (mirroring `TaskStarted/TaskMessage/TaskFinished`). `PrimeRunner` forwards each Claude stdout line instead of discarding it. The UI `WorkerClient` re-raises these as events; `DetailsIslandViewModel` gains a `PrepLog` + `IsPrepMode` panel rendered with the existing terminal renderer. A `ClearMyDay` hub method bulk-clears `IsMyDay`. MyDay header gets "Vorbereitungs-Log" and "Tag leeren" buttons.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, ASP.NET Core SignalR, EF Core (SQLite), Avalonia + CommunityToolkit.Mvvm, xUnit.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build & test commands
|
||||||
|
|
||||||
|
`.slnx` needs .NET 9; build/test individual csproj with `-c Release` (a running Worker may lock Debug).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
UI cannot be GUI-smoke-tested headlessly — note that explicitly where it applies; the human verifies visuals.
|
||||||
|
|
||||||
|
## Reference anchors (verify before editing — line numbers drift)
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs` — currently only `PrimeFiredAsync`.
|
||||||
|
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs:13-57` — broadcast methods; `PrimeFired` at ~52-56.
|
||||||
|
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs:31-79` — `FireAsync`; discard lambda at ~55-60; ctor at ~19-29.
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs:542-549` — `RunDailyPrepNow` (uses `_broadcaster`); DailyNote CRUD at 559-583 (shows the db-context pattern this hub uses).
|
||||||
|
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:19` — `TaskMessageEvent`; `:55` — `RunDailyPrepNowAsync`.
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs:99-122` — `TaskStarted/Finished/Message` hub.On; `:170-173` — `PrimeFired` hub.On (the pattern to copy).
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — `IsNotesMode` ~56, `Log` ~193, ctor/subscriptions ~272-337, `OnTaskMessage` ~339-363 (stdout→`StreamLineFormatter`→`Log`), `ShowNotes` ~478-483.
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml:131-302` — body grid; task panel `IsVisible="{Binding !IsNotesMode}"`, notes panel `IsVisible="{Binding IsNotesMode}"`; `SessionTerminalView` embedded ~295.
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml:54-75` — `ItemsControl ItemsSource="{Binding Log}"` + the `LogLineViewModel` item template to reuse.
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` — `NotesRequested` ~29, `OpenNotesCommand`+`PrepareDayCommand` ~33-45, `ShowNotesRow`/`IsMyDayList` ~65-66, both set in `LoadForList` ~212-213.
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml:69-84` — Notes + PrepareDay buttons (styling to copy).
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs:199-201` — island event wiring; `:225` — `PrimeFired` subscription.
|
||||||
|
- Fakes to keep in sync: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (`FakeWorkerClient`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Worker — prep output broadcast + streaming
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test.** Extend `PrimeRunnerTests` with a fake `IPrimeBroadcaster` that records calls. The fake `IClaudeProcess` should invoke `onStdoutLine` with two sample lines and return `RunResult { ExitCode = 0, ResultMarkdown = "ok" }`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task FireAsync_streams_started_lines_and_finished()
|
||||||
|
{
|
||||||
|
var broadcaster = new RecordingPrimeBroadcaster();
|
||||||
|
var claude = new FakeClaudeProcess(emitLines: new[] { "{\"a\":1}", "{\"b\":2}" }, exitCode: 0, result: "ok");
|
||||||
|
var runner = NewRunner(claude, broadcaster); // build with temp-sqlite dbFactory + fake clock + logger + broadcaster
|
||||||
|
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||||
|
|
||||||
|
var outcome = await runner.FireAsync(schedule, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(outcome.Success);
|
||||||
|
Assert.Equal(1, broadcaster.StartedCount);
|
||||||
|
Assert.Equal(new[] { "{\"a\":1}", "{\"b\":2}" }, broadcaster.Lines);
|
||||||
|
Assert.Single(broadcaster.FinishedResults);
|
||||||
|
Assert.True(broadcaster.FinishedResults[0]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`RecordingPrimeBroadcaster` implements `IPrimeBroadcaster`: `StartedCount`, `List<string> Lines`, `List<bool> FinishedResults`, and a no-op `PrimeFiredAsync`. If the existing `FakeClaudeProcess` cannot emit lines, add an optional `emitLines` parameter that loops `await onStdoutLine(line)` before returning.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL** (interface methods + ctor param missing).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Extend `IPrimeBroadcaster`:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPrimeBroadcaster
|
||||||
|
{
|
||||||
|
Task PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
|
||||||
|
Task PrepStartedAsync();
|
||||||
|
Task PrepLineAsync(string line);
|
||||||
|
Task PrepFinishedAsync(bool success);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Keep the existing `PrimeFiredAsync` signature exactly as it is in the current file.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement in `HubBroadcaster`** (add next to `PrimeFired`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task PrepStarted() => _hub.Clients.All.SendAsync("PrepStarted");
|
||||||
|
public Task PrepLine(string line) => _hub.Clients.All.SendAsync("PrepLine", line);
|
||||||
|
public Task PrepFinished(bool success) => _hub.Clients.All.SendAsync("PrepFinished", success);
|
||||||
|
|
||||||
|
Task IPrimeBroadcaster.PrepStartedAsync() => PrepStarted();
|
||||||
|
Task IPrimeBroadcaster.PrepLineAsync(string line) => PrepLine(line);
|
||||||
|
Task IPrimeBroadcaster.PrepFinishedAsync(bool success) => PrepFinished(success);
|
||||||
|
```
|
||||||
|
|
||||||
|
(Match the existing explicit-interface style used for `PrimeFiredAsync`.)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Wire `PrimeRunner`.** Add `IPrimeBroadcaster _broadcaster` as a ctor param (and field). Rewrite the body of `FireAsync` after the gate check to:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (!await _gate.WaitAsync(0, ct))
|
||||||
|
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||||
|
|
||||||
|
var success = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _broadcaster.PrepStartedAsync();
|
||||||
|
|
||||||
|
var cwd = Paths.AppDataRoot();
|
||||||
|
Directory.CreateDirectory(cwd);
|
||||||
|
|
||||||
|
int maxTasks;
|
||||||
|
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||||
|
{
|
||||||
|
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
|
||||||
|
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
||||||
|
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||||
|
|
||||||
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
timeoutCts.CancelAfter(FireTimeout);
|
||||||
|
|
||||||
|
var result = await _claude.RunAsync(
|
||||||
|
arguments: args,
|
||||||
|
prompt: prompt,
|
||||||
|
workingDirectory: cwd,
|
||||||
|
onStdoutLine: line => _broadcaster.PrepLineAsync(line),
|
||||||
|
ct: timeoutCts.Token);
|
||||||
|
|
||||||
|
success = result.IsSuccess;
|
||||||
|
return success
|
||||||
|
? new PrimeRunOutcome(true, "Daily prep complete")
|
||||||
|
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Daily prep run failed");
|
||||||
|
return new PrimeRunOutcome(false, ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _broadcaster.PrepFinishedAsync(success);
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
DI is unchanged: `AddSingleton<IPrimeRunner, PrimeRunner>()` resolves `IPrimeBroadcaster` (registered as `sp => sp.GetRequiredService<HubBroadcaster>()`).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update existing `PrimeRunnerTests` ctor calls** to pass the recording broadcaster; build + run.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Prime src/ClaudeDo.Worker/Hub/HubBroadcaster.cs tests/ClaudeDo.Worker.Tests/Prime
|
||||||
|
git commit -m "feat(daily-prep): stream prep output via PrepStarted/PrepLine/PrepFinished"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Worker — `ClearMyDay` hub method
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- Test: a new/existing hub test under `tests/ClaudeDo.Worker.Tests/Hub/` (mirror an existing hub test that seeds a real SQLite db and constructs `WorkerHub`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test.** Seed three tasks: two with `IsMyDay=true` (one Idle, one Done), one with `IsMyDay=false`. Construct `WorkerHub` the way existing hub tests do (the same `null!` argument list, plus a recording `HubBroadcaster`/clients). Call `ClearMyDay()`; assert both MyDay rows are now `false`, the third is untouched, and the returned count is 2.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task ClearMyDay_clears_all_isMyDay_tasks()
|
||||||
|
{
|
||||||
|
// seed via the test's db helper ...
|
||||||
|
var hub = NewHub(/* ... */);
|
||||||
|
var cleared = await hub.ClearMyDay();
|
||||||
|
|
||||||
|
Assert.Equal(2, cleared);
|
||||||
|
await using var ctx = NewContext();
|
||||||
|
Assert.False(await ctx.Tasks.AnyAsync(t => t.IsMyDay));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the method** to `WorkerHub` (use the same db-context acquisition the neighbouring hub methods use — e.g. `_dbFactory`/repository field name found in the file — and the existing `_broadcaster` field):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<int> ClearMyDay()
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var ids = await ctx.Tasks.Where(t => t.IsMyDay).Select(t => t.Id).ToListAsync();
|
||||||
|
if (ids.Count == 0) return 0;
|
||||||
|
|
||||||
|
await ctx.Tasks.Where(t => t.IsMyDay)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.IsMyDay, false));
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
await _broadcaster.TaskUpdated(id);
|
||||||
|
|
||||||
|
return ids.Count;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `WorkerHub` does not already have an `IDbContextFactory<ClaudeDoDbContext>` field, use whatever data-access dependency the other hub methods use (read the file). Do NOT add a new ctor param unless unavoidable (it would break hub-test fakes — if you must, update all `new WorkerHub(...)` call sites).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run — expect PASS.** Build Worker.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Hub
|
||||||
|
git commit -m "feat(daily-prep): add ClearMyDay hub method"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: UI — WorkerClient prep events + ClearMyDayAsync
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- Modify fakes: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (FakeWorkerClient)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Declare on `IWorkerClient`** (near `TaskMessageEvent` / `RunDailyPrepNowAsync`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
event Action? PrepStartedEvent;
|
||||||
|
event Action<string>? PrepLineEvent;
|
||||||
|
event Action<bool>? PrepFinishedEvent;
|
||||||
|
Task ClearMyDayAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement in `WorkerClient`.** Add the events; register hub callbacks mirroring the `PrimeFired` registration (~line 170):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public event Action? PrepStartedEvent;
|
||||||
|
public event Action<string>? PrepLineEvent;
|
||||||
|
public event Action<bool>? PrepFinishedEvent;
|
||||||
|
|
||||||
|
// in the hub-wiring section:
|
||||||
|
_hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
|
||||||
|
_hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
|
||||||
|
_hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));
|
||||||
|
|
||||||
|
public Task ClearMyDayAsync() => _connection.InvokeAsync("ClearMyDay");
|
||||||
|
```
|
||||||
|
|
||||||
|
(Use the exact connection field name and async-call style of neighbouring methods like `RunDailyPrepNowAsync` / `GenerateWeekReport`. `ClearMyDay` returns `int` on the hub; invoking it as a void `InvokeAsync("ClearMyDay")` is fine, or `InvokeAsync<int>` if you want the count.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the fakes.** Add the three events (as `public event …` auto-implemented) and `ClearMyDayAsync() => Task.CompletedTask` to both `StubWorkerClient` and `FakeWorkerClient`. For the ClearDay command test (Task 5), give `StubWorkerClient` a `ClearMyDayCalls` counter incremented in `ClearMyDayAsync`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build App + both test projects; fix any remaining fake gaps.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Services tests
|
||||||
|
git commit -m "feat(daily-prep): expose prep stream events and ClearMyDay on the UI worker client"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: UI — Details island prep mode + live log
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/...DetailsIslandViewModel...` (mirror existing Details VM tests; if none, add a small test file)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test.** Construct `DetailsIslandViewModel` with a `StubWorkerClient` (mirror existing construction). Then:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void PrepLine_event_appends_to_PrepLog()
|
||||||
|
{
|
||||||
|
var stub = new StubWorkerClient();
|
||||||
|
var vm = NewDetailsVm(stub);
|
||||||
|
|
||||||
|
stub.RaisePrepLine("{\"type\":\"assistant\",\"text\":\"hi\"}"); // helper that invokes PrepLineEvent
|
||||||
|
Assert.NotEmpty(vm.PrepLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShowPrep_sets_prep_mode_and_clears_notes_mode()
|
||||||
|
{
|
||||||
|
var vm = NewDetailsVm(new StubWorkerClient());
|
||||||
|
vm.ShowPrep();
|
||||||
|
Assert.True(vm.IsPrepMode);
|
||||||
|
Assert.False(vm.IsNotesMode);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `RaisePrepStarted/RaisePrepLine/RaisePrepFinished` helpers to `StubWorkerClient` that invoke the corresponding events.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement in `DetailsIslandViewModel`:**
|
||||||
|
- Add `[ObservableProperty] private bool _isPrepMode;` and `[ObservableProperty] private bool _isPrepRunning;`.
|
||||||
|
- Add `public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();`.
|
||||||
|
- In the ctor, subscribe: `_worker.PrepStartedEvent += OnPrepStarted; _worker.PrepLineEvent += OnPrepLine; _worker.PrepFinishedEvent += OnPrepFinished;` (guard with the same `_worker is not null` pattern used for other events).
|
||||||
|
- Handlers:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void OnPrepStarted()
|
||||||
|
{
|
||||||
|
PrepLog.Clear();
|
||||||
|
IsPrepRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrepLine(string line) => AppendStdoutLine(PrepLog, line);
|
||||||
|
|
||||||
|
private void OnPrepFinished(bool success) => IsPrepRunning = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Factor the stdout-formatting currently inside `OnTaskMessage` into a reusable
|
||||||
|
`private void AppendStdoutLine(ObservableCollection<LogLineViewModel> target, string line)`
|
||||||
|
that runs the line through `StreamLineFormatter` and appends `LogLineViewModel`(s).
|
||||||
|
Have `OnTaskMessage`'s stdout branch call `AppendStdoutLine(Log, strippedLine)` so both
|
||||||
|
paths share one implementation. (Events arrive already on the UI thread via
|
||||||
|
`Dispatcher.UIThread.Post` in `WorkerClient`, so direct collection mutation is correct.)
|
||||||
|
- Add `public void ShowPrep()` mirroring `ShowNotes()`: call `Bind(null)`, set
|
||||||
|
`IsNotesMode = false`, `IsPrepMode = true`.
|
||||||
|
- In `ShowNotes()` add `IsPrepMode = false`. In `Bind(...)` reset both `IsNotesMode` and
|
||||||
|
`IsPrepMode` to false (find where `IsNotesMode` is reset; add `IsPrepMode` beside it).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update `DetailsIslandView.axaml`.**
|
||||||
|
- Change the task-details panel visibility from `IsVisible="{Binding !IsNotesMode}"` to a
|
||||||
|
converter-free multi-condition. Avalonia lacks `&&` in bindings, so add a computed
|
||||||
|
property `public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;` to the VM
|
||||||
|
(raise its change notification from the `OnIsNotesModeChanged`/`OnIsPrepModeChanged`
|
||||||
|
partial methods generated by `[ObservableProperty]`) and bind the task panel to
|
||||||
|
`IsVisible="{Binding IsTaskDetailVisible}"`.
|
||||||
|
- Add a third panel after the notes panel:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Panel IsVisible="{Binding IsPrepMode}">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Margin="16,12"
|
||||||
|
Text="{loc:Tr details.prepTitle}" Classes="h2"/>
|
||||||
|
<ScrollViewer>
|
||||||
|
<ItemsControl ItemsSource="{Binding PrepLog}"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Panel>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ItemsControl` reuses the implicit `LogLineViewModel` `DataTemplate` that
|
||||||
|
`SessionTerminalView` relies on. If that template is defined locally inside
|
||||||
|
`SessionTerminalView.axaml` (not in a shared resource), either move it to a shared
|
||||||
|
`ResourceDictionary` (e.g. App resources) and reference it from both, or set the
|
||||||
|
`ItemsControl.ItemTemplate` to a copy of that template. Prefer sharing over copying.
|
||||||
|
Add `details.prepTitle` ("Daily prep" / "Tagesvorbereitung") to both locale json files.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run UI tests — expect PASS; build App.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests/ClaudeDo.Ui.Tests
|
||||||
|
git commit -m "feat(daily-prep): add live prep-output mode to the Details island"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: UI — MyDay buttons + shell wiring
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/UiVm/...` or `tests/ClaudeDo.Ui.Tests/...` (TasksIslandViewModel)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task ClearDayCommand_calls_worker()
|
||||||
|
{
|
||||||
|
var stub = new StubWorkerClient();
|
||||||
|
var vm = NewTasksVm(stub);
|
||||||
|
await vm.ClearDayCommand.ExecuteAsync(null);
|
||||||
|
Assert.Equal(1, stub.ClearMyDayCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PrepareDayCommand_raises_PrepRequested()
|
||||||
|
{
|
||||||
|
var vm = NewTasksVm(new StubWorkerClient());
|
||||||
|
var raised = false;
|
||||||
|
vm.PrepRequested += () => raised = true;
|
||||||
|
await vm.PrepareDayCommand.ExecuteAsync(null);
|
||||||
|
Assert.True(raised);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement in `TasksIslandViewModel`:**
|
||||||
|
- Add `public event Action? PrepRequested;` next to `NotesRequested`.
|
||||||
|
- In `PrepareDayAsync` (the existing `[RelayCommand]`), raise `PrepRequested?.Invoke();`
|
||||||
|
in addition to the existing `RunDailyPrepNowAsync()` call.
|
||||||
|
- Add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowPrepLog() => PrepRequested?.Invoke();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ClearDayAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
try { await _worker.ClearMyDayAsync(); }
|
||||||
|
catch { /* worker offline; broadcast will reconcile on return */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the two buttons** to the MyDay header in `TasksIslandView.axaml`,
|
||||||
|
immediately after the existing "Prepare day" button (~line 84), copying its styling
|
||||||
|
(`DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left" Margin="16,0,16,8" IsVisible="{Binding IsMyDayList}"`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left" Margin="16,0,16,8"
|
||||||
|
IsVisible="{Binding IsMyDayList}"
|
||||||
|
Command="{Binding ShowPrepLogCommand}"
|
||||||
|
Content="{loc:Tr tasks.prepLog}"/>
|
||||||
|
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left" Margin="16,0,16,8"
|
||||||
|
IsVisible="{Binding IsMyDayList}"
|
||||||
|
Command="{Binding ClearDayCommand}"
|
||||||
|
Content="{loc:Tr tasks.clearDay}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `tasks.prepLog` (en "Prep log" / de "Vorbereitungs-Log") and `tasks.clearDay`
|
||||||
|
(en "Clear day" / de "Tag leeren") to both locale json files.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Wire the shell.** In `IslandsShellViewModel` where `Tasks.NotesRequested`
|
||||||
|
is wired (~line 201), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run tests + build App.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Manual smoke (human, not headless):** start Worker + App, open MyDay, click
|
||||||
|
"Tag vorbereiten" → Details island opens in prep mode and streams readable lines; click
|
||||||
|
"Tag leeren" → MyDay empties; after a scheduled run, "Vorbereitungs-Log" opens the filled
|
||||||
|
log. Confirm the three buttons only appear on MyDay.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests
|
||||||
|
git commit -m "feat(daily-prep): add Prep-log and Clear-day buttons to MyDay header"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification
|
||||||
|
|
||||||
|
- [ ] Build Worker + App (Release).
|
||||||
|
- [ ] `dotnet test` Worker.Tests, Ui.Tests, Localization.Tests — all green.
|
||||||
|
- [ ] Manual: prep streams live into the Details island (manual opens it; scheduled fills it silently, opened via the button); Clear Day empties MyDay immediately.
|
||||||
|
|
||||||
|
## Notes / risks
|
||||||
|
|
||||||
|
- Mode flags `IsNotesMode` / `IsPrepMode` are mutually exclusive; the task-details panel
|
||||||
|
uses the computed `IsTaskDetailVisible`. Verify all three modes switch cleanly.
|
||||||
|
- Reusing the `LogLineViewModel` template: prefer promoting it to a shared resource over
|
||||||
|
copying, to avoid drift between the session terminal and the prep log.
|
||||||
|
- `ClearMyDay` broadcasts one `TaskUpdated` per affected id; MyDay is small (capped), so
|
||||||
|
this is fine.
|
||||||
|
- Keep `PrimeRunner`'s "already running" early-return emitting no prep events.
|
||||||
736
docs/superpowers/plans/2026-06-03-daily-prep.md
Normal file
736
docs/superpowers/plans/2026-06-03-daily-prep.md
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
# Daily Prep ("Prime Claude") 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:** Turn the Prime Time warm-up into a daily preparation where Claude reads open tasks and moves an effort-aware, capped subset into MyDay, triggered by the Prime schedule and a manual button.
|
||||||
|
|
||||||
|
**Architecture:** Agentic. Two new tools on the always-on `ExternalMcpService` (`get_daily_prep_candidates`, `set_my_day` with a server-side cap-guard). The existing `PrimeRunner` is rewritten to launch a headless `claude -p` run with a fixed parameterized prompt and `--allowedTools` for those two tools, relying on the already-registered `claudedo` MCP (no separate `--mcp-config`). A new `DailyPrepMaxTasks` app setting drives the cap. A manual hub method reuses the same runner with a single-flight guard.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, ASP.NET Core, EF Core (SQLite), SignalR, ModelContextProtocol, Avalonia (CommunityToolkit.Mvvm), xUnit.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-06-03-daily-prep-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deviation from spec (deliberate, to minimize churn)
|
||||||
|
|
||||||
|
The spec proposed renaming `IPrimeRunner`/`PrimeRunner`/`PrimeScheduler` → `DailyPrep*`. **We keep the existing names and the `FireAsync(PrimeScheduleDto, ct)` signature** and only rewrite the runner body. This avoids touching the scheduler, DI registration, `IPrimeBroadcaster`, and the existing Prime tests for a pure rename. The per-schedule `PromptOverride` field becomes unused by the runner (left in the DB/UI untouched).
|
||||||
|
|
||||||
|
## Build & test commands (this repo)
|
||||||
|
|
||||||
|
`.slnx` needs .NET 9; on .NET 8 build/test individual projects. Use `-c Release` if a running Worker locks `Debug`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests use **real SQLite + real git** (project convention). Mirror the setup already present in the test file you are extending.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Create**
|
||||||
|
- `src/ClaudeDo.Data/Migrations/<timestamp>_DailyPrepMaxTasks.cs` (+ Designer, via `dotnet ef`)
|
||||||
|
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — pure prompt + args builder (easy to unit-test)
|
||||||
|
|
||||||
|
**Modify**
|
||||||
|
- `src/ClaudeDo.Data/Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`
|
||||||
|
- `src/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs` — map column
|
||||||
|
- `src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs` — persist field in `UpdateAsync`
|
||||||
|
- `src/ClaudeDo.Worker/External/ExternalMcpService.cs` — add 2 tools + DTOs
|
||||||
|
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs` — rewrite body to daily prep + single-flight
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — add `DailyPrepMaxTasks` to AppSettings DTO + `RunDailyPrepNow`
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — mirror `DailyPrepMaxTasks` in the UI AppSettings DTO + add `RunDailyPrepNow` call
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` (+ its view) — numeric editor for `DailyPrepMaxTasks`
|
||||||
|
- MyDay list header view + its ViewModel — "Tag vorbereiten" button + command
|
||||||
|
|
||||||
|
**Test**
|
||||||
|
- `tests/ClaudeDo.Data.Tests/...AppSettings...` — new field persists / default 5
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs` — candidate filter + set_my_day + cap-guard
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs` — prompt/args content
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs` (if present) — single-flight + success/failure via `IClaudeProcess` fake
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: `DailyPrepMaxTasks` app setting
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/Models/AppSettingsEntity.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Data/Configuration/AppSettingsEntityConfiguration.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Data/Repositories/AppSettingsRepository.cs`
|
||||||
|
- Create (via `dotnet ef`): `src/ClaudeDo.Data/Migrations/<timestamp>_DailyPrepMaxTasks.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Data.Tests` (extend existing AppSettings repository test, or add `AppSettingsRepositoryTests.cs`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
In a Data.Tests file (mirror the existing repo test harness that opens a real SQLite `ClaudeDoDbContext`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task DailyPrepMaxTasks_defaults_to_5_and_persists()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext(); // existing helper that migrates a temp sqlite db
|
||||||
|
var repo = new AppSettingsRepository(ctx);
|
||||||
|
|
||||||
|
var initial = await repo.GetAsync();
|
||||||
|
Assert.Equal(5, initial.DailyPrepMaxTasks);
|
||||||
|
|
||||||
|
initial.DailyPrepMaxTasks = 8;
|
||||||
|
await repo.UpdateAsync(initial);
|
||||||
|
|
||||||
|
var reloaded = await repo.GetAsync();
|
||||||
|
Assert.Equal(8, reloaded.DailyPrepMaxTasks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run it — expect FAIL** (`AppSettingsEntity` has no `DailyPrepMaxTasks`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --filter DailyPrepMaxTasks_defaults_to_5_and_persists
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the property** to `AppSettingsEntity.cs` after `StandupWeekday`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||||
|
public int DailyPrepMaxTasks { get; set; } = 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Map the column** in `AppSettingsEntityConfiguration.cs`, after the `StandupWeekday` mapping (before `builder.HasData(...)`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Property(s => s.DailyPrepMaxTasks)
|
||||||
|
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Persist it** in `AppSettingsRepository.UpdateAsync`, after the `StandupWeekday` assignment:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Generate the migration** (regenerates the model snapshot — do NOT hand-edit the snapshot):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet ef migrations add DailyPrepMaxTasks \
|
||||||
|
-p src/ClaudeDo.Data/ClaudeDo.Data.csproj \
|
||||||
|
-s src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the generated `Up` contains an `AddColumn<int>("daily_prep_max_tasks", ... defaultValue: 5)` and an `UpdateData` setting the singleton row's `daily_prep_max_tasks` to 5. If `dotnet ef` is unavailable, hand-write the migration mirroring `20260603072822_WeeklyReport.cs` **and** add the matching `Property<int>("DailyPrepMaxTasks").HasColumnName("daily_prep_max_tasks")` line to `ClaudeDoDbContextModelSnapshot.cs` under the `AppSettingsEntity` builder.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run the test — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Data tests/ClaudeDo.Data.Tests
|
||||||
|
git commit -m "feat(daily-prep): add DailyPrepMaxTasks app setting"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: `get_daily_prep_candidates` MCP tool
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||||
|
|
||||||
|
Read `ExternalMcpServiceTests.cs` first and reuse its existing harness (how it builds an `ExternalMcpService` with a real SQLite context, `ListRepository`, `TaskRepository`, fake `HubBroadcaster`, etc.). The new tool reads **all** lists/tasks itself via the injected `_dbFactory`, so it needs no new constructor args.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test.** Seed: a list with `WorkingDir = @"D:\work\repo"` holding two `Idle` tasks (one blocked, one not) and one `Done` task; a second list with `WorkingDir = @"C:\Private\secret"` holding one `Idle` task; a third list with `WorkingDir = null` holding one `Idle` task; and one `Idle` task with `IsMyDay = true` in the first list. Set `AppSettings.ReportExcludedPaths = "[\"C:\\\\Private\"]"`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDailyPrepCandidates_filters_by_status_block_and_excluded_repo()
|
||||||
|
{
|
||||||
|
// ... seed as described, using the file's existing seed helpers ...
|
||||||
|
var svc = NewService();
|
||||||
|
|
||||||
|
var result = await svc.GetDailyPrepCandidates(CancellationToken.None);
|
||||||
|
|
||||||
|
// Only the non-blocked, Idle, non-MyDay task in the non-excluded repo is a candidate.
|
||||||
|
Assert.Single(result.Candidates);
|
||||||
|
Assert.Equal("idle-unblocked", result.Candidates[0].Id);
|
||||||
|
// The Idle MyDay task is reported separately, not as a candidate.
|
||||||
|
Assert.Single(result.CurrentMyDay);
|
||||||
|
Assert.Equal(1, result.MaxTasks > 0 ? 1 : 1); // MaxTasks comes from AppSettings (default 5)
|
||||||
|
Assert.Equal(5, result.MaxTasks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run it — expect FAIL** (method missing).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the DTOs** near the other record declarations at the top of `ExternalMcpService.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record DailyPrepCandidateDto(
|
||||||
|
string Id, string ListId, string ListName, string Title, string? Description,
|
||||||
|
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
|
||||||
|
|
||||||
|
public sealed record DailyPrepDataDto(
|
||||||
|
int MaxTasks,
|
||||||
|
IReadOnlyList<DailyPrepCandidateDto> Candidates,
|
||||||
|
IReadOnlyList<DailyPrepCandidateDto> CurrentMyDay);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the tool method** to the `ExternalMcpService` class body:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Daily prep: returns the open tasks eligible for today's MyDay selection. " +
|
||||||
|
"candidates = Idle, not blocked, in a git repo not excluded from the weekly report, and not already in MyDay. " +
|
||||||
|
"currentMyDay = Idle tasks already flagged IsMyDay (count them toward the cap). " +
|
||||||
|
"maxTasks = the hard cap on total open MyDay tasks. Use set_my_day to add tasks (never exceed maxTasks).")]
|
||||||
|
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
|
var excludes = DailyPrepFilter.ParseExcludes(settings.ReportExcludedPaths);
|
||||||
|
var maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
|
||||||
|
var idle = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.List)
|
||||||
|
.Where(t => t.Status == TaskStatus.Idle)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var currentMyDay = idle
|
||||||
|
.Where(t => t.IsMyDay)
|
||||||
|
.OrderBy(t => t.SortOrder)
|
||||||
|
.Select(ToCandidate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var candidates = idle
|
||||||
|
.Where(t => !t.IsMyDay
|
||||||
|
&& t.BlockedByTaskId == null
|
||||||
|
&& DailyPrepFilter.IsIncludedRepo(t.List?.WorkingDir, excludes))
|
||||||
|
.OrderBy(t => t.CreatedAt)
|
||||||
|
.Select(ToCandidate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new DailyPrepDataDto(maxTasks, candidates, currentMyDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
||||||
|
t.Id, t.ListId, t.List?.Name ?? "", t.Title, t.Description,
|
||||||
|
t.IsStarred, t.ScheduledFor, t.CreatedAt);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the filter helper** as a small static class at the bottom of `ExternalMcpService.cs` (single-consumer helper lives beside its consumer, per repo convention):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static class DailyPrepFilter
|
||||||
|
{
|
||||||
|
public static string[] ParseExcludes(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||||
|
return list is null ? [] : list.Select(Normalize).Where(p => p.Length > 0).ToArray();
|
||||||
|
}
|
||||||
|
catch (System.Text.Json.JsonException) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsIncludedRepo(string? workingDir, string[] excludes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(workingDir)) return false; // not a repo → excluded
|
||||||
|
var norm = Normalize(workingDir);
|
||||||
|
return !excludes.Any(p => norm.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string path) =>
|
||||||
|
path.Trim().Replace('/', '\\').TrimEnd('\\');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Data.Repositories;` if not already present (it is, via existing usings).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the test — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||||
|
git commit -m "feat(daily-prep): add get_daily_prep_candidates MCP tool"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: `set_my_day` MCP tool with cap-guard
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task SetMyDay_sets_flag_and_sort_order()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var id = await SeedIdleTask("My task"); // existing/added helper returning task id
|
||||||
|
|
||||||
|
var dto = await svc.SetMyDay(id, isMyDay: true, sortOrder: 3, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(dto.IsMyDay);
|
||||||
|
Assert.Equal(3, dto.SortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetMyDay_rejects_when_cap_reached()
|
||||||
|
{
|
||||||
|
// AppSettings.DailyPrepMaxTasks = 1 (set in seed)
|
||||||
|
var svc = NewService();
|
||||||
|
var first = await SeedIdleTask("a");
|
||||||
|
var second = await SeedIdleTask("b");
|
||||||
|
await svc.SetMyDay(first, true, null, CancellationToken.None);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => svc.SetMyDay(second, true, null, CancellationToken.None));
|
||||||
|
Assert.Contains("limit", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetMyDay_unset_is_always_allowed()
|
||||||
|
{
|
||||||
|
var svc = NewService();
|
||||||
|
var id = await SeedIdleTask("a");
|
||||||
|
await svc.SetMyDay(id, true, null, CancellationToken.None);
|
||||||
|
|
||||||
|
var dto = await svc.SetMyDay(id, false, null, CancellationToken.None);
|
||||||
|
Assert.False(dto.IsMyDay);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`SetMyDay` returns the existing `TaskDto`. Add a `SortOrder` field to `TaskDto` — see Step 3a. (`SeedIdleTask` / the `DailyPrepMaxTasks=1` seed reuse the file's existing seeding helpers.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3a: Add `SortOrder` to `TaskDto`** (record + `ToDto`) so the result reflects ordering:
|
||||||
|
|
||||||
|
In the `TaskDto` record add `int SortOrder` as the last positional member, and in `ToDto(TaskEntity t)` add `t.SortOrder` as the last argument. (Update any test that constructs `TaskDto` positionally — search the test project.)
|
||||||
|
|
||||||
|
- [ ] **Step 3b: Add the tool method:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
||||||
|
"(use consecutive sortOrder values to keep related tasks together). " +
|
||||||
|
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
||||||
|
"clearing (isMyDay=false) is always allowed.")]
|
||||||
|
public async Task<TaskDto> SetMyDay(
|
||||||
|
string taskId,
|
||||||
|
bool isMyDay,
|
||||||
|
int? sortOrder,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var task = await ctx.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
if (isMyDay && !task.IsMyDay)
|
||||||
|
{
|
||||||
|
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
|
var max = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
var openMyDay = await ctx.Tasks.CountAsync(
|
||||||
|
t => t.IsMyDay && t.Status == TaskStatus.Idle, cancellationToken);
|
||||||
|
if (openMyDay >= max)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"MyDay limit {max} reached. Clear a task before adding another.");
|
||||||
|
}
|
||||||
|
|
||||||
|
task.IsMyDay = isMyDay;
|
||||||
|
if (sortOrder is not null) task.SortOrder = sortOrder.Value;
|
||||||
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return ToDto(task);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests
|
||||||
|
git commit -m "feat(daily-prep): add set_my_day MCP tool with cap-guard"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Rewrite `PrimeRunner` to run the daily prep
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs`, and extend `PrimeRunnerTests.cs` if it exists
|
||||||
|
|
||||||
|
The runner needs the cap `X` (read from `AppSettings`) and today's date. Inject `IDbContextFactory<ClaudeDoDbContext>` into `PrimeRunner` (it is resolvable in the main app DI) and an `IPrimeClock` for the date (already registered).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing prompt/args tests.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DailyPrepPromptTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Build_prompt_contains_cap_and_date()
|
||||||
|
{
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: new DateOnly(2026, 6, 3));
|
||||||
|
Assert.Contains("5", prompt);
|
||||||
|
Assert.Contains("2026-06-03", prompt);
|
||||||
|
Assert.Contains("get_daily_prep_candidates", prompt);
|
||||||
|
Assert.Contains("set_my_day", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_args_allows_only_the_two_tools()
|
||||||
|
{
|
||||||
|
var args = DailyPrepPrompt.BuildArgs(maxTurns: 30);
|
||||||
|
Assert.Contains("--output-format stream-json", args);
|
||||||
|
Assert.Contains("--max-turns 30", args);
|
||||||
|
Assert.Contains("--allowedTools", args);
|
||||||
|
Assert.Contains("mcp__claudedo__get_daily_prep_candidates", args);
|
||||||
|
Assert.Contains("mcp__claudedo__set_my_day", args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `DailyPrepPrompt.cs`:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace ClaudeDo.Worker.Prime;
|
||||||
|
|
||||||
|
public static class DailyPrepPrompt
|
||||||
|
{
|
||||||
|
public const string CandidatesTool = "mcp__claudedo__get_daily_prep_candidates";
|
||||||
|
public const string SetMyDayTool = "mcp__claudedo__set_my_day";
|
||||||
|
|
||||||
|
public static string BuildArgs(int maxTurns) =>
|
||||||
|
"-p --output-format stream-json --verbose --permission-mode acceptEdits " +
|
||||||
|
$"--max-turns {maxTurns} " +
|
||||||
|
$"--allowedTools {CandidatesTool} {SetMyDayTool}";
|
||||||
|
|
||||||
|
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
||||||
|
$"""
|
||||||
|
Du bereitest meinen Arbeitstag fuer {today:yyyy-MM-dd} vor.
|
||||||
|
|
||||||
|
1. Rufe {CandidatesTool} auf.
|
||||||
|
2. Behalte bereits als MyDay markierte offene Tasks (currentMyDay) — entferne sie nicht.
|
||||||
|
3. Fuelle bis maximal {maxTasks} offene Tasks GESAMT in MyDay auf (currentMyDay zaehlt mit). Niemals mehr.
|
||||||
|
4. Schaetze pro Kandidat grob den Aufwand und waehle eine machbare Mischung (nicht nur Grossbrocken).
|
||||||
|
Priorisiere isStarred, faellige (scheduledFor) und aeltere Tasks.
|
||||||
|
5. Lege thematisch verwandte Tasks durch aufeinanderfolgende sortOrder-Werte nebeneinander.
|
||||||
|
6. Setze die Auswahl via {SetMyDayTool}(taskId, true, sortOrder). Markiere nichts ausserhalb der Kandidatenliste.
|
||||||
|
|
||||||
|
Wenn es keine Kandidaten gibt, tue nichts.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run prompt tests — expect PASS.**
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rewrite `PrimeRunner.cs`:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Prime;
|
||||||
|
|
||||||
|
public sealed class PrimeRunner : IPrimeRunner
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
private const int MaxTurns = 30;
|
||||||
|
|
||||||
|
private readonly IClaudeProcess _claude;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly IPrimeClock _clock;
|
||||||
|
private readonly ILogger<PrimeRunner> _logger;
|
||||||
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
|
|
||||||
|
public PrimeRunner(
|
||||||
|
IClaudeProcess claude,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
IPrimeClock clock,
|
||||||
|
ILogger<PrimeRunner> logger)
|
||||||
|
{
|
||||||
|
_claude = claude;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_clock = clock;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!await _gate.WaitAsync(0, ct))
|
||||||
|
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cwd = Paths.AppDataRoot();
|
||||||
|
Directory.CreateDirectory(cwd);
|
||||||
|
|
||||||
|
int maxTasks;
|
||||||
|
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||||
|
{
|
||||||
|
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
|
||||||
|
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
||||||
|
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||||
|
|
||||||
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
timeoutCts.CancelAfter(FireTimeout);
|
||||||
|
|
||||||
|
var result = await _claude.RunAsync(
|
||||||
|
arguments: args,
|
||||||
|
prompt: prompt,
|
||||||
|
workingDirectory: cwd,
|
||||||
|
onStdoutLine: _ => Task.CompletedTask,
|
||||||
|
ct: timeoutCts.Token);
|
||||||
|
|
||||||
|
return result.IsSuccess
|
||||||
|
? new PrimeRunOutcome(true, "Daily prep complete")
|
||||||
|
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Daily prep run failed");
|
||||||
|
return new PrimeRunOutcome(false, ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Fix the DI registration is unchanged** (`AddSingleton<IPrimeRunner, PrimeRunner>()` already works — the new ctor deps `IDbContextFactory` and `IPrimeClock` are registered). Build the Worker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Update/extend `PrimeRunnerTests.cs`** (if present) to match the new ctor: construct `PrimeRunner` with a fake `IClaudeProcess`, a real temp-SQLite `IDbContextFactory`, a fake `IPrimeClock`, and a logger. Add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task FireAsync_returns_already_running_when_gate_held()
|
||||||
|
{
|
||||||
|
var runner = NewRunner(claudeDelay: TimeSpan.FromSeconds(2));
|
||||||
|
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||||
|
|
||||||
|
var first = runner.FireAsync(schedule, CancellationToken.None);
|
||||||
|
var second = await runner.FireAsync(schedule, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(second.Success);
|
||||||
|
Assert.Contains("already running", second.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
await first;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If no `PrimeRunnerTests.cs` exists, create one. The fake `IClaudeProcess` should optionally delay (to keep the gate held) and return a successful `RunResult { ExitCode = 0, ResultMarkdown = "ok" }`.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run — expect PASS.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "DailyPrepPrompt|PrimeRunner"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Prime tests/ClaudeDo.Worker.Tests/Prime
|
||||||
|
git commit -m "feat(daily-prep): run daily prep from PrimeRunner via allowed MCP tools"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Hub — `RunDailyPrepNow` + expose `DailyPrepMaxTasks`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
|
||||||
|
Read `WorkerHub.cs` first. It already exposes a `GetAppSettings`/`UpdateAppSettings` pair backed by a DTO record (the one carrying `ReportExcludedPaths`, `StandupWeekday`).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `DailyPrepMaxTasks` to the hub AppSettings DTO record** (the record near the top of `WorkerHub.cs` that lists `ReportExcludedPaths`). Add `int DailyPrepMaxTasks` as a member. In the read mapping (`GetAppSettings`, where `row.ReportExcludedPaths` is read) add `row.DailyPrepMaxTasks`; in the write mapping (`UpdateAppSettings`, where `ReportExcludedPaths = dto.ReportExcludedPaths`) add `DailyPrepMaxTasks = dto.DailyPrepMaxTasks`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the hub method.** Inject `IPrimeRunner` and `HubBroadcaster` if the hub does not already have them (the hub is constructed by SignalR via DI; both are registered singletons). Then:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<bool> RunDailyPrepNow()
|
||||||
|
{
|
||||||
|
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||||
|
var firedAt = DateTimeOffset.Now;
|
||||||
|
var outcome = await _primeRunner.FireAsync(schedule, Context.ConnectionAborted);
|
||||||
|
await _broadcaster.PrimeFired(Guid.Empty, outcome.Success, outcome.Message, firedAt);
|
||||||
|
return outcome.Success;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Worker.Prime;` to `WorkerHub.cs` if missing.
|
||||||
|
|
||||||
|
> **Caution (memory):** changing the `WorkerHub` constructor breaks hand-rolled hub-test fakes in `ClaudeDo.Worker.Tests` and possibly `ClaudeDo.Ui.Tests`. After editing, build the test projects and fix every `new WorkerHub(...)` / fake `IWorkerClient` construction the compiler flags.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Mirror the DTO in the UI** (`WorkerClient.cs`, the AppSettings DTO around line 498): add `int DailyPrepMaxTasks` to the record (same position as in the hub DTO). Add a `RunDailyPrepNow` client call:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<bool> RunDailyPrepNowAsync() =>
|
||||||
|
_connection.InvokeAsync<bool>("RunDailyPrepNow");
|
||||||
|
```
|
||||||
|
|
||||||
|
(Match the exact connection field/name and the async-wrapper style used by neighbouring calls like `GenerateWeekReport`.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build Worker + App + test projects; fix any broken fakes.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/WorkerClient.cs tests
|
||||||
|
git commit -m "feat(daily-prep): add RunDailyPrepNow hub method and expose DailyPrepMaxTasks"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Settings UI — edit `DailyPrepMaxTasks`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`
|
||||||
|
- Modify: the Prime Claude tab markup in `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs` (load/save wiring, where other AppSettings fields are mapped)
|
||||||
|
|
||||||
|
Read these three files first; mirror how an existing numeric AppSetting (e.g. `MaxParallelExecutions` or `WorktreeAutoCleanupDays`) is loaded from the hub DTO, bound, and saved back.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add an observable property** to `PrimeClaudeTabViewModel.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ObservableProperty] private int _dailyPrepMaxTasks = 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Wire load/save** in `SettingsModalViewModel.cs`: where the AppSettings DTO is read into the tabs, set `PrimeClaude.DailyPrepMaxTasks = dto.DailyPrepMaxTasks;`. Where the DTO is written, include `DailyPrepMaxTasks = PrimeClaude.DailyPrepMaxTasks`. (Use the exact tab property name for the Prime Claude tab in that VM.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the editor** in the Prime Claude tab of `SettingsModalView.axaml`, near the schedule list:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{x:Static loc:L.Settings_DailyPrepMaxTasks}" VerticalAlignment="Center"/>
|
||||||
|
<NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100"
|
||||||
|
Value="{Binding PrimeClaude.DailyPrepMaxTasks}"/>
|
||||||
|
</StackPanel>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the `Settings_DailyPrepMaxTasks` key to both `locales/en.json` and `locales/de.json` (en: "Max tasks per day", de: "Max. Aufgaben pro Tag"). If the tab does not use localized labels yet, use a plain `Text="Max tasks per day"` string to match its current style.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the App; smoke-build the UI.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui src/ClaudeDo.Localization
|
||||||
|
git commit -m "feat(daily-prep): add DailyPrepMaxTasks editor to Prime Claude settings"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: MyDay header — "Tag vorbereiten" button
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: the ViewModel backing the MyDay list view (the one that exposes the smart-list header/toolbar; find it under `src/ClaudeDo.Ui/ViewModels/Islands/` — likely the tasks/list island VM that has access to `IWorkerClient`)
|
||||||
|
- Modify: the corresponding view (`.axaml`) that renders the list header
|
||||||
|
|
||||||
|
Read the island VM + view first. Find where the active list is known to be `smart:my-day` so the button can be shown only there (mirror any existing conditional header content). The VM already holds a worker-client reference used by other commands (e.g. RunNow) — reuse it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the command** to the island VM:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task PrepareDayAsync()
|
||||||
|
{
|
||||||
|
await _workerClient.RunDailyPrepNowAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Use the VM's existing worker-client field name. The MyDay list refreshes automatically via the `TaskUpdated` broadcast the tools emit, so no manual reload is needed.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add an `IsMyDayList` (or reuse existing selected-list) guard** so the button only appears on the MyDay smart list. If the VM already exposes the selected list id, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool IsMyDayList => SelectedListId == "smart:my-day";
|
||||||
|
```
|
||||||
|
|
||||||
|
and raise its change notification wherever `SelectedListId` changes (mirror existing patterns; if a `[NotifyPropertyChangedFor]` or manual `OnPropertyChanged` is already used for the selection, add this property to it).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the button** to the list header in the view, visible only on MyDay:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button Content="{x:Static loc:L.MyDay_PrepareDay}"
|
||||||
|
Command="{Binding PrepareDayCommand}"
|
||||||
|
IsVisible="{Binding IsMyDayList}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `MyDay_PrepareDay` to `locales/en.json` ("Prepare day") and `locales/de.json` ("Tag vorbereiten"), or a plain string if the view is not localized.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the App.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Manual smoke (cannot be unit-tested):** start the Worker and App, open MyDay, click "Tag vorbereiten", confirm tasks appear (capped) and the button is hidden on other lists. Report results explicitly — do not claim UI success without running it.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui src/ClaudeDo.Localization
|
||||||
|
git commit -m "feat(daily-prep): add Prepare-day button to MyDay header"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification
|
||||||
|
|
||||||
|
- [ ] `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
- [ ] `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
- [ ] `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||||
|
- [ ] `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||||
|
- [ ] `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||||
|
- [ ] End-to-end manual run: schedule fires (or button) → Claude calls the two tools → MyDay gets a capped subset; re-run keeps existing MyDay and tops up without exceeding the cap.
|
||||||
|
|
||||||
|
## Notes / risks
|
||||||
|
|
||||||
|
- Relies on the globally registered `claudedo` MCP (installer `RegisterMcpStep`). If absent, the prep run produces 0 changes — acceptable for v1.
|
||||||
|
- `--permission-mode acceptEdits` + explicit `--allowedTools` pre-approves exactly the two tools so the headless run never blocks on a permission prompt.
|
||||||
|
- The cap-guard counts `Idle && IsMyDay` tasks; it is the source of truth for the "never move everything in" invariant regardless of Claude's behavior.
|
||||||
|
- Future phase (out of scope): external ticket sources (Jira) feed into `get_daily_prep_candidates` behind a task-source abstraction.
|
||||||
972
docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
Normal file
972
docs/superpowers/plans/2026-06-04-bundled-prompts-overhaul.md
Normal file
@@ -0,0 +1,972 @@
|
|||||||
|
# Bundled Prompts Overhaul 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:** Externalize every bundled prose prompt into editable files with strong defaults, collapse system+agent, and add an inline `CLAUDEDO_BLOCKED:` roadblock protocol surfaced at review.
|
||||||
|
|
||||||
|
**Architecture:** `PromptFiles` becomes the single source of prompt defaults + a pure token renderer. Each consumer (TaskRunner, PlanningSessionManager, DailyPrepPrompt, WeekReportPromptBuilder) reads its prompt via `PromptFiles`. `StreamAnalyzer` collects roadblock markers from streamed assistant text; the runner folds them into the review result.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, xUnit, EF Core (no schema change in this plan).
|
||||||
|
|
||||||
|
Spec: `docs/superpowers/specs/2026-06-04-bundled-prompts-overhaul-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Data/PromptFiles.cs` — new `PromptKind` members, new defaults, `RenderTemplate` + `ReadOrDefault` + `Render`.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` — collect `Blocks` from assistant text.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/RunResult.cs` — carry `Blocks`.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs` — pass `Blocks`; expose no-result prefix const.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — drop agent file; retry via `retry.md`; fold blocks into review result.
|
||||||
|
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — read planning prompts via `PromptFiles`.
|
||||||
|
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — read `daily-prep.md`.
|
||||||
|
- `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` — read `weekly-report.md`.
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs` + its view — expose new prompt files, drop agent.
|
||||||
|
- Tests in `tests/ClaudeDo.Data.Tests` and `tests/ClaudeDo.Worker.Tests`.
|
||||||
|
|
||||||
|
Build commands (this repo is on .NET 8 — build per project, not the .slnx):
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: PromptFiles — kinds, defaults, pure renderer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Data/PromptFiles.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs` (create)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests for the pure renderer**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Tests;
|
||||||
|
|
||||||
|
public class PromptFilesTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RenderTemplate_replaces_known_tokens()
|
||||||
|
{
|
||||||
|
var outp = PromptFiles.RenderTemplate(
|
||||||
|
"Plan for {date}, cap {maxTasks}.",
|
||||||
|
new Dictionary<string, string> { ["date"] = "2026-06-04", ["maxTasks"] = "5" });
|
||||||
|
Assert.Equal("Plan for 2026-06-04, cap 5.", outp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderTemplate_leaves_unknown_braces_intact()
|
||||||
|
{
|
||||||
|
var outp = PromptFiles.RenderTemplate(
|
||||||
|
"## {Wochentag}, {dd.MM.yyyy} — {start}",
|
||||||
|
new Dictionary<string, string> { ["start"] = "01.06.2026" });
|
||||||
|
Assert.Equal("## {Wochentag}, {dd.MM.yyyy} — 01.06.2026", outp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_system_mentions_blocked_marker_and_scope()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.System);
|
||||||
|
Assert.Contains("CLAUDEDO_BLOCKED:", d);
|
||||||
|
Assert.Contains("unattended", d, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultFor_planning_initial_has_title_and_description_tokens()
|
||||||
|
{
|
||||||
|
var d = PromptFiles.DefaultFor(PromptKind.PlanningInitial);
|
||||||
|
Assert.Contains("{title}", d);
|
||||||
|
Assert.Contains("{description}", d);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PathFor_planning_is_planning_system_file()
|
||||||
|
{
|
||||||
|
Assert.EndsWith("planning-system.md", PromptFiles.PathFor(PromptKind.Planning));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||||
|
Expected: FAIL — `RenderTemplate`/`DefaultFor` don't exist, `PromptKind.PlanningInitial` undefined.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rewrite PromptFiles.cs**
|
||||||
|
|
||||||
|
Replace the entire contents of `src/ClaudeDo.Data/PromptFiles.cs` with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data;
|
||||||
|
|
||||||
|
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport }
|
||||||
|
|
||||||
|
public static class PromptFiles
|
||||||
|
{
|
||||||
|
public static string Root => Path.Combine(Paths.AppDataRoot(), "prompts");
|
||||||
|
|
||||||
|
public static string PathFor(PromptKind kind) => kind switch
|
||||||
|
{
|
||||||
|
PromptKind.System => Path.Combine(Root, "system.md"),
|
||||||
|
PromptKind.Planning => Path.Combine(Root, "planning-system.md"),
|
||||||
|
PromptKind.PlanningInitial => Path.Combine(Root, "planning-initial.md"),
|
||||||
|
PromptKind.Retry => Path.Combine(Root, "retry.md"),
|
||||||
|
PromptKind.DailyPrep => Path.Combine(Root, "daily-prep.md"),
|
||||||
|
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void EnsureExists(PromptKind kind)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Root);
|
||||||
|
var path = PathFor(kind);
|
||||||
|
if (File.Exists(path)) return;
|
||||||
|
File.WriteAllText(path, DefaultFor(kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ReadOrNull(PromptKind kind)
|
||||||
|
{
|
||||||
|
var path = PathFor(kind);
|
||||||
|
if (!File.Exists(path)) return null;
|
||||||
|
var content = File.ReadAllText(path).Trim();
|
||||||
|
return string.IsNullOrEmpty(content) ? null : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>File content if present and non-empty, otherwise the bundled default.</summary>
|
||||||
|
public static string ReadOrDefault(PromptKind kind) => ReadOrNull(kind) ?? DefaultFor(kind);
|
||||||
|
|
||||||
|
/// <summary>Render a prompt: read file-or-default, then substitute named tokens.</summary>
|
||||||
|
public static string Render(PromptKind kind, IReadOnlyDictionary<string, string> values)
|
||||||
|
=> RenderTemplate(ReadOrDefault(kind), values);
|
||||||
|
|
||||||
|
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.</summary>
|
||||||
|
public static string RenderTemplate(string template, IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(template);
|
||||||
|
foreach (var (key, val) in values)
|
||||||
|
sb.Replace("{" + key + "}", val);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DefaultFor(PromptKind kind) => kind switch
|
||||||
|
{
|
||||||
|
PromptKind.System => SystemDefault,
|
||||||
|
PromptKind.Planning => PlanningSystemDefault,
|
||||||
|
PromptKind.PlanningInitial => PlanningInitialDefault,
|
||||||
|
PromptKind.Retry => RetryDefault,
|
||||||
|
PromptKind.DailyPrep => DailyPrepDefault,
|
||||||
|
PromptKind.WeeklyReport => WeeklyReportDefault,
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
private const string SystemDefault = """
|
||||||
|
# Working Agreement
|
||||||
|
|
||||||
|
You are completing one well-defined task autonomously in a git repository.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Do exactly what the task asks — no unrequested refactors, renames, dependency
|
||||||
|
changes, or "while I'm here" cleanup.
|
||||||
|
- If intent is ambiguous, state the assumption you're making and proceed with the
|
||||||
|
most reasonable reading. Stop only if you genuinely cannot move forward.
|
||||||
|
- Prefer three similar lines over a premature abstraction. Don't build for
|
||||||
|
hypothetical future needs.
|
||||||
|
|
||||||
|
## Working in the repo
|
||||||
|
- Read a file before editing it. Match the conventions already in this codebase —
|
||||||
|
they override generic defaults.
|
||||||
|
- Prefer editing existing files to creating new ones. Don't write comments that
|
||||||
|
just restate the code.
|
||||||
|
- Validate only at real boundaries (user input, external APIs).
|
||||||
|
|
||||||
|
## Finishing
|
||||||
|
- Before claiming done, verify: run the build and relevant tests, confirm they
|
||||||
|
pass, and report what you ran. If you couldn't verify something, say so plainly.
|
||||||
|
- Make focused commits using the repository's existing commit-message convention.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
- Never force-push, hard-reset, or delete branches/files beyond the task's scope
|
||||||
|
without being asked.
|
||||||
|
- Don't introduce injection/XSS/secret-leak issues. Never commit credentials.
|
||||||
|
|
||||||
|
## You are running unattended
|
||||||
|
You run autonomously with no human watching. There is no one to answer mid-task
|
||||||
|
questions, so never stop to ask — make the most reasonable decision, note the
|
||||||
|
assumption, and continue.
|
||||||
|
|
||||||
|
## When you are blocked
|
||||||
|
If something genuinely prevents you from completing part of the task (missing
|
||||||
|
credentials, contradictory requirements, a destructive action you won't take
|
||||||
|
unasked), do NOT silently give up. Write this marker on its own line, then keep
|
||||||
|
working on whatever else you can:
|
||||||
|
|
||||||
|
CLAUDEDO_BLOCKED: <one short sentence describing what blocked you>
|
||||||
|
|
||||||
|
Emit it as many times as needed — once per distinct blocker. Use it only for true
|
||||||
|
blockers, not for routine decisions you can make yourself.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string PlanningSystemDefault = """
|
||||||
|
You are the planning assistant for ClaudeDo. Your job is to break a task into
|
||||||
|
smaller, independently executable subtasks — the session ends by creating those
|
||||||
|
subtasks.
|
||||||
|
|
||||||
|
Start every session by invoking the `superpowers:brainstorming` skill (Skill
|
||||||
|
tool) and follow it end to end: clarifying questions one at a time, then 2–3
|
||||||
|
approaches with a recommendation, then a short design. Do not create any subtasks
|
||||||
|
until the user has approved the design.
|
||||||
|
|
||||||
|
You can ONLY shape this task's plan — you cannot edit files or touch other tasks.
|
||||||
|
The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask,
|
||||||
|
DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else.
|
||||||
|
|
||||||
|
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||||
|
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||||
|
done-state, ordered so dependencies come first.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string PlanningInitialDefault = """
|
||||||
|
# Task to plan: {title}
|
||||||
|
|
||||||
|
{description}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string RetryDefault = """
|
||||||
|
The task did not complete on the previous attempt — you may have run out of
|
||||||
|
turns, hit an error, or stopped before finishing.
|
||||||
|
|
||||||
|
Review the work already done in this session and the current state of the
|
||||||
|
repository, identify what is still incomplete or broken, and finish the task.
|
||||||
|
Don't restart from scratch or repeat a failed approach. Verify the result
|
||||||
|
(build + tests) before you stop.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string DailyPrepDefault = """
|
||||||
|
You are preparing my workday for {date}.
|
||||||
|
|
||||||
|
1. Call mcp__claudedo__get_daily_prep_candidates.
|
||||||
|
2. Keep tasks already marked MyDay (currentMyDay) — never remove them.
|
||||||
|
3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it.
|
||||||
|
4. Estimate each candidate's effort and pick a feasible mix — not only big items.
|
||||||
|
Prioritize isStarred, due (scheduledFor), and older tasks.
|
||||||
|
5. Place related tasks next to each other using consecutive sortOrder values.
|
||||||
|
6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything
|
||||||
|
outside the candidate list.
|
||||||
|
|
||||||
|
If there are no candidates, do nothing.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string WeeklyReportDefault = """
|
||||||
|
You are generating a concise weekly standup report for a software developer,
|
||||||
|
covering {start} to {end}.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Write the ENTIRE report in German.
|
||||||
|
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||||||
|
activity (German weekday names). Omit days with no activity.
|
||||||
|
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||||||
|
"- Y behoben"). Merge related small work into one bullet.
|
||||||
|
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||||||
|
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||||||
|
bullet list per day. The notes are authoritative — never omit or contradict them.
|
||||||
|
- Name the project/repo when it adds clarity.
|
||||||
|
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||||||
|
|
||||||
|
Two sections follow below: an activity log derived from Claude session history,
|
||||||
|
and the developer's own notes. Base the report on both; the notes are
|
||||||
|
authoritative where they conflict with the derived activity.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release`
|
||||||
|
Expected: PASS (5 new tests).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
|
||||||
|
git commit -m "feat(prompts): externalize prompt kinds with defaults and token renderer"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: TaskRunner — drop agent file from system prompt merge
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:382-386`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Remove the agent-file read and merge**
|
||||||
|
|
||||||
|
In `ResolveConfigAsync`, replace:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
|
||||||
|
var agentFile = PromptFiles.ReadOrNull(PromptKind.Agent);
|
||||||
|
|
||||||
|
var instructions = MergeInstructions(
|
||||||
|
systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile);
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
|
||||||
|
|
||||||
|
var instructions = MergeInstructions(
|
||||||
|
systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify it compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
Expected: PASS (no reference to `PromptKind.Agent` remains).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs
|
||||||
|
git commit -m "refactor(prompts): collapse agent prompt into system prompt"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Retry prompt from file + conditional stderr append
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs:101-103` (expose prefix const)
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (add `BuildRetryPrompt`, use it at ~L107)
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs` (create)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests for the retry-prompt helper**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Runner;
|
||||||
|
|
||||||
|
public class RetryPromptTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Generic_no_result_error_is_not_appended()
|
||||||
|
{
|
||||||
|
var prompt = TaskRunner.BuildRetryPrompt($"{ClaudeProcess.NoResultPrefix} 1 and no result.");
|
||||||
|
Assert.DoesNotContain("Captured error", prompt);
|
||||||
|
Assert.Contains("did not complete", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Real_error_is_appended()
|
||||||
|
{
|
||||||
|
var prompt = TaskRunner.BuildRetryPrompt("error CS1002: ; expected");
|
||||||
|
Assert.Contains("Captured error", prompt);
|
||||||
|
Assert.Contains("CS1002", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Null_error_yields_bare_prompt()
|
||||||
|
{
|
||||||
|
var prompt = TaskRunner.BuildRetryPrompt(null);
|
||||||
|
Assert.DoesNotContain("Captured error", prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RetryPromptTests`
|
||||||
|
Expected: FAIL — `BuildRetryPrompt` / `NoResultPrefix` don't exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Expose the no-result prefix in ClaudeProcess**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs`, add the const near the top of the class and use it in the error fallback. Replace:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var error = lastStderr.Length > 0
|
||||||
|
? lastStderr.ToString().Trim()
|
||||||
|
: $"Claude exited with code {exitCode} and no result.";
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var error = lastStderr.Length > 0
|
||||||
|
? lastStderr.ToString().Trim()
|
||||||
|
: $"{NoResultPrefix} {exitCode} and no result.";
|
||||||
|
```
|
||||||
|
|
||||||
|
and add inside the class (e.g. just below the fields):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public const string NoResultPrefix = "Claude exited with code";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add BuildRetryPrompt to TaskRunner and use it**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add this static method (next to `MergeInstructions`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static string BuildRetryPrompt(string? capturedError)
|
||||||
|
{
|
||||||
|
var basePrompt = PromptFiles.ReadOrDefault(PromptKind.Retry);
|
||||||
|
var isReal = !string.IsNullOrWhiteSpace(capturedError)
|
||||||
|
&& !capturedError!.StartsWith(ClaudeProcess.NoResultPrefix, StringComparison.Ordinal);
|
||||||
|
return isReal
|
||||||
|
? $"{basePrompt}\n\nCaptured error from the failed run:\n\n{capturedError!.Trim()}"
|
||||||
|
: basePrompt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then replace the inline retry prompt at ~L107:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var retryPrompt = $"The previous attempt failed with:\n\n{result.ErrorMarkdown}\n\nTry again and fix the issues.";
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var retryPrompt = BuildRetryPrompt(result.ErrorMarkdown);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter RetryPromptTests`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/ClaudeProcess.cs src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/RetryPromptTests.cs
|
||||||
|
git commit -m "feat(prompts): retry prompt from file, append only real captured errors"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: PlanningSessionManager reads planning prompts from files
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` (`BuildSystemPrompt` ~L366, `BuildInitialPrompt` ~L392)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace BuildSystemPrompt body**
|
||||||
|
|
||||||
|
Replace the whole method body of `BuildSystemPrompt()` with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static string BuildSystemPrompt() => PromptFiles.ReadOrDefault(PromptKind.Planning);
|
||||||
|
```
|
||||||
|
|
||||||
|
(Delete the inline fallback string literal that followed.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace BuildInitialPrompt body**
|
||||||
|
|
||||||
|
Replace the whole method body of `BuildInitialPrompt(TaskEntity task)` with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static string BuildInitialPrompt(TaskEntity task) =>
|
||||||
|
PromptFiles.Render(PromptKind.PlanningInitial, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["title"] = task.Title,
|
||||||
|
["description"] = task.Description ?? "",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure `using ClaudeDo.Data;` is present (it is — `PromptFiles` lived there already via `ReadOrNull`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build to verify it compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs
|
||||||
|
git commit -m "refactor(prompts): planning prompts read from editable files"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: DailyPrepPrompt reads from file
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`
|
||||||
|
- Modify: `tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update DailyPrepPromptTests to assert the English default render**
|
||||||
|
|
||||||
|
Replace the `Build_prompt_contains_cap_and_date` test body with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void Build_prompt_contains_cap_and_date()
|
||||||
|
{
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: new DateOnly(2026, 6, 3));
|
||||||
|
Assert.Contains("5", prompt);
|
||||||
|
Assert.Contains("2026-06-03", prompt);
|
||||||
|
Assert.Contains("get_daily_prep_candidates", prompt);
|
||||||
|
Assert.Contains("set_my_day", prompt);
|
||||||
|
Assert.Contains("preparing my workday", prompt);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(The new assertion pins the English default; the file-read path is exercised by the same default when no `daily-prep.md` exists.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DailyPrepPromptTests`
|
||||||
|
Expected: FAIL — current German prompt has no "preparing my workday".
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rewrite BuildPrompt to read the file**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs`, replace the `BuildPrompt` method with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
||||||
|
ClaudeDo.Data.PromptFiles.Render(
|
||||||
|
ClaudeDo.Data.PromptKind.DailyPrep,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["date"] = today.ToString("yyyy-MM-dd"),
|
||||||
|
["maxTasks"] = maxTasks.ToString(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave `BuildArgs`, `LogPath`, and the tool-name consts unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DailyPrepPromptTests`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs tests/ClaudeDo.Worker.Tests/Prime/DailyPrepPromptTests.cs
|
||||||
|
git commit -m "feat(prompts): daily-prep prompt from file, English default"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: WeekReportPromptBuilder reads instructions from file
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs`
|
||||||
|
- Check: `tests/ClaudeDo.Worker.Tests/Report/WeekReportPromptBuilderTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the inline Instructions with a file read**
|
||||||
|
|
||||||
|
In `WeekReportPromptBuilder.Build`, replace:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Instructions,
|
||||||
|
start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||||
|
end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)));
|
||||||
|
sb.AppendLine();
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(ClaudeDo.Data.PromptFiles.Render(
|
||||||
|
ClaudeDo.Data.PromptKind.WeeklyReport,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["start"] = start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||||
|
["end"] = end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||||
|
}));
|
||||||
|
sb.AppendLine();
|
||||||
|
```
|
||||||
|
|
||||||
|
Then delete the now-unused `private const string Instructions = ...` block. (The `{Wochentag}`/`{dd.MM.yyyy}` literals inside the default survive because `RenderTemplate` only replaces `{start}`/`{end}`.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify the existing builder test still passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter WeekReportPromptBuilderTests`
|
||||||
|
Expected: PASS. If a test asserted exact old wording, update it to assert the date appears and that activity/notes sections render (the new default keeps German output rules).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs tests/ClaudeDo.Worker.Tests/Report/WeekReportPromptBuilderTests.cs
|
||||||
|
git commit -m "feat(prompts): weekly-report instructions from file, point at data sections"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: StreamAnalyzer collects roadblock markers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests**
|
||||||
|
|
||||||
|
Append to `StreamAnalyzerTests`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void Collects_Blocked_Markers_From_Assistant_Text()
|
||||||
|
{
|
||||||
|
var analyzer = new StreamAnalyzer();
|
||||||
|
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"working\nCLAUDEDO_BLOCKED: missing API key\nmoving on"}]}}""");
|
||||||
|
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: cannot reach db"}]}}""");
|
||||||
|
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
|
||||||
|
var result = analyzer.GetResult();
|
||||||
|
Assert.Equal(2, result.Blocks.Count);
|
||||||
|
Assert.Equal("missing API key", result.Blocks[0]);
|
||||||
|
Assert.Equal("cannot reach db", result.Blocks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Strips_Blocked_Markers_From_Result_Text()
|
||||||
|
{
|
||||||
|
var analyzer = new StreamAnalyzer();
|
||||||
|
analyzer.ProcessLine("""{"type":"result","result":"All set.\nCLAUDEDO_BLOCKED: no creds\nDone.","session_id":"s1"}""");
|
||||||
|
var result = analyzer.GetResult();
|
||||||
|
Assert.DoesNotContain("CLAUDEDO_BLOCKED", result.ResultMarkdown);
|
||||||
|
Assert.Single(result.Blocks);
|
||||||
|
Assert.Equal("no creds", result.Blocks[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void No_Markers_Means_Empty_Blocks()
|
||||||
|
{
|
||||||
|
var analyzer = new StreamAnalyzer();
|
||||||
|
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
|
||||||
|
Assert.Empty(analyzer.GetResult().Blocks);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter StreamAnalyzerTests`
|
||||||
|
Expected: FAIL — `Blocks` doesn't exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement marker collection in StreamAnalyzer**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs`:
|
||||||
|
|
||||||
|
Add to `StreamResult`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public IReadOnlyList<string> Blocks { get; set; } = Array.Empty<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a field and a constant to `StreamAnalyzer`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private readonly List<string> _blocks = new();
|
||||||
|
private const string BlockedPrefix = "CLAUDEDO_BLOCKED:";
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `case "result":` branch, after `_resultMarkdown` is assigned, scan and strip:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (root.TryGetProperty("result", out var resultProp))
|
||||||
|
_resultMarkdown = StripAndCollect(resultProp.GetString());
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `case "assistant":` branch, collect from text content (keep `_turnCount++`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
case "assistant":
|
||||||
|
_turnCount++;
|
||||||
|
CollectFromAssistant(root);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add these helpers to the class:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private void CollectFromAssistant(JsonElement root)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("message", out var msg)) return;
|
||||||
|
if (!msg.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array) return;
|
||||||
|
foreach (var block in content.EnumerateArray())
|
||||||
|
if (block.TryGetProperty("type", out var t) && t.GetString() == "text"
|
||||||
|
&& block.TryGetProperty("text", out var txt))
|
||||||
|
ScanForBlocks(txt.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScanForBlocks(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return;
|
||||||
|
foreach (var line in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.Trim();
|
||||||
|
if (trimmed.StartsWith(BlockedPrefix, StringComparison.Ordinal))
|
||||||
|
_blocks.Add(trimmed[BlockedPrefix.Length..].Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? StripAndCollect(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return text;
|
||||||
|
ScanForBlocks(text);
|
||||||
|
var kept = text.Split('\n')
|
||||||
|
.Where(l => !l.Trim().StartsWith(BlockedPrefix, StringComparison.Ordinal));
|
||||||
|
return string.Join('\n', kept).Trim();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `Blocks = _blocks` to the `GetResult()` initializer:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public StreamResult GetResult() => new()
|
||||||
|
{
|
||||||
|
ResultMarkdown = FallbackResult(),
|
||||||
|
StructuredOutputJson = _structuredOutputJson,
|
||||||
|
SessionId = _sessionId,
|
||||||
|
TurnCount = _turnCount,
|
||||||
|
TokensIn = _tokensIn,
|
||||||
|
TokensOut = _tokensOut,
|
||||||
|
ApiRetryCount = _apiRetryCount,
|
||||||
|
Blocks = _blocks,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter StreamAnalyzerTests`
|
||||||
|
Expected: PASS (all old + 3 new).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs
|
||||||
|
git commit -m "feat(roadblock): collect and strip CLAUDEDO_BLOCKED markers in StreamAnalyzer"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: RunResult + ClaudeProcess carry Blocks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/RunResult.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs:89-113`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add Blocks to RunResult**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Runner/RunResult.cs`, add inside the class:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public IReadOnlyList<string> Blocks { get; init; } = Array.Empty<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Populate Blocks in both RunResult returns**
|
||||||
|
|
||||||
|
In `ClaudeProcess.RunAsync`, add `Blocks = streamResult.Blocks,` to **both** the success `RunResult { ... }` (after `TokensOut`) and the error `RunResult { ... }` initializer.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build to verify it compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/RunResult.cs src/ClaudeDo.Worker/Runner/ClaudeProcess.cs
|
||||||
|
git commit -m "feat(roadblock): carry blocks through RunResult"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: Fold roadblocks into the review result
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` (`HandleSuccess` ~L319-352; add `ComposeReviewResult`)
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs` (create)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests for the compose helper**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Runner;
|
||||||
|
|
||||||
|
public class ReviewResultTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void No_blocks_returns_result_unchanged()
|
||||||
|
{
|
||||||
|
Assert.Equal("done", TaskRunner.ComposeReviewResult("done", Array.Empty<string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Blocks_are_appended_as_a_section()
|
||||||
|
{
|
||||||
|
var outp = TaskRunner.ComposeReviewResult("done", new[] { "no creds", "db down" });
|
||||||
|
Assert.Contains("⚠ Roadblocks", outp);
|
||||||
|
Assert.Contains("- no creds", outp);
|
||||||
|
Assert.Contains("- db down", outp);
|
||||||
|
Assert.Contains("done", outp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Null_result_with_blocks_still_lists_them()
|
||||||
|
{
|
||||||
|
var outp = TaskRunner.ComposeReviewResult(null, new[] { "x" });
|
||||||
|
Assert.Contains("⚠ Roadblocks", outp);
|
||||||
|
Assert.Contains("- x", outp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter ReviewResultTests`
|
||||||
|
Expected: FAIL — `ComposeReviewResult` doesn't exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add ComposeReviewResult and use it in HandleSuccess**
|
||||||
|
|
||||||
|
In `TaskRunner`, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static string? ComposeReviewResult(string? result, IReadOnlyList<string> blocks)
|
||||||
|
{
|
||||||
|
if (blocks.Count == 0) return result;
|
||||||
|
var section = "⚠ Roadblocks reported during the run:\n"
|
||||||
|
+ string.Join('\n', blocks.Select(b => $"- {b}"));
|
||||||
|
return string.IsNullOrWhiteSpace(result) ? section : $"{result}\n\n{section}";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `HandleSuccess`, compute the composed result once and pass it to both terminal writes:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var finishedAt = DateTime.UtcNow;
|
||||||
|
var reviewResult = ComposeReviewResult(result.ResultMarkdown, result.Blocks);
|
||||||
|
if (task.ParentTaskId is null && task.PlanningPhase == PlanningPhase.None)
|
||||||
|
{
|
||||||
|
await _state.SubmitForReviewAsync(task.Id, finishedAt, reviewResult, CancellationToken.None);
|
||||||
|
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||||
|
await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _state.CompleteAsync(task.Id, finishedAt, reviewResult, CancellationToken.None);
|
||||||
|
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||||
|
await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Make sure `using System.Linq;` is available — it is, via implicit usings.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter ReviewResultTests`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs
|
||||||
|
git commit -m "feat(roadblock): surface reported roadblocks in the review result"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Files-settings UI exposes the new prompt files
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs`
|
||||||
|
- Modify: the Files settings view (find with: `Grep "SystemPromptPath" src/ClaudeDo.Ui` → the `.axaml` binding to `OpenPromptCommand`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the prompt-path properties**
|
||||||
|
|
||||||
|
In `FilesSettingsTabViewModel`, replace the three path properties with the new set (drop Agent, add the rest):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
||||||
|
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
||||||
|
public string PlanningInitialPromptPath { get; } = PromptFiles.PathFor(PromptKind.PlanningInitial);
|
||||||
|
public string RetryPromptPath { get; } = PromptFiles.PathFor(PromptKind.Retry);
|
||||||
|
public string DailyPrepPromptPath { get; } = PromptFiles.PathFor(PromptKind.DailyPrep);
|
||||||
|
public string WeeklyReportPromptPath { get; } = PromptFiles.PathFor(PromptKind.WeeklyReport);
|
||||||
|
```
|
||||||
|
|
||||||
|
(`OpenPromptCommand` already parses the `PromptKind` name from its parameter, so no command change is needed.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update the view**
|
||||||
|
|
||||||
|
Open the Files settings `.axaml`. For the existing System/Planning/Agent rows: keep System, keep Planning, **remove the Agent row**, and add four rows mirroring the System row's markup — each binding its label/path to the new property and passing the matching `PromptKind` name as the `OpenPromptCommand` parameter:
|
||||||
|
|
||||||
|
- `Planning` (system) → "Planning system prompt", `PlanningPromptPath`, parameter `Planning`
|
||||||
|
- `PlanningInitial` → "Planning kickoff prompt", `PlanningInitialPromptPath`, parameter `PlanningInitial`
|
||||||
|
- `Retry` → "Retry prompt", `RetryPromptPath`, parameter `Retry`
|
||||||
|
- `DailyPrep` → "Daily-prep prompt", `DailyPrepPromptPath`, parameter `DailyPrep`
|
||||||
|
- `WeeklyReport` → "Weekly-report prompt", `WeeklyReportPromptPath`, parameter `WeeklyReport`
|
||||||
|
|
||||||
|
Use the exact same control template as the existing System row (same button + `CommandParameter` shape); only the bound property, label text, and parameter string differ.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build the UI project**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Visual check (manual — flag for user)**
|
||||||
|
|
||||||
|
Start the app, open Settings → Files tab. Confirm six "Open" prompt buttons appear (System, Planning system, Planning kickoff, Retry, Daily-prep, Weekly-report), no Agent row, and each opens/seeds the right file under `~/.todo-app/prompts/`. **This step cannot be verified by the agent — ask the user to confirm visually.**
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs src/ClaudeDo.Ui/Views/**/*Files*.axaml
|
||||||
|
git commit -m "feat(ui): expose all editable prompt files, drop agent prompt"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11: Full build + test sweep
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build worker + app**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
```
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run all affected test projects**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update docs**
|
||||||
|
|
||||||
|
Update `docs/prompts-inventory.md` to note the externalized files and that `agent.md`/`planning.md` are retired in favor of `system.md`/`planning-system.md`. Note `CLAUDEDO_BLOCKED:` in the inventory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/prompts-inventory.md
|
||||||
|
git commit -m "docs: refresh prompt inventory for externalized prompts + roadblock marker"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review notes
|
||||||
|
|
||||||
|
- **Spec coverage:** system.md collapse (T2), planning prompts (T4), retry (T3), daily-prep English (T5), weekly-report + data pointer (T6), templating/`Render` (T1), roadblock detect/strip/route (T7–T9), file layout + migration via `EnsureExists`/new `PathFor` (T1), UI surface (T10). The "Out-of-scope improvements" system.md section is intentionally **deferred to the child-tasks plan** (it depends on the `SuggestImprovement` tool).
|
||||||
|
- **Migration:** old `planning.md`/`agent.md` go inert automatically — `TaskRunner` no longer reads agent (T2), planning now reads `planning-system.md` (T1 PathFor). No code deletes the old files; harmless.
|
||||||
|
- **Determinism:** content tests target `DefaultFor`/`RenderTemplate` (pure, no disk). Consumers fall back to the same default when no user file exists.
|
||||||
File diff suppressed because it is too large
Load Diff
725
docs/superpowers/plans/2026-06-04-debug-logging-traceability.md
Normal file
725
docs/superpowers/plans/2026-06-04-debug-logging-traceability.md
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
# Debug Logging & Frontend↔Backend Traceability 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:** Build-configuration-driven logging — verbose in Debug builds (Rider run button), minimal `Warning`+ in Release (installed app) — with both processes writing one shared `claudedo-.log` and a `TaskId` correlation key threading UI→Worker→UI.
|
||||||
|
|
||||||
|
**Architecture:** A new `ClaudeDo.Logging` library owns all Serilog setup: a `BuildConfig.IsDebug` runtime check (via the entry assembly's `DebuggableAttribute`, no `#if DEBUG`), a default-`TaskId` enricher, and a `LoggingSetup.Configure` method that branches sinks/levels on `IsDebug`. Worker and App both call it. `TaskId` rides Serilog `LogContext`, pushed at the per-task entry points on each side.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, Serilog (core + File + Console sinks), Serilog.Extensions.Logging (App bridge), Serilog.AspNetCore (Worker, already present), xUnit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Create the `ClaudeDo.Logging` project
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Logging/ClaudeDo.Logging.csproj`
|
||||||
|
- Create: `src/ClaudeDo.Logging/Placeholder.cs` (temporary, removed in Task 2)
|
||||||
|
- Modify: `ClaudeDo.slnx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the csproj**
|
||||||
|
|
||||||
|
Create `src/ClaudeDo.Logging/ClaudeDo.Logging.csproj`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
```
|
||||||
|
|
||||||
|
> If NuGet reports a version conflict between `Serilog 4.1.0` and the `Serilog` core pulled transitively by `Serilog.AspNetCore 8.0.3` (Worker), align this `Serilog` version to whatever `Serilog.AspNetCore 8.0.3` resolves (check `dotnet list package --include-transitive`) and rebuild.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a temporary placeholder so the project compiles**
|
||||||
|
|
||||||
|
Create `src/ClaudeDo.Logging/Placeholder.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
internal static class Placeholder;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Register the project in the solution**
|
||||||
|
|
||||||
|
Edit `ClaudeDo.slnx` — add inside the `/src/` folder, after the `ClaudeDo.Localization` line:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Project Path="src/ClaudeDo.Logging/ClaudeDo.Logging.csproj" />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the new project**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Logging/ClaudeDo.Logging.csproj -c Release`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Logging/ClaudeDo.Logging.csproj src/ClaudeDo.Logging/Placeholder.cs ClaudeDo.slnx
|
||||||
|
git commit -m "build(logging): scaffold ClaudeDo.Logging project"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `DefaultTaskIdEnricher` (TDD)
|
||||||
|
|
||||||
|
Adds `TaskId = "-"` to any log event that doesn't already carry a `TaskId` property, so the `[{TaskId}]` column never renders the raw token. A pushed `LogContext` value takes precedence (because `Enrich.FromLogContext()` runs first and the property is then already present).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs`
|
||||||
|
- Delete: `src/ClaudeDo.Logging/Placeholder.cs`
|
||||||
|
- Create: `tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs`
|
||||||
|
- Modify: `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` (add project reference)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Reference `ClaudeDo.Logging` from the test project**
|
||||||
|
|
||||||
|
Edit `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` — add to the existing `ProjectReference` ItemGroup:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ProjectReference Include="..\..\src\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Logging;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Context;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Logging;
|
||||||
|
|
||||||
|
public sealed class DefaultTaskIdEnricherTests
|
||||||
|
{
|
||||||
|
private sealed class CollectingSink : ILogEventSink
|
||||||
|
{
|
||||||
|
public List<LogEvent> Events { get; } = new();
|
||||||
|
public void Emit(LogEvent logEvent) => Events.Add(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddsDash_WhenNoTaskIdInScope()
|
||||||
|
{
|
||||||
|
var sink = new CollectingSink();
|
||||||
|
using var logger = new LoggerConfiguration()
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.With(new DefaultTaskIdEnricher())
|
||||||
|
.WriteTo.Sink(sink)
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
logger.Information("hello");
|
||||||
|
|
||||||
|
var prop = Assert.Single(sink.Events).Properties["TaskId"];
|
||||||
|
Assert.Equal("\"-\"", prop.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KeepsPushedTaskId_WhenInScope()
|
||||||
|
{
|
||||||
|
var sink = new CollectingSink();
|
||||||
|
using var logger = new LoggerConfiguration()
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.With(new DefaultTaskIdEnricher())
|
||||||
|
.WriteTo.Sink(sink)
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
using (LogContext.PushProperty("TaskId", "task-42"))
|
||||||
|
logger.Information("hello");
|
||||||
|
|
||||||
|
var prop = Assert.Single(sink.Events).Properties["TaskId"];
|
||||||
|
Assert.Equal("\"task-42\"", prop.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DefaultTaskIdEnricherTests`
|
||||||
|
Expected: FAIL — `DefaultTaskIdEnricher` does not exist (compile error).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement the enricher and remove the placeholder**
|
||||||
|
|
||||||
|
Delete `src/ClaudeDo.Logging/Placeholder.cs`.
|
||||||
|
|
||||||
|
Create `src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
/// <summary>Ensures every log event carries a TaskId property (defaulting to "-")
|
||||||
|
/// so the output template's [{TaskId}] column never renders the raw token.</summary>
|
||||||
|
public sealed class DefaultTaskIdEnricher : ILogEventEnricher
|
||||||
|
{
|
||||||
|
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||||
|
{
|
||||||
|
if (!logEvent.Properties.ContainsKey("TaskId"))
|
||||||
|
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter DefaultTaskIdEnricherTests`
|
||||||
|
Expected: PASS (2 tests).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
|
||||||
|
git rm src/ClaudeDo.Logging/Placeholder.cs
|
||||||
|
git commit -m "feat(logging): default TaskId enricher with passing tests"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: `BuildConfig.IsDebug`
|
||||||
|
|
||||||
|
Detects whether the entry assembly was compiled in the Debug configuration (JIT optimizer disabled) — the runtime replacement for `#if DEBUG`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Logging/BuildConfig.cs`
|
||||||
|
- Create: `tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
The test asserts the property returns *some* bool without throwing, and that the underlying detection logic agrees with the test assembly's own `DebuggableAttribute` (the test runs under whatever config `dotnet test` used). We assert the helper's result equals a locally-computed expectation so it passes under both Debug and Release test runs.
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using ClaudeDo.Logging;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Logging;
|
||||||
|
|
||||||
|
public sealed class BuildConfigTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsDebug_MatchesEntryAssemblyDebuggableAttribute()
|
||||||
|
{
|
||||||
|
var entry = Assembly.GetEntryAssembly();
|
||||||
|
var expected = entry?
|
||||||
|
.GetCustomAttribute<DebuggableAttribute>()
|
||||||
|
?.IsJITOptimizerDisabled ?? false;
|
||||||
|
|
||||||
|
Assert.Equal(expected, BuildConfig.IsDebug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter BuildConfigTests`
|
||||||
|
Expected: FAIL — `BuildConfig` does not exist (compile error).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement `BuildConfig`**
|
||||||
|
|
||||||
|
Create `src/ClaudeDo.Logging/BuildConfig.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
/// <summary>Runtime build-configuration detection — the replacement for #if DEBUG.
|
||||||
|
/// Debug builds compile with the JIT optimizer disabled; Release builds enable it.</summary>
|
||||||
|
public static class BuildConfig
|
||||||
|
{
|
||||||
|
public static bool IsDebug { get; } =
|
||||||
|
Assembly.GetEntryAssembly()
|
||||||
|
?.GetCustomAttribute<DebuggableAttribute>()
|
||||||
|
?.IsJITOptimizerDisabled ?? false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter BuildConfigTests`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Logging/BuildConfig.cs tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs
|
||||||
|
git commit -m "feat(logging): runtime Debug-build detection via DebuggableAttribute"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: `LoggingSetup.Configure`
|
||||||
|
|
||||||
|
The single shared configuration entry point. Applies enrichers, the output template, and branches sinks/levels on `BuildConfig.IsDebug`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ClaudeDo.Logging/LoggingSetup.cs`
|
||||||
|
- Create: `tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Verifies a configured logger actually writes a `Warning` (emitted in both build configs) to a `claudedo-*.log` file under the given log root.
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using ClaudeDo.Logging;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Logging;
|
||||||
|
|
||||||
|
public sealed class LoggingSetupTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Configure_WritesSharedLogFile()
|
||||||
|
{
|
||||||
|
var logRoot = Path.Combine(Path.GetTempPath(), "claudedo-logtest-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(logRoot);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var logger = LoggingSetup.Configure(new LoggerConfiguration(), "test", logRoot).CreateLogger();
|
||||||
|
logger.Warning("marker-{Marker}", "xyz");
|
||||||
|
logger.Dispose(); // flush + release the file handle
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(logRoot, "claudedo-*.log");
|
||||||
|
var file = Assert.Single(files);
|
||||||
|
var contents = File.ReadAllText(file);
|
||||||
|
Assert.Contains("marker-", contents);
|
||||||
|
Assert.Contains("test/", contents); // {Process} tag in the template
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { Directory.Delete(logRoot, recursive: true); } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter LoggingSetupTests`
|
||||||
|
Expected: FAIL — `LoggingSetup` does not exist (compile error).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement `LoggingSetup`**
|
||||||
|
|
||||||
|
Create `src/ClaudeDo.Logging/LoggingSetup.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
public static class LoggingSetup
|
||||||
|
{
|
||||||
|
private const string OutputTemplate =
|
||||||
|
"[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}";
|
||||||
|
|
||||||
|
/// <summary>Apply the shared ClaudeDo logging configuration.
|
||||||
|
/// Debug builds: Debug level, console + shared file. Release builds: Warning level, shared file only.</summary>
|
||||||
|
/// <param name="processTag">"worker" or "app" — tags every line so the interleaved file is readable.</param>
|
||||||
|
/// <param name="logRoot">Directory for the shared claudedo-.log (created if missing).</param>
|
||||||
|
public static LoggerConfiguration Configure(LoggerConfiguration cfg, string processTag, string logRoot)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(logRoot);
|
||||||
|
var logFile = Path.Combine(logRoot, "claudedo-.log");
|
||||||
|
|
||||||
|
cfg.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithProperty("Process", processTag)
|
||||||
|
.Enrich.With(new DefaultTaskIdEnricher());
|
||||||
|
|
||||||
|
if (BuildConfig.IsDebug)
|
||||||
|
{
|
||||||
|
cfg.MinimumLevel.Debug()
|
||||||
|
.WriteTo.Console(outputTemplate: OutputTemplate)
|
||||||
|
.WriteTo.File(
|
||||||
|
logFile,
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 2,
|
||||||
|
shared: true,
|
||||||
|
outputTemplate: OutputTemplate);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cfg.MinimumLevel.Warning()
|
||||||
|
.WriteTo.File(
|
||||||
|
logFile,
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 2,
|
||||||
|
shared: true,
|
||||||
|
outputTemplate: OutputTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter LoggingSetupTests`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Logging/LoggingSetup.cs tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs
|
||||||
|
git commit -m "feat(logging): shared LoggingSetup with build-config sink branching"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Wire the Worker to the shared setup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Program.cs:34-40`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the project reference**
|
||||||
|
|
||||||
|
Edit `src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` — add to the existing `ProjectReference` ItemGroup (the one with `ClaudeDo.Data`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace the inline Serilog config**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Program.cs`, replace lines 34-40:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Host.UseSerilog((ctx, lc) => lc
|
||||||
|
.MinimumLevel.Information()
|
||||||
|
.WriteTo.File(
|
||||||
|
System.IO.Path.Combine(logRoot, "worker-.log"),
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 7,
|
||||||
|
shared: true));
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Host.UseSerilog((ctx, lc) =>
|
||||||
|
ClaudeDo.Logging.LoggingSetup.Configure(lc, "worker", logRoot));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build the Worker**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
Expected: Build succeeded. (If the Worker is running and locks the Debug output, this Release build is unaffected.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/ClaudeDo.Worker.csproj src/ClaudeDo.Worker/Program.cs
|
||||||
|
git commit -m "feat(logging): route Worker logging through shared LoggingSetup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Wire the App/Ui (currently log-silent) to the shared setup
|
||||||
|
|
||||||
|
The App uses a plain `ServiceCollection` with **no** logging registered. Add the Serilog→`ILogger` bridge so all `ILogger<T>` injections across App/Ui flow to the shared sinks, and flush on shutdown.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.App/ClaudeDo.App.csproj`
|
||||||
|
- Modify: `src/ClaudeDo.App/Program.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add packages and the project reference**
|
||||||
|
|
||||||
|
Edit `src/ClaudeDo.App/ClaudeDo.App.csproj` — add to the package `ItemGroup`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||||
|
```
|
||||||
|
|
||||||
|
and to the `ProjectReference` ItemGroup:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the logging registration in `BuildServices`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.App/Program.cs`, inside `BuildServices()`, immediately after the `var sc = new ServiceCollection();` line (currently line 78), insert:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var logRoot = Path.Combine(Path.GetDirectoryName(dbPath)!, "logs");
|
||||||
|
var serilogLogger = ClaudeDo.Logging.LoggingSetup
|
||||||
|
.Configure(new Serilog.LoggerConfiguration(), "app", logRoot)
|
||||||
|
.CreateLogger();
|
||||||
|
sc.AddLogging(b => b.AddSerilog(serilogLogger, dispose: true));
|
||||||
|
```
|
||||||
|
|
||||||
|
Add these usings to the top of `Program.cs` (the `AddSerilog` `ILoggingBuilder` extension lives in the `Serilog` namespace; `AddLogging` lives in `Microsoft.Extensions.DependencyInjection`, already imported):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Serilog;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
```
|
||||||
|
|
||||||
|
> `dbPath` is already computed just above (`var dbPath = Paths.Expand(settings.DbPath);`). Its parent directory is `~/.todo-app`, so `logs` sits beside the Worker's log root.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build the App**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
Expected: Build succeeded (pulls in Ui + Data + Logging).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify manually from Rider (visual-verification gap)**
|
||||||
|
|
||||||
|
This is a Debug-build behavior that cannot be asserted in a Release test run. Launch the App from Rider's run button and confirm:
|
||||||
|
- A `claudedo-*.log` appears in `~/.todo-app/logs/`.
|
||||||
|
- Console output (Rider run window) shows `Debug`-level lines tagged `app/...`.
|
||||||
|
|
||||||
|
Flag to the user that this step needs their eyes.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.App/ClaudeDo.App.csproj src/ClaudeDo.App/Program.cs
|
||||||
|
git commit -m "feat(logging): wire App/Ui logging to shared LoggingSetup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Push `TaskId` into `LogContext` in the Worker
|
||||||
|
|
||||||
|
Wraps the two per-task entry points so every nested log line (runner, state service, worktree, planning) carries the task's id automatically.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs:47` (`RunAsync`) and `:171` (`ContinueAsync`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the using directive**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Worker/Runner/TaskRunner.cs`, add to the top usings:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Serilog.Context;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Push TaskId at the top of `RunAsync`**
|
||||||
|
|
||||||
|
In `RunAsync` (line 47), insert as the very first statement of the method body (before `string? mcpToken = null;`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var _taskScope = LogContext.PushProperty("TaskId", task.Id);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Push TaskId at the top of `ContinueAsync`**
|
||||||
|
|
||||||
|
In `ContinueAsync` (line 171), insert as the very first statement of the method body (before `TaskEntity task;`). The parameter is `taskId`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using var _taskScope = LogContext.PushProperty("TaskId", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the Worker**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs
|
||||||
|
git commit -m "feat(logging): tag Worker task execution with TaskId for traceability"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Push `TaskId` and add trace lines on the App side
|
||||||
|
|
||||||
|
`WorkerClient` currently logs nothing. Inject `ILogger<WorkerClient>`, add a small helper that pushes `TaskId` + emits a `Debug` trace line, and route the fire-and-forget task-targeted hub calls through it. This produces the UI half of the UI→Worker→UI trace under a shared `TaskId`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.App/Program.cs:101` (registration)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add usings and the logger field/ctor param**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add to the usings:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog.Context;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a field beside `private readonly HubConnection _hub;` (line 32):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private readonly ILogger<WorkerClient> _logger;
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the constructor signature (line 68) from:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public WorkerClient(string signalRUrl)
|
||||||
|
{
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public WorkerClient(string signalRUrl, ILogger<WorkerClient> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the task-scoped invoke helper**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, add this private method next to `TryInvokeAsync` (after line 241):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line.</summary>
|
||||||
|
private async Task InvokeForTaskAsync(string taskId, string method, params object?[] args)
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("TaskId", taskId))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("UI invoking {Method} for task {TaskId}", method, taskId);
|
||||||
|
await _hub.InvokeCoreAsync(method, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Route the fire-and-forget task actions through the helper**
|
||||||
|
|
||||||
|
In the same file, replace each of these method bodies:
|
||||||
|
|
||||||
|
`RunNowAsync` (line 243):
|
||||||
|
```csharp
|
||||||
|
public Task RunNowAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "RunNow", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
`ContinueTaskAsync` (line 248):
|
||||||
|
```csharp
|
||||||
|
public Task ContinueTaskAsync(string taskId, string followUpPrompt)
|
||||||
|
=> InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt);
|
||||||
|
```
|
||||||
|
|
||||||
|
`ResetTaskAsync` (line 253):
|
||||||
|
```csharp
|
||||||
|
public Task ResetTaskAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "ResetTask", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
`CancelTaskAsync` (line 267):
|
||||||
|
```csharp
|
||||||
|
public Task CancelTaskAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "CancelTask", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
`ApproveReviewAsync` (line 389):
|
||||||
|
```csharp
|
||||||
|
public Task ApproveReviewAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "ApproveReview", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
`RejectReviewToQueueAsync` (line 394):
|
||||||
|
```csharp
|
||||||
|
public Task RejectReviewToQueueAsync(string taskId, string feedback)
|
||||||
|
=> InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback);
|
||||||
|
```
|
||||||
|
|
||||||
|
`RejectReviewToIdleAsync` (line 399):
|
||||||
|
```csharp
|
||||||
|
public Task RejectReviewToIdleAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
`CancelReviewAsync` (line 404):
|
||||||
|
```csharp
|
||||||
|
public Task CancelReviewAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "CancelReview", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
> These all previously did `await _hub.InvokeAsync(method, ...)` with no return value, so converting them to expression-bodied delegations preserves behavior. Do **not** touch methods that return DTOs (e.g. `MergeTaskAsync`) or the planning methods — keep this change scoped to the void task actions above.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update the DI registration to pass the logger**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.App/Program.cs`, replace line 101:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
sc.AddSingleton(sp => new WorkerClient(
|
||||||
|
sp.GetRequiredService<AppSettings>().SignalRUrl,
|
||||||
|
sp.GetRequiredService<ILogger<WorkerClient>>()));
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using Microsoft.Extensions.Logging;` to the top of `Program.cs` if not already present.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build the App**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
> Note: `WorkerClient` is faked in tests via the `IWorkerClient` *interface* (hand-rolled fakes implement the interface, they do not subclass `WorkerClient`). This change adds a ctor parameter to the concrete class only and does not alter `IWorkerClient`, so the fakes are unaffected. Confirm by building the test projects in the next step.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Build the test projects to confirm fakes still compile**
|
||||||
|
|
||||||
|
Run: `dotnet build tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release && dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||||
|
Expected: Build succeeded for both.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run the full Worker.Tests suite**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||||
|
Expected: PASS (all existing tests + the 4 new logging tests).
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.App/Program.cs
|
||||||
|
git commit -m "feat(logging): tag UI task actions with TaskId + debug trace lines"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification
|
||||||
|
|
||||||
|
- [ ] **Build the whole desktop + worker stack in Release:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Run the logging tests:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~Logging"
|
||||||
|
```
|
||||||
|
Expected: PASS (DefaultTaskIdEnricher × 2, BuildConfig × 1, LoggingSetup × 1).
|
||||||
|
|
||||||
|
- [ ] **Manual smoke test (visual-verification gap — needs the user):**
|
||||||
|
1. Run the Worker and App from Rider (Debug build). Confirm both write to one `~/.todo-app/logs/claudedo-*.log` with `app/...` and `worker/...` lines.
|
||||||
|
2. Run a task; grep that file for the task's id — confirm UI (`UI invoking RunNow…`) and Worker lines share the same `[<taskId>]`.
|
||||||
|
3. Build/install the Release app; confirm the log is near-silent (no `Debug`/`Information` noise, `Warning`+ only) and no console window logging.
|
||||||
1107
docs/superpowers/plans/2026-06-04-inherited-settings-and-turns.md
Normal file
1107
docs/superpowers/plans/2026-06-04-inherited-settings-and-turns.md
Normal file
File diff suppressed because it is too large
Load Diff
147
docs/superpowers/plans/2026-06-04-myday-icons-terminal-reuse.md
Normal file
147
docs/superpowers/plans/2026-06-04-myday-icons-terminal-reuse.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# MyDay Icon Buttons + Terminal Reuse + Sort Icon Fix — Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||||
|
|
||||||
|
**Goal:** Move the "Clear day" and "Prep log" actions into the MyDay header icon row as icon buttons (broom + list), render the prep log in the real `SessionTerminalView` ("cool terminal") by making that control reusable, and fix the invisible Sort icon.
|
||||||
|
|
||||||
|
**Approved design (chat):**
|
||||||
|
- Header icon row (`TasksIslandView.axaml`, the Sort/Eye/Settings `icon-btn` StackPanel) gets two more `icon-btn`, both `IsVisible="{Binding IsMyDayList}"`, inserted after the Eye button: **broom** (`Icon.Broom`) → `ClearDayCommand`, **list** (`Icon.List`) → `ShowPrepLogCommand`. The two full-width text buttons "Prep log" and "Clear day" are removed. "Tag vorbereiten" stays as the full-width button (already opens the prep view via `PrepRequested`).
|
||||||
|
- `SessionTerminalView` becomes reusable via StyledProperties so it renders both the task `Log` and the prep `PrepLog` with the same terminal look. The prep panel in `DetailsIslandView` embeds it instead of the copied `ItemsControl`.
|
||||||
|
- **Sort icon bug:** `PathIcon` fills geometry; `Icon.Sort` is an open-line path (no enclosed area) → invisible. Replace with a filled geometry. New icons (Broom, List) are authored as filled geometries too.
|
||||||
|
|
||||||
|
**Tech:** Avalonia (PathIcon/StreamGeometry, StyledProperty), CommunityToolkit.Mvvm, xUnit.
|
||||||
|
|
||||||
|
## Build/test
|
||||||
|
`.slnx` needs .NET 9 — build the csproj. Use `-c Release` if a Worker locks Debug.
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
GUI cannot be smoke-tested headlessly — note it; the human verifies visuals.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task A: Icons + reusable SessionTerminalView
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (icon geometries)
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml` + `SessionTerminalView.axaml.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` (both embeds)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Fix `Icon.Sort` + add `Icon.Broom`, `Icon.List`** as filled geometries in `IslandStyles.axaml` (in the `Styles.Resources` icon block). Replace the existing `Icon.Sort` line and add the two new ones:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Icon.Sort (filled bars, decreasing width) -->
|
||||||
|
<StreamGeometry x:Key="Icon.Sort">M4 6 H20 V8 H4 Z M4 11 H16 V13 H4 Z M4 16 H11 V18 H4 Z</StreamGeometry>
|
||||||
|
|
||||||
|
<!-- Icon.Broom (filled: handle + binding band + flared bristles) -->
|
||||||
|
<StreamGeometry x:Key="Icon.Broom">M11 3 H13 V10 H11 Z M8.5 10 H15.5 V12 H8.5 Z M9 12 H15 L17 21 H7 Z</StreamGeometry>
|
||||||
|
|
||||||
|
<!-- Icon.List (filled: square bullets + lines) -->
|
||||||
|
<StreamGeometry x:Key="Icon.List">M4 5 H6 V7 H4 Z M8 5 H20 V7 H8 Z M4 11 H6 V13 H4 Z M8 11 H20 V13 H8 Z M4 17 H6 V19 H4 Z M8 17 H20 V19 H8 Z</StreamGeometry>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add StyledProperties to `SessionTerminalView`** (code-behind `SessionTerminalView.axaml.cs`). Add public StyledProperties and CLR wrappers:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static readonly StyledProperty<System.Collections.IEnumerable?> EntriesProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, System.Collections.IEnumerable?>(nameof(Entries));
|
||||||
|
public static readonly StyledProperty<string?> LabelProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(Label));
|
||||||
|
public static readonly StyledProperty<bool> IsRunningProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsRunning));
|
||||||
|
public static readonly StyledProperty<bool> IsDoneProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsDone));
|
||||||
|
public static readonly StyledProperty<bool> IsFailedProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsFailed));
|
||||||
|
|
||||||
|
public System.Collections.IEnumerable? Entries { get => GetValue(EntriesProperty); set => SetValue(EntriesProperty, value); }
|
||||||
|
public string? Label { get => GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
|
||||||
|
public bool IsRunning { get => GetValue(IsRunningProperty); set => SetValue(IsRunningProperty, value); }
|
||||||
|
public bool IsDone { get => GetValue(IsDoneProperty); set => SetValue(IsDoneProperty, value); }
|
||||||
|
public bool IsFailed { get => GetValue(IsFailedProperty); set => SetValue(IsFailedProperty, value); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the existing auto-scroll hook (which cast `DataContext as DetailsIslandViewModel` and watched `.Log.CollectionChanged`) with one that watches whichever collection `Entries` points at: in `OnPropertyChanged`, when `change.Property == EntriesProperty`, detach the old `INotifyCollectionChanged.CollectionChanged` handler and attach to the new value (if it implements `INotifyCollectionChanged`); the handler scrolls the existing ScrollViewer to the end (reuse the existing scroll logic / named ScrollViewer). Keep the named ScrollViewer's `x:Name`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Repoint `SessionTerminalView.axaml` internal bindings to the control's own properties.** Give the root `UserControl` `x:Name="Root"`. Change:
|
||||||
|
- the `ItemsControl ItemsSource="{Binding Log}"` → `ItemsSource="{Binding #Root.Entries}"`
|
||||||
|
- the label `TextBlock` `Text="{Binding BranchLine, StringFormat='claude-session · {0}'}"` (or whatever it is) → `Text="{Binding #Root.Label}"`
|
||||||
|
- the LIVE chip `IsVisible="{Binding IsRunning}"` → `{Binding #Root.IsRunning}`; DONE → `#Root.IsDone`; FAILED → `#Root.IsFailed`.
|
||||||
|
Keep the `LogLineViewModel` item template as-is (it binds the item, not the VM). The `x:DataType` can stay `DetailsIslandViewModel` (element-name bindings to `#Root` don't depend on it) or be removed if it causes compile issues — verify the build.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update both embeds in `DetailsIslandView.axaml`.**
|
||||||
|
- Task embed (currently `<islands:SessionTerminalView MaxHeight="420"/>`):
|
||||||
|
```xml
|
||||||
|
<islands:SessionTerminalView MaxHeight="420"
|
||||||
|
Entries="{Binding Log}"
|
||||||
|
Label="{Binding BranchLine, StringFormat='claude-session · {0}'}"
|
||||||
|
IsRunning="{Binding IsRunning}" IsDone="{Binding IsDone}" IsFailed="{Binding IsFailed}"/>
|
||||||
|
```
|
||||||
|
(Use the exact label binding the old internal header used — match the prior `StringFormat` text precisely so the task view is visually unchanged.)
|
||||||
|
- Prep panel: replace the whole copied `ItemsControl` (and its surrounding `ScrollViewer`/title) with:
|
||||||
|
```xml
|
||||||
|
<islands:SessionTerminalView
|
||||||
|
Entries="{Binding PrepLog}" Label="daily-prep"
|
||||||
|
IsRunning="{Binding IsPrepRunning}"/>
|
||||||
|
```
|
||||||
|
Keep the panel wrapper `<Panel IsVisible="{Binding IsPrepMode}">`. Drop the now-redundant `details.prepTitle` title TextBlock (the terminal header shows the `daily-prep` label). Leave the `details.prepTitle` locale key in place (harmless) OR remove it from both en/de if you prefer — if removing, run the localization test.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build the App; confirm no binding/compile errors.**
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
(The existing DetailsIsland prep tests must still pass — `PrepLog`/`IsPrepMode`/`ShowPrep` are unchanged.)
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit** (stage only Task A files; do NOT `git add -A`):
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(daily-prep): reuse SessionTerminal for prep log; fix invisible Sort icon; add Broom/List icons"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task B: MyDay header icon buttons
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||||
|
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json`
|
||||||
|
|
||||||
|
Depends on Task A (uses `Icon.Broom` / `Icon.List`).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add two `icon-btn` to the header icon StackPanel** (the one with Sort/Eye/Settings), inserted right after the Eye button and before Settings, both MyDay-only:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
|
||||||
|
Command="{Binding ClearDayCommand}" ToolTip.Tip="{loc:Tr tasks.clearDayTip}">
|
||||||
|
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Broom}"/>
|
||||||
|
</Button>
|
||||||
|
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
|
||||||
|
Command="{Binding ShowPrepLogCommand}" ToolTip.Tip="{loc:Tr tasks.prepLogTip}">
|
||||||
|
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.List}"/>
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove the two full-width buttons** "Prep log" (`ShowPrepLogCommand`) and "Clear day" (`ClearDayCommand`) from the DockPanel button stack. Keep the "Prepare day" (`PrepareDayCommand`) full-width button and the Notes pinned-row button.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Locales.** Add `tasks.clearDayTip` (en "Clear day", de "Tag leeren") and `tasks.prepLogTip` (en "Prep log", de "Vorbereitungs-Log") to both json files. Remove the now-unused `tasks.clearDay` and `tasks.prepLog` keys from both (keep en/de in parity).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build + test.**
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Manual smoke (human):** on MyDay the header shows Sort (now visible) + Eye + Broom + List + Settings; broom clears the day; list opens the prep terminal; "Tag vorbereiten" opens the prep terminal and streams; the three MyDay-only controls hide on other lists; the task session terminal still renders normally.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit** (stage only Task B files):
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(daily-prep): move Clear-day and Prep-log into MyDay header icon row"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes / risks
|
||||||
|
- Element-name bindings (`#Root.*`) require the `UserControl` to have `x:Name="Root"`; verify compiled bindings accept them (they do in Avalonia).
|
||||||
|
- The auto-scroll hook must re-subscribe when `Entries` changes; without it the prep log won't auto-scroll.
|
||||||
|
- `ClearDayCommand` / `ShowPrepLogCommand` already exist on `TasksIslandViewModel` — no VM changes; existing VM tests remain valid.
|
||||||
120
docs/superpowers/plans/2026-06-04-plan-day-in-log-window.md
Normal file
120
docs/superpowers/plans/2026-06-04-plan-day-in-log-window.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Move "Plan day" into the Prep-Log Window — Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||||
|
|
||||||
|
**Goal:** Guard daily-prep planning behind a second click. The MyDay header's full-width "Tag vorbereiten" button is removed; instead the user opens the prep-log window (list icon), sees the last run or an empty-state hint, and clicks a **"Plan day"** button inside that window to run the prep.
|
||||||
|
|
||||||
|
**Approved flow:** Header list-icon (`ShowPrepLogCommand`) opens the prep window → if empty, an empty-state hint shows → "Plan day" button in the window runs `RunDailyPrepNowAsync()`.
|
||||||
|
|
||||||
|
**Tech:** Avalonia + CommunityToolkit.Mvvm, xUnit.
|
||||||
|
|
||||||
|
## Build/test
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
GUI not headlessly verifiable — note it; human verifies visuals.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task: relocate planning trigger + empty-state
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs` (remove PrepareDay)
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml` (remove header button)
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` (PlanDayCommand + empty-state)
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml` (prep panel toolbar + empty hint)
|
||||||
|
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json`
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPrepModeTests.cs`, and the existing `TasksIslandDailyPrepTests.cs` (remove the obsolete prepare test)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write/adjust tests first.**
|
||||||
|
- In `DetailsIslandPrepModeTests.cs` add:
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task PlanDayCommand_calls_worker()
|
||||||
|
{
|
||||||
|
var stub = new StubWorkerClient();
|
||||||
|
var vm = NewDetailsVm(stub);
|
||||||
|
await vm.PlanDayCommand.ExecuteAsync(null);
|
||||||
|
Assert.Equal(1, stub.RunDailyPrepNowCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShowPrepEmptyState_true_when_empty_and_not_running()
|
||||||
|
{
|
||||||
|
var vm = NewDetailsVm(new StubWorkerClient());
|
||||||
|
Assert.True(vm.ShowPrepEmptyState);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`StubWorkerClient` needs a `RunDailyPrepNowCalls` counter incremented in `RunDailyPrepNowAsync` (add if missing; it currently likely returns `Task.FromResult(true)` — keep that and bump a counter).
|
||||||
|
- In `TasksIslandDailyPrepTests.cs` **remove** `PrepareDayCommand_raises_PrepRequested` (the command is being deleted). Keep `ClearDayCommand_calls_worker`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run — expect FAIL/compile error.**
|
||||||
|
|
||||||
|
- [ ] **Step 3: `TasksIslandViewModel` — remove planning trigger.**
|
||||||
|
- Delete the `PrepareDayAsync` `[RelayCommand]` entirely.
|
||||||
|
- Keep the `PrepRequested` event and `ShowPrepLog` command (the list icon still raises `PrepRequested` to open the window).
|
||||||
|
- Grep the VM for any remaining `PrepareDay` references and remove them.
|
||||||
|
|
||||||
|
- [ ] **Step 4: `TasksIslandView.axaml` — remove the header button.** Delete the full-width "Prepare day" `<Button … Command="{Binding PrepareDayCommand}" …>`. Leave the Notes pinned-row button, and the header icon buttons (broom = ClearDay, list = ShowPrepLog) untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 5: `DetailsIslandViewModel` — add PlanDayCommand + empty-state.**
|
||||||
|
- Add:
|
||||||
|
```csharp
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task PlanDayAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
try { await _worker.RunDailyPrepNowAsync(); }
|
||||||
|
catch { /* worker offline; PrepStarted/PrepLine will reconcile */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowPrepEmptyState => !IsPrepRunning && PrepLog.Count == 0;
|
||||||
|
```
|
||||||
|
- Notify `ShowPrepEmptyState`: in the constructor add `PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));`, and add `partial void OnIsPrepRunningChanged(bool value) => OnPropertyChanged(nameof(ShowPrepEmptyState));`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: `DetailsIslandView.axaml` — prep panel toolbar + empty hint.** In the `<Panel IsVisible="{Binding IsPrepMode}">`, wrap the existing `SessionTerminalView` in a `DockPanel`; dock a top toolbar row with the Plan-day button, and overlay/stack an empty-state hint:
|
||||||
|
```xml
|
||||||
|
<Panel IsVisible="{Binding IsPrepMode}">
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Top" Padding="12,8">
|
||||||
|
<Button Classes="btn primary"
|
||||||
|
Command="{Binding PlanDayCommand}"
|
||||||
|
IsEnabled="{Binding !IsPrepRunning}"
|
||||||
|
Content="{loc:Tr details.planDay}"/>
|
||||||
|
</Border>
|
||||||
|
<Panel>
|
||||||
|
<islands:SessionTerminalView
|
||||||
|
Entries="{Binding PrepLog}" Label="daily-prep"
|
||||||
|
IsRunning="{Binding IsPrepRunning}"/>
|
||||||
|
<TextBlock IsVisible="{Binding ShowPrepEmptyState}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
|
Text="{loc:Tr details.prepEmpty}"/>
|
||||||
|
</Panel>
|
||||||
|
</DockPanel>
|
||||||
|
</Panel>
|
||||||
|
```
|
||||||
|
(Match the surrounding view's class names/brushes; use the existing button class style seen elsewhere, e.g. `Classes="btn"` — verify `primary` exists, else plain `btn`.)
|
||||||
|
|
||||||
|
- [ ] **Step 7: Locales.** Add `details.planDay` (en "Plan day", de "Tag planen") and `details.prepEmpty` (en "No prep run today yet — click Plan day", de "Heute noch keine Vorbereitung — klick Tag planen") to both json files. Remove the now-unused `tasks.prepareDay` key from both (grep first to confirm no other reference). Keep en/de key parity.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Build + tests.**
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 9: Manual smoke (human):** on MyDay there is no "Tag vorbereiten" button; the list icon opens the prep window showing the empty hint; "Plan day" runs the prep and streams; the hint disappears while running; after restart the persisted last run shows and "Plan day" is available to re-run.
|
||||||
|
|
||||||
|
- [ ] **Step 10: Commit:**
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(daily-prep): trigger planning from inside the prep-log window with an empty-state hint"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes / risks
|
||||||
|
- `PrepRequested` and `ShowPrepLogCommand` stay — only `PrepareDayCommand` and its header button are removed.
|
||||||
|
- `ShowPrepEmptyState` must re-notify on both `PrepLog` changes and `IsPrepRunning` changes, else the hint won't hide when a run starts or lines arrive.
|
||||||
|
- Removing `tasks.prepareDay`: confirm via grep it has no remaining references before deleting (keep locale parity or the Localization.Tests parity check fails).
|
||||||
208
docs/superpowers/plans/2026-06-04-prep-log-persistence.md
Normal file
208
docs/superpowers/plans/2026-06-04-prep-log-persistence.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# Persist Daily-Prep Log Across Restarts — Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`.
|
||||||
|
|
||||||
|
**Goal:** The prep log currently lives only in memory (`DetailsIslandViewModel.PrepLog`), so after an app restart the prep terminal is empty. Persist the last prep run's output to a file in the worker and load it into the prep terminal when opened.
|
||||||
|
|
||||||
|
**Root cause (confirmed):** `PrimeRunner.FireAsync` streams stdout lines via `_broadcaster.PrepLineAsync(line)` only — it writes no file and stores no record. `PrepLog` is an in-memory `ObservableCollection` populated only by live `PrepLine` events. Nothing persists → empty after restart.
|
||||||
|
|
||||||
|
**Approach:** Worker writes each streamed line to `<appdata>/logs/daily-prep.log` (truncated at run start = last run only) using the existing `LogWriter`. A new hub method `GetLastPrepLog()` returns the file (tail-capped, like `get_task_log`). The UI loads it into `PrepLog` when the prep view opens, but only when `PrepLog` is empty and no run is in progress.
|
||||||
|
|
||||||
|
**Tech:** ASP.NET Core SignalR, Avalonia + CommunityToolkit.Mvvm, xUnit.
|
||||||
|
|
||||||
|
## Build/test
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
GUI not headlessly verifiable — note it; human verifies visuals.
|
||||||
|
|
||||||
|
## Shared constant
|
||||||
|
The prep-log path must be identical in `PrimeRunner` (writer) and `WorkerHub` (reader). Define it once and reference from both:
|
||||||
|
`Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "logs", "daily-prep.log")`.
|
||||||
|
Add a small static helper so both sides agree, e.g. in `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` (already the prep "home"):
|
||||||
|
```csharp
|
||||||
|
public static string LogPath() =>
|
||||||
|
System.IO.Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "logs", "daily-prep.log");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Worker — write the prep log + serve it
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` (add `LogPath()` helper)
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `DailyPrepPrompt.LogPath()`** (code above).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test.** Extend the existing streaming test (or add one) asserting that after `FireAsync` with emitted stdout lines, the file at `DailyPrepPrompt.LogPath()` contains those lines, and that a prior run's content is replaced (truncate-on-start). Since the path is the real app-data logs dir, the test should delete the file first and clean up after; assert exact line content.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task FireAsync_writes_last_run_to_prep_log_file()
|
||||||
|
{
|
||||||
|
var path = DailyPrepPrompt.LogPath();
|
||||||
|
if (File.Exists(path)) File.Delete(path);
|
||||||
|
|
||||||
|
var claude = new FakeClaudeProcess(emitLines: new[] { "lineA", "lineB" }, exitCode: 0, result: "ok");
|
||||||
|
var runner = NewRunner(claude, new RecordingPrimeBroadcaster());
|
||||||
|
await runner.FireAsync(new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null), CancellationToken.None);
|
||||||
|
|
||||||
|
var contents = await File.ReadAllTextAsync(path);
|
||||||
|
Assert.Contains("lineA", contents);
|
||||||
|
Assert.Contains("lineB", contents);
|
||||||
|
|
||||||
|
// Truncation: a second run with different lines replaces the file.
|
||||||
|
var claude2 = new FakeClaudeProcess(emitLines: new[] { "lineC" }, exitCode: 0, result: "ok");
|
||||||
|
var runner2 = NewRunner(claude2, new RecordingPrimeBroadcaster());
|
||||||
|
await runner2.FireAsync(new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null), CancellationToken.None);
|
||||||
|
var after = await File.ReadAllTextAsync(path);
|
||||||
|
Assert.DoesNotContain("lineA", after);
|
||||||
|
Assert.Contains("lineC", after);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run — expect FAIL.**
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write the file in `PrimeRunner.FireAsync`.** After the gate is acquired and before `RunAsync`: compute `var logPath = DailyPrepPrompt.LogPath();`, delete it if present (truncate → last run only), then create `await using var logWriter = new LogWriter(logPath);`. Change the stream callback to write AND broadcast:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var logPath = DailyPrepPrompt.LogPath();
|
||||||
|
try { if (File.Exists(logPath)) File.Delete(logPath); } catch { /* best effort */ }
|
||||||
|
await using var logWriter = new LogWriter(logPath);
|
||||||
|
|
||||||
|
await _broadcaster.PrepStartedAsync();
|
||||||
|
// ... build prompt/args/timeoutCts ...
|
||||||
|
var result = await _claude.RunAsync(
|
||||||
|
arguments: args, prompt: prompt, workingDirectory: cwd,
|
||||||
|
onStdoutLine: async line =>
|
||||||
|
{
|
||||||
|
await logWriter.WriteLineAsync(line);
|
||||||
|
await _broadcaster.PrepLineAsync(line);
|
||||||
|
},
|
||||||
|
ct: timeoutCts.Token);
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the existing `success`/`finally`/`PrepFinishedAsync`/gate logic. `using ClaudeDo.Worker.Runner;` is already present (LogWriter lives there). The `await using` LogWriter disposes (flushes) before the method returns.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run — expect PASS.** Build the Worker.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Add `WorkerHub.GetLastPrepLog()`** (no ctor change — reads the static path):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<string> GetLastPrepLog()
|
||||||
|
{
|
||||||
|
var path = DailyPrepPrompt.LogPath();
|
||||||
|
if (!File.Exists(path)) return Task.FromResult(string.Empty);
|
||||||
|
|
||||||
|
const int maxBytes = 256 * 1024;
|
||||||
|
var bytes = File.ReadAllBytes(path);
|
||||||
|
var text = bytes.Length <= maxBytes
|
||||||
|
? System.Text.Encoding.UTF8.GetString(bytes)
|
||||||
|
: System.Text.Encoding.UTF8.GetString(bytes, bytes.Length - maxBytes, maxBytes);
|
||||||
|
return Task.FromResult(text);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `using ClaudeDo.Worker.Prime;` to `WorkerHub.cs` if not present.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Build Worker; run the full Worker.Tests project.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit** (stage only Task 1 files):
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(daily-prep): persist last prep run to a log file and serve it via GetLastPrepLog"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: UI — load the persisted prep log when opening
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||||
|
- Modify fakes: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` (FakeWorkerClient)
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPrepModeTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Declare on `IWorkerClient`:** `Task<string> GetLastPrepLogAsync();`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement in `WorkerClient`:** `public Task<string> GetLastPrepLogAsync() => _hub.InvokeAsync<string>("GetLastPrepLog");` (match neighbouring call style; if there is a `TryInvokeAsync` helper for resilience, mirror `GetWeekReportAsync` and return `?? string.Empty`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update fakes.** Add `public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);` to both fakes. In `StubWorkerClient`, make it return a settable backing field, e.g. `public string LastPrepLog = ""; public Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write the failing test.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task ShowPrep_loads_persisted_log_when_empty()
|
||||||
|
{
|
||||||
|
var stub = new StubWorkerClient { LastPrepLog = "{\"type\":\"assistant\",\"text\":\"restored\"}" };
|
||||||
|
var vm = NewDetailsVm(stub);
|
||||||
|
|
||||||
|
vm.ShowPrep();
|
||||||
|
await Task.Delay(50); // allow the async load to run; or expose the load task to await deterministically
|
||||||
|
|
||||||
|
Assert.NotEmpty(vm.PrepLog);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer determinism over `Task.Delay`: have `ShowPrep` start the load and expose the in-flight `Task` (e.g. a `LoadLastPrepLogAsync()` method the test can call/await directly), then assert. Use whichever the existing test style favors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement load in `DetailsIslandViewModel`.** Add a method and call it from `ShowPrep`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void ShowPrep()
|
||||||
|
{
|
||||||
|
Bind(null);
|
||||||
|
IsNotesMode = false;
|
||||||
|
IsPrepMode = true;
|
||||||
|
_ = LoadLastPrepLogIfEmptyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadLastPrepLogIfEmptyAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null || IsPrepRunning || PrepLog.Count > 0) return;
|
||||||
|
string text;
|
||||||
|
try { text = await _worker.GetLastPrepLogAsync(); }
|
||||||
|
catch { return; }
|
||||||
|
if (IsPrepRunning || PrepLog.Count > 0) return; // a live run may have started meanwhile
|
||||||
|
foreach (var line in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.TrimEnd('\r');
|
||||||
|
if (trimmed.Length > 0) AppendStdoutLine(PrepLog, trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This reuses the existing `AppendStdoutLine(PrepLog, line)` formatter path, so persisted NDJSON renders identically to the live stream. The guards ensure it never overwrites a live run (`PrepStarted` clears `PrepLog` and sets `IsPrepRunning`) or an already-loaded log.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Build App + run UI tests.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Manual smoke (human):** run a prep, restart the app, open the prep log on MyDay → the last run's output is shown.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit** (stage only Task 2 files):
|
||||||
|
```bash
|
||||||
|
git commit -m "feat(daily-prep): load persisted prep log into the terminal on open"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes / risks
|
||||||
|
- `PrimeRunner` writes via the same `LogWriter` pattern `TaskRunner` uses; concurrency behavior matches existing code (no new locking introduced).
|
||||||
|
- Path is shared via `DailyPrepPrompt.LogPath()` so writer and reader never diverge.
|
||||||
|
- Load is guarded (`PrepLog empty && !IsPrepRunning`) to avoid clobbering a live stream — the order of `ShowPrep`'s flag set vs. the async load matters; re-check the guard after the await.
|
||||||
|
- Last run only (file truncated each run); history is out of scope.
|
||||||
74
docs/superpowers/plans/2026-06-04-review-and-roadblock-ux.md
Normal file
74
docs/superpowers/plans/2026-06-04-review-and-roadblock-ux.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Review & Roadblock UX Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** execute task-by-task (subagent-driven-development). Steps use `- [ ]`.
|
||||||
|
|
||||||
|
**Goal:** Move the task-row review actions into the Details panel, give the Details panel a real `WaitingForReview` state + a populated diff meter, and add a glanceable yellow roadblock indicator on the task card.
|
||||||
|
|
||||||
|
**Architecture:** Persist a `RoadblockCount` on `TaskEntity` (set by the runner when it folds in `CLAUDEDO_BLOCKED` markers). The row shows a warning badge when count > 0; review controls relocate to `DetailsIslandView`.
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, Avalonia, EF Core (one migration), xUnit.
|
||||||
|
|
||||||
|
**Coordination:** A second session (`claudedo-childloop`) is building the child-tasks/improvement-loop in a worktree and will rebase onto main *after* these commits. It also touches `DetailsIslandViewModel`, `TaskRowView.axaml`, `TaskStateService`, `TaskStatus`. This plan deliberately stays OUT of `TaskStateService` and the `TaskStatus` enum (persisting `RoadblockCount` from the runner via the repository instead).
|
||||||
|
|
||||||
|
Build/test (per-project, .NET 8):
|
||||||
|
```bash
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||||
|
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||||
|
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task A — Persist RoadblockCount (Data + Worker, no UI)
|
||||||
|
|
||||||
|
**Files:** `TaskEntity.cs`, `TaskEntityConfiguration.cs`, new migration, `TaskRepository.cs`, `TaskRunner.cs`; test in `tests/ClaudeDo.Data.Tests`.
|
||||||
|
|
||||||
|
- Add `public int RoadblockCount { get; set; }` to `TaskEntity` (default 0).
|
||||||
|
- Map it in `TaskEntityConfiguration` to column `roadblock_count` (default 0). Mirror the pattern used by an existing scalar column (e.g. how `DailyPrepMaxTasks`/other ints are configured).
|
||||||
|
- Create EF migration `AddRoadblockCount` (run `dotnet ef migrations add AddRoadblockCount` against `src/ClaudeDo.Data`; if EF tooling is unavailable, hand-author the migration + Designer + snapshot edit mirroring the most recent migration). One column, default 0, no backfill needed.
|
||||||
|
- Add `TaskRepository.SetRoadblockCountAsync(string taskId, int count, CancellationToken ct)` using `ExecuteUpdateAsync` on `RoadblockCount`.
|
||||||
|
- In `TaskRunner.HandleSuccess`, BEFORE the terminal state write (`SubmitForReviewAsync`/`CompleteAsync`), call `SetRoadblockCountAsync(task.Id, result.Blocks.Count, CancellationToken.None)` so the `TaskUpdated` broadcast reflects it. (Do NOT route this through `TaskStateService`.)
|
||||||
|
- Test: a `TaskRepository` test that sets a count and reads it back.
|
||||||
|
- Commit: `feat(roadblock): persist roadblock count on the task`.
|
||||||
|
|
||||||
|
**Acceptance:** a finished run with N roadblocks leaves `tasks.roadblock_count = N`; a clean run leaves 0.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task B — Detail panel: host review actions + real WaitingForReview state + diff meter
|
||||||
|
|
||||||
|
**Files:** `DetailsIslandViewModel.cs`, `DetailsIslandView.axaml` (+ `.axaml.cs` if needed), locales if new keys; reuse `IWorkerClient.ApproveReview/RejectReviewToQueue/RejectReviewToIdle/CancelReview` (already exist).
|
||||||
|
|
||||||
|
1. **WaitingForReview state:**
|
||||||
|
- In `StatusToStateKey` map `WaitingForReview => "review"` (was `"running"`); in `FinishedStatusToStateKey` map `"waiting_for_review" => "review"`.
|
||||||
|
- Add `public bool IsWaitingForReview => AgentState == "review";` and raise it in `OnAgentStateChanged`.
|
||||||
|
- Add a `vm.agentStatus.review` locale key (en + de, parity) for the status label.
|
||||||
|
- Confirm `IsAgentSectionEnabled => !IsRunning` still holds (review is no longer "running", so the agent settings section re-enables in review — correct).
|
||||||
|
2. **Review actions (moved from the row):** add commands to `DetailsIslandViewModel` that call the worker for the selected task: `ApproveReviewCommand`, `RejectReviewCommand` (takes feedback text → `RejectReviewToQueueAsync`), `ParkReviewCommand` (`RejectReviewToIdleAsync`), `CancelReviewCommand` (`CancelReviewAsync`). Add a `ReviewFeedback` string property for the rejection comment. Mirror how the row's code-behind currently invokes these (see `TaskRowView.axaml.cs`).
|
||||||
|
- In `DetailsIslandView.axaml`, add a review section (visible when `IsWaitingForReview` and `IsTaskDetailVisible`) with Approve / Reject(+feedback box) / Park / Cancel, reusing the existing `tasks.approve/reject/park/cancel` + `tasks.feedback*` locale keys.
|
||||||
|
3. **Diff meter:** in `RefreshWorktreeAsync`, after setting `row.DiffStat`, parse the `--stat` summary into additions/deletions and assign `DiffAdditions`/`DiffDeletions` (drives `DiffMeterRatio`). Add a small static parser `ParseDiffStat(string?) -> (int add, int del)` reading the "N insertions(+), M deletions(-)" tail; unit-test it.
|
||||||
|
- Commit: `feat(ui): host review actions in the details panel; show review state and diff meter`.
|
||||||
|
|
||||||
|
**Acceptance:** selecting a `WaitingForReview` task shows a "review" status (not "running"), the four review actions work from the detail panel, and the diff meter reflects real additions/deletions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task C — Task row: remove review buttons, add roadblock badge
|
||||||
|
|
||||||
|
**Files:** `TaskRowView.axaml`, `TaskRowView.axaml.cs`, `TaskRowViewModel.cs`; warning icon resource if missing.
|
||||||
|
|
||||||
|
- Remove the review-actions `StackPanel` (lines ~142–157) and the now-unused `RejectAnchor` flyout (~250–279) from `TaskRowView.axaml`, and the corresponding click handlers (`OnApproveReviewClick`, `OnRejectReviewClick`, `OnParkReviewClick`, `OnCancelReviewClick`, reject-flyout handlers) from the code-behind. (Review now lives in the detail panel — Task B.)
|
||||||
|
- `TaskRowViewModel`: add `int RoadblockCount` + `bool HasRoadblock => RoadblockCount > 0` + `string RoadblockTooltip` (e.g. `"{n} roadblock(s) reported — see details"`); map `RoadblockCount` in `FromEntity`.
|
||||||
|
- `TaskRowView.axaml`: add a yellow warning `PathIcon` immediately left of the action area (in the chip row, before the status chip or before the star — pick the spot that reads as "left of the Done/action button"), `IsVisible="{Binding HasRoadblock}"`, `ToolTip.Tip="{Binding RoadblockTooltip}"`. Use a filled-geometry warning icon (PathIcon fills geometry — a stroke path renders invisible); if no `Icon.Warning` resource exists, add one (filled triangle + exclamation) to the icon resources, colored with a yellow/amber brush.
|
||||||
|
- Commit: `feat(ui): roadblock badge on the task card; relocate review actions`.
|
||||||
|
|
||||||
|
**Acceptance:** rows no longer show the four review buttons; a task with `RoadblockCount > 0` shows a yellow ⚠ left of the action button with a tooltip; review still fully works via the detail panel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task D — Build + visual-check
|
||||||
|
|
||||||
|
- Full build (`App` + `Worker`) and run Data + Worker test suites; all green.
|
||||||
|
- **Manual (flag for user):** start the app, take a `WaitingForReview` task (the deploy roadblock task qualifies), confirm: row shows the ⚠ badge + no row review buttons; detail panel shows "review" state, working review actions, and a non-zero diff meter for the farewell/README tasks. The agent cannot verify GUI — ask the user.
|
||||||
|
- Then ping `claudedo-childloop` via mailbox with the exact shared-file diffs so it can rebase.
|
||||||
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
182
docs/superpowers/specs/2026-06-03-daily-prep-design.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Daily Prep ("Prime Claude") — Design
|
||||||
|
|
||||||
|
Date: 2026-06-03
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Turn the existing Prime Time warm-up into a **daily preparation** ("Tagesvorbereitung").
|
||||||
|
At a scheduled time (or on demand), Claude reads the open tasks, estimates effort,
|
||||||
|
and selects a focused subset into the MyDay list — capped so it never moves
|
||||||
|
everything in. Claude does the reasoning itself (agentic), via the already-registered
|
||||||
|
ClaudeDo MCP. This replaces the current `"ping"` behavior entirely.
|
||||||
|
|
||||||
|
A later phase will feed external tickets (Jira, possibly a second system) into the
|
||||||
|
same candidate pool; that is out of scope for this spec.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Scheduled and manual ("Tag vorbereiten" button) daily prep.
|
||||||
|
- Claude picks a subset of open tasks into MyDay, ordered so related tasks sit together.
|
||||||
|
- Effort-aware selection, hard-capped at `X` open MyDay tasks.
|
||||||
|
- Keep existing MyDay tasks across re-runs; only top up to `X`.
|
||||||
|
- Candidates limited to tasks in repos that are **not** excluded from the weekly report.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- External ticket integration (Jira etc.) — future phase.
|
||||||
|
- Group labels/headers in the MyDay view — grouping is ordering-only via `SortOrder`.
|
||||||
|
- A user-editable prep prompt — the prompt is fixed, parameterized.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Who reasons | Agentic — Claude decides via MCP tools. |
|
||||||
|
| MyDay model | `TaskEntity.IsMyDay` flag (smart list `smart:my-day`). |
|
||||||
|
| Grouping | Ordering only via existing `SortOrder` (no new field, no migration for grouping). |
|
||||||
|
| Selection | Effort estimate, hard cap `X` tasks/day. |
|
||||||
|
| Candidates | `Status == Idle`, `BlockedByTaskId == null`, list `WorkingDir` not under `ReportExcludedPaths`. |
|
||||||
|
| Re-run | Keep existing MyDay tasks; top up to `X`. |
|
||||||
|
| Trigger | Existing Prime schedule **and** a manual button. |
|
||||||
|
| Ping | Removed — daily prep replaces it. |
|
||||||
|
| Prompt | Fixed, with injected parameters (`X`, today's date). |
|
||||||
|
| Tool access | Reuse the globally registered `claudedo` MCP — **no** separate `--mcp-config`. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### 1. MCP tools (extend `ExternalMcpService`, port 47822)
|
||||||
|
|
||||||
|
The worker already exposes `ExternalMcpService` as the `claudedo` MCP server. Add two tools;
|
||||||
|
they automatically surface as `mcp__claudedo__get_daily_prep_candidates` and
|
||||||
|
`mcp__claudedo__set_my_day`.
|
||||||
|
|
||||||
|
- **`get_daily_prep_candidates()`** → JSON containing:
|
||||||
|
- `candidates[]`: open, non-blocked tasks in non-excluded repos, each with
|
||||||
|
`id, title, description, listName, isStarred, scheduledFor, age` (age derived from `CreatedAt`).
|
||||||
|
- `currentMyDay[]`: currently-`IsMyDay` open tasks (so Claude sees remaining capacity).
|
||||||
|
- Filter: `Status == Idle` AND `BlockedByTaskId == null` AND the task's list `WorkingDir`
|
||||||
|
does not start with any prefix in `AppSettings.ReportExcludedPaths`
|
||||||
|
(default `["C:\\Private"]`; case-insensitive prefix match, same semantics as the weekly report).
|
||||||
|
|
||||||
|
- **`set_my_day(taskId, isMyDay, sortOrder?)`** →
|
||||||
|
- Sets `IsMyDay` and (optionally) `SortOrder` on the task via `TaskRepository`.
|
||||||
|
- Broadcasts `TaskUpdated` via `HubBroadcaster` so the UI updates live.
|
||||||
|
- **Cap-guard:** when `isMyDay == true`, count current open (`Idle`) tasks with
|
||||||
|
`IsMyDay == true`. If `count >= X`, reject with an error message
|
||||||
|
("MyDay limit {X} reached"). `isMyDay == false` is always allowed.
|
||||||
|
`X = AppSettings.DailyPrepMaxTasks`. This guarantees the "never move everything in"
|
||||||
|
invariant server-side, independent of Claude's behavior.
|
||||||
|
|
||||||
|
### 2. `DailyPrepRunner` (replaces ping logic)
|
||||||
|
|
||||||
|
Rename `IPrimeRunner`/`PrimeRunner` → `IDailyPrepRunner`/`DailyPrepRunner` (the `"ping"`
|
||||||
|
concept is gone). It:
|
||||||
|
|
||||||
|
- Loads `AppSettings` (`X = DailyPrepMaxTasks`).
|
||||||
|
- Builds the fixed prompt with injected parameters (`X`, today's date).
|
||||||
|
- Invokes `claude -p --output-format stream-json --verbose` with:
|
||||||
|
- `--permission-mode` set so the headless run won't block on permission prompts,
|
||||||
|
- `--allowedTools mcp__claudedo__get_daily_prep_candidates mcp__claudedo__set_my_day`,
|
||||||
|
- `--max-turns 30` (constant), timeout 5 min (constant; larger than the old 60s ping).
|
||||||
|
- **No `--mcp-config`** — relies on the globally registered `claudedo` MCP (the worker runs
|
||||||
|
as the user via the per-user logon Scheduled Task, so the headless run inherits the
|
||||||
|
user-scope registration and its auth).
|
||||||
|
- Returns an outcome (e.g. number of tasks added) for broadcasting.
|
||||||
|
|
||||||
|
### 3. Scheduler
|
||||||
|
|
||||||
|
`PrimeScheduler` is unchanged in structure — it now calls `IDailyPrepRunner` instead of the
|
||||||
|
ping runner. `NextDueCalculator` and the schedule model are untouched.
|
||||||
|
|
||||||
|
### 4. Manual trigger
|
||||||
|
|
||||||
|
- Worker hub method `RunDailyPrepNow()` invokes the same `DailyPrepRunner`.
|
||||||
|
- UI button **"Tag vorbereiten"** in the MyDay list header.
|
||||||
|
- **Single-flight guard:** if a prep run is already in progress, the trigger reports
|
||||||
|
"already running" and does not start a parallel run (applies to both schedule and button).
|
||||||
|
|
||||||
|
### 5. Parameter config
|
||||||
|
|
||||||
|
- New field **`DailyPrepMaxTasks`** (int, default `5`) on `AppSettingsEntity`.
|
||||||
|
- Plumbing: EF config + migration, `AppSettingsRepository`, `WorkerHub` AppSettings DTO,
|
||||||
|
UI DTO mirror + `WorkerClient`, and a numeric editor in the Prime Claude settings tab.
|
||||||
|
- `ReportExcludedPaths` is reused as-is (already on `AppSettings`).
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. Trigger (schedule due **or** button) → `DailyPrepRunner.RunAsync`.
|
||||||
|
2. Runner loads `AppSettings` (`X`), builds prompt, launches Claude.
|
||||||
|
3. Claude → `get_daily_prep_candidates` → DB query returns filtered candidates + current MyDay.
|
||||||
|
4. Claude estimates effort, tops up to **X total**, calls `set_my_day(id, true, sortOrder)`
|
||||||
|
for each chosen task (consecutive `sortOrder` for related tasks).
|
||||||
|
5. `ExternalMcpService` writes `IsMyDay`/`SortOrder`, broadcasts `TaskUpdated` → MyDay list
|
||||||
|
updates live.
|
||||||
|
6. Runner updates `LastRunAt`, broadcasts "prep done" (count added).
|
||||||
|
|
||||||
|
## Fixed Prompt (parameterized)
|
||||||
|
|
||||||
|
Content (parameters in `{}`):
|
||||||
|
|
||||||
|
> Du bereitest meinen Arbeitstag für **{today}** vor.
|
||||||
|
> 1. Rufe `get_daily_prep_candidates` auf.
|
||||||
|
> 2. Behalte bereits als MyDay markierte offene Tasks.
|
||||||
|
> 3. Fülle bis **maximal {X} offene Tasks gesamt** in MyDay auf — niemals mehr.
|
||||||
|
> 4. Schätze pro Task grob den Aufwand; wähle eine machbare Mischung (nicht nur Großbrocken).
|
||||||
|
> Priorisiere `isStarred`, fällige (`scheduledFor`) und ältere Tasks.
|
||||||
|
> 5. Lege thematisch verwandte Tasks durch aufeinanderfolgende `sortOrder`-Werte nebeneinander.
|
||||||
|
> 6. Setze die Auswahl via `set_my_day(id, true, sortOrder)`. Markiere nichts außerhalb der
|
||||||
|
> Kandidatenliste.
|
||||||
|
|
||||||
|
Injected parameters: `{today}` (date) and `{X}` (= `DailyPrepMaxTasks`).
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- No candidates → Claude marks nothing; runner reports "0 added".
|
||||||
|
- Claude run fails / times out → log + failure broadcast (existing scheduler event channel);
|
||||||
|
`LastRunAt` is set on attempt, as today, to avoid tight retry loops.
|
||||||
|
- `set_my_day` on an invalid/ineligible id → tool returns an error string; Claude adapts.
|
||||||
|
- Cap exceeded → tool returns an error; Claude stops adding.
|
||||||
|
- Concurrent trigger → single-flight guard reports "already running".
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Real SQLite + real git (project convention).
|
||||||
|
|
||||||
|
- `get_daily_prep_candidates`: only `Idle`; blocked excluded; tasks in excluded repos
|
||||||
|
(`ReportExcludedPaths`) excluded; current MyDay tasks included.
|
||||||
|
- `set_my_day`: sets flag + `SortOrder`; broadcasts `TaskUpdated`; cap-guard rejects at limit;
|
||||||
|
unset always allowed.
|
||||||
|
- `DailyPrepRunner`: prompt contains `{X}` + date; args contain `--allowedTools` +
|
||||||
|
permission-mode + `--max-turns`; success/failure outcomes via an `IClaudeProcess` fake.
|
||||||
|
- Rename `IPrimeRunner` → `IDailyPrepRunner` requires syncing `PrimeScheduler` tests/fakes.
|
||||||
|
|
||||||
|
## Files to Create / Modify (high level)
|
||||||
|
|
||||||
|
**Data**
|
||||||
|
- `Models/AppSettingsEntity.cs` — add `DailyPrepMaxTasks`.
|
||||||
|
- `Configuration/AppSettingsEntityConfiguration.cs` — map new column.
|
||||||
|
- `Migrations/` — new migration for `daily_prep_max_tasks`.
|
||||||
|
- `Repositories/AppSettingsRepository.cs` — persist new field.
|
||||||
|
|
||||||
|
**Worker**
|
||||||
|
- `External/ExternalMcpService.cs` — add `get_daily_prep_candidates`, `set_my_day` (+ cap-guard).
|
||||||
|
- `Prime/PrimeRunner.cs` → `DailyPrepRunner.cs`; `Prime/Interfaces/IPrimeRunner.cs`
|
||||||
|
→ `IDailyPrepRunner.cs`; prompt builder + arg builder.
|
||||||
|
- `Prime/PrimeScheduler.cs` — depend on `IDailyPrepRunner`.
|
||||||
|
- `Hub/WorkerHub.cs` — AppSettings DTO field; `RunDailyPrepNow()`.
|
||||||
|
- `Program.cs` — DI registration update.
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
- `Services/WorkerClient.cs` + AppSettings DTO mirror — new field; `RunDailyPrepNow` call.
|
||||||
|
- Prime Claude settings tab VM/view — numeric editor for `DailyPrepMaxTasks`.
|
||||||
|
- MyDay list header — "Tag vorbereiten" button + command (Lists/IslandsShell VM).
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
- `ClaudeDo.Worker.Tests` — MCP tools, runner, scheduler fakes.
|
||||||
|
- `ClaudeDo.Data.Tests` — AppSettings persistence (if covered there).
|
||||||
|
- `ClaudeDo.Ui.Tests` — settings VM / button wiring as applicable.
|
||||||
|
|
||||||
|
## Future Phase (out of scope)
|
||||||
|
|
||||||
|
External ticket sources (Jira, possibly a second system) feed into the candidate pool used by
|
||||||
|
`get_daily_prep_candidates`, behind a task-source abstraction. Designed separately.
|
||||||
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
151
docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Daily Prep — Live Output View + Clear Day — Design
|
||||||
|
|
||||||
|
Date: 2026-06-03
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Two follow-ups to the daily-prep ("Prime Claude") feature:
|
||||||
|
|
||||||
|
1. **Live output view.** While Claude prepares the day, there is no feedback. Add a
|
||||||
|
live, human-readable view of the prep run's output, shown as a new content mode in
|
||||||
|
the existing right-hand **Details island** (mirroring how Daily Notes works — a mode
|
||||||
|
swap, not a separate window/column).
|
||||||
|
2. **Clear Day button.** A MyDay-header button that clears the MyDay selection
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- See the prep run's progress live, rendered with the same friendly terminal renderer
|
||||||
|
used for task runs (assistant text + tool calls like `set_my_day …`, not raw NDJSON).
|
||||||
|
- Both manual (button) and scheduled prep runs stream into the log.
|
||||||
|
- The manual button opens the prep view; a scheduled run fills the log silently and is
|
||||||
|
opened via a dedicated "Vorbereitungs-Log" button (the existing `PrimeStatus` footer
|
||||||
|
remains the hint that a run happened).
|
||||||
|
- A "Tag leeren" button clears all MyDay tasks (any status) with no confirmation.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No new island/column and no popup/overlay — reuse the Details island as a mode swap.
|
||||||
|
- No persistence of prep output across app restarts (in-memory log only).
|
||||||
|
- No undo for Clear Day (re-runnable via "Tag vorbereiten").
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Rendering | Reuse the existing `SessionTerminalView` / `StreamLineFormatter` renderer. |
|
||||||
|
| Location | New `IsPrepMode` content panel inside the Details island (like `IsNotesMode`). |
|
||||||
|
| Lifecycle | Manual click opens the view (UI-local); `PrepStarted/PrepLine/PrepFinished` events fill the log regardless of current mode; scheduled runs do not auto-open. |
|
||||||
|
| Open after schedule | Dedicated "Vorbereitungs-Log" header button + existing `PrimeStatus` footer hint. |
|
||||||
|
| Clear Day scope | All MyDay tasks regardless of status. |
|
||||||
|
| Clear Day confirm | None — clear directly. |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Feature A — Live prep output
|
||||||
|
|
||||||
|
**Worker**
|
||||||
|
- Extend `IPrimeBroadcaster` (`src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`)
|
||||||
|
with `PrepStartedAsync()`, `PrepLineAsync(string line)`, `PrepFinishedAsync(bool success)`.
|
||||||
|
- Implement in `HubBroadcaster` (`src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`) sending
|
||||||
|
SignalR events `PrepStarted`, `PrepLine` (string), `PrepFinished` (bool).
|
||||||
|
- `PrimeRunner` (`src/ClaudeDo.Worker/Prime/PrimeRunner.cs`): inject `IPrimeBroadcaster`.
|
||||||
|
In `FireAsync`, after the single-flight gate is entered and a run will actually happen:
|
||||||
|
call `PrepStartedAsync()` before `RunAsync`; replace the discard lambda with
|
||||||
|
`async line => await _broadcaster.PrepLineAsync(line)`; call
|
||||||
|
`PrepFinishedAsync(result.IsSuccess)` after. The "already running" early-return path
|
||||||
|
emits nothing (no run occurs). Both scheduled and manual runs go through `FireAsync`,
|
||||||
|
so both stream.
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
- `WorkerClient` (`src/ClaudeDo.Ui/Services/WorkerClient.cs`): register
|
||||||
|
`_hub.On<…>("PrepStarted"/"PrepLine"/"PrepFinished", …)` each via
|
||||||
|
`Dispatcher.UIThread.Post`, raising `PrepStartedEvent` / `PrepLineEvent(string)` /
|
||||||
|
`PrepFinishedEvent(bool)`. Declare these on `IWorkerClient`.
|
||||||
|
- `DetailsIslandViewModel`: add `IsPrepMode` (bool), `IsPrepRunning` (bool), a dedicated
|
||||||
|
`PrepLog` (`ObservableCollection<LogLineViewModel>`), and `ShowPrep()` (calls
|
||||||
|
`Bind(null)`, sets `IsNotesMode=false`, `IsPrepMode=true`). Subscribe to the three prep
|
||||||
|
events in the ctor (always active, independent of mode):
|
||||||
|
- `PrepStarted` → clear `PrepLog`, `IsPrepRunning=true`.
|
||||||
|
- `PrepLine` → format the line with the same `StreamLineFormatter` path used by the
|
||||||
|
stdout branch of `OnTaskMessage`, append a `LogLineViewModel` to `PrepLog`.
|
||||||
|
- `PrepFinished` → `IsPrepRunning=false` (optionally append a status line).
|
||||||
|
Mode exclusivity: the normal task-details panel becomes visible on
|
||||||
|
`!IsNotesMode && !IsPrepMode`; `ShowNotes()` also sets `IsPrepMode=false`; `Bind(task)`
|
||||||
|
resets both flags.
|
||||||
|
- `DetailsIslandView.axaml`: add a third `<Panel IsVisible="{Binding IsPrepMode}">` in the
|
||||||
|
body grid alongside the existing details/notes panels, rendering `PrepLog` in the
|
||||||
|
terminal style (reuse the `LogLineViewModel` item template used by `SessionTerminalView`).
|
||||||
|
|
||||||
|
**Wiring**
|
||||||
|
- `TasksIslandViewModel`: add a `PrepRequested` event (mirror `NotesRequested`).
|
||||||
|
`PrepareDayCommand` raises `PrepRequested` in addition to calling
|
||||||
|
`RunDailyPrepNowAsync()`. Add `ShowPrepLogCommand` that raises `PrepRequested`. Add the
|
||||||
|
"Vorbereitungs-Log" button to the MyDay header (`IsVisible="{Binding IsMyDayList}"`).
|
||||||
|
- `IslandsShellViewModel`: wire `Tasks.PrepRequested += () => Details.ShowPrep()`.
|
||||||
|
|
||||||
|
### Feature B — Clear Day
|
||||||
|
|
||||||
|
**Worker**
|
||||||
|
- `WorkerHub.ClearMyDay()` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs`): query ids where
|
||||||
|
`IsMyDay == true`; `ExecuteUpdateAsync` setting `is_my_day = false`; broadcast
|
||||||
|
`TaskUpdated(id)` for each affected id (the UI reloads the current list on `TaskUpdated`).
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
- `IWorkerClient.ClearMyDayAsync()` + `WorkerClient` impl invoking `"ClearMyDay"`.
|
||||||
|
- `TasksIslandViewModel.ClearDayCommand` calls `_worker.ClearMyDayAsync()` (no confirm).
|
||||||
|
Add the "Tag leeren" button to the MyDay header next to "Tag vorbereiten".
|
||||||
|
|
||||||
|
## Data Flow (live view)
|
||||||
|
|
||||||
|
1. Trigger (schedule or button) → `PrimeRunner.FireAsync`.
|
||||||
|
2. `PrepStartedAsync()` → SignalR `PrepStarted` → `WorkerClient.PrepStartedEvent` →
|
||||||
|
`DetailsIslandViewModel` clears `PrepLog`, sets `IsPrepRunning`.
|
||||||
|
3. Each Claude stdout line → `PrepLineAsync(line)` → `PrepLine` → formatted, appended to
|
||||||
|
`PrepLog` (visible if the user is in prep mode; filled silently otherwise).
|
||||||
|
4. Run ends → `PrepFinishedAsync(success)` → `PrepFinished` → `IsPrepRunning=false`.
|
||||||
|
5. Manual button click also raised `PrepRequested` → `Details.ShowPrep()` (view open).
|
||||||
|
After a scheduled run, the user clicks "Vorbereitungs-Log" to open it.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Prep run fails/times out → `PrepFinished(false)`; the existing `PrimeFired` footer
|
||||||
|
status still reports failure.
|
||||||
|
- "Already running" → no prep events emitted (no run happened); existing behavior intact.
|
||||||
|
- `ClearMyDay` with zero MyDay tasks → no-op, no broadcasts.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Worker: `PrimeRunner` streams `PrepStarted` → N×`PrepLine` → `PrepFinished` (fake
|
||||||
|
`IClaudeProcess` invokes `onStdoutLine` with sample lines; fake `IPrimeBroadcaster`
|
||||||
|
records calls). `WorkerHub.ClearMyDay` clears all IsMyDay rows and broadcasts per id
|
||||||
|
(real SQLite, mirror existing hub tests).
|
||||||
|
- UI: `DetailsIslandViewModel` appends to `PrepLog` on `PrepLineEvent` and `ShowPrep()`
|
||||||
|
sets the mode flags (mutual exclusivity with notes); `TasksIslandViewModel.ClearDayCommand`
|
||||||
|
calls `ClearMyDayAsync` (stub worker client).
|
||||||
|
|
||||||
|
## Files (high level)
|
||||||
|
|
||||||
|
**Modify**
|
||||||
|
- `src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Prime/PrimeRunner.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` (ClearMyDay)
|
||||||
|
- `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml`
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
|
||||||
|
- `src/ClaudeDo.Localization/locales/en.json`, `de.json` (button labels)
|
||||||
|
|
||||||
|
**Test**
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs`
|
||||||
|
- `tests/ClaudeDo.Worker.Tests/Hub/…` (ClearMyDay)
|
||||||
|
- `tests/ClaudeDo.Ui.Tests/…` (DetailsIslandViewModel prep events; TasksIslandViewModel ClearDay) + `StubWorkerClient`
|
||||||
|
|
||||||
|
## Known fragility
|
||||||
|
|
||||||
|
Changing `IWorkerClient` / `WorkerClient` / VM constructors breaks hand-rolled fakes
|
||||||
|
(`StubWorkerClient`, `FakeWorkerClient`) in both test projects — update all of them.
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Bundled Prompts Overhaul — Design
|
||||||
|
|
||||||
|
Date: 2026-06-04
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace ClaudeDo's bundled prompts with a clean, professional baseline and make
|
||||||
|
every prose prompt a user-editable file with a bundled default. Add a roadblock
|
||||||
|
protocol so an autonomous run can flag problems mid-task without aborting.
|
||||||
|
|
||||||
|
The execution-side defaults (`system.md`) ship as a moderate, **project-agnostic**
|
||||||
|
engineering baseline — ClaudeDo users run tasks against their *own* repos, so no
|
||||||
|
ClaudeDo-specific rules belong there. Everything is in English (tighter
|
||||||
|
tokenization, more reliable instruction-following); the only German output is the
|
||||||
|
weekly report, which a human reads.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
All prompts live under `~/.todo-app/prompts/` as editable files with bundled
|
||||||
|
defaults seeded by `PromptFiles.EnsureExists` (which never overwrites a file the
|
||||||
|
user already has). The `system` + `agent` prompts collapse into one `system.md`;
|
||||||
|
the old `agent`/manual distinction was removed when tags were retired.
|
||||||
|
|
||||||
|
| File | Replaces | Placeholders |
|
||||||
|
|---|---|---|
|
||||||
|
| `system.md` | system + agent (merged) | — |
|
||||||
|
| `planning-system.md` | planning system prompt | — |
|
||||||
|
| `planning-initial.md` | "analyze & break down" kickoff | `{title}`, `{description}` |
|
||||||
|
| `retry.md` | "try again and fix" prompt | — |
|
||||||
|
| `daily-prep.md` | daily-prep prompt | `{date}`, `{maxTasks}` |
|
||||||
|
| `weekly-report.md` | weekly-report instructions | `{start}`, `{end}` |
|
||||||
|
|
||||||
|
The task-execution prompt (title + description + `## Sub-Tasks` checkboxes) stays
|
||||||
|
assembled in code — it is data-shaped, not prose.
|
||||||
|
|
||||||
|
### Templating
|
||||||
|
|
||||||
|
`PromptFiles` gains `Render(PromptKind kind, IReadOnlyDictionary<string,string> values)`
|
||||||
|
that replaces **only** the known named tokens for that kind. Any other `{...}` in
|
||||||
|
the file (e.g. the literal `{Wochentag}` / `{dd.MM.yyyy}` in the German report
|
||||||
|
rules) passes through untouched. Daily-prep tool names are inlined as literals —
|
||||||
|
`--allowedTools` already carries the real names, and inlining keeps the file from
|
||||||
|
silently breaking if a user edits a placeholder.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
`EnsureExists` keeps its current semantics: it seeds a default only when the file
|
||||||
|
is missing, never overwriting user edits. The old `planning.md` and `agent.md`
|
||||||
|
become inert — `TaskRunner` stops reading `agent.md`, and the planning system
|
||||||
|
prompt now reads `planning-system.md`. Old files are harmless to leave or delete.
|
||||||
|
`PromptKind` changes: `Agent` is removed; `Planning` maps to `planning-system.md`;
|
||||||
|
new kinds `PlanningInitial`, `Retry`, `DailyPrep`, `WeeklyReport` are added.
|
||||||
|
|
||||||
|
## Roadblock protocol
|
||||||
|
|
||||||
|
An autonomous run has no human watching, so it must not silently stop or block on
|
||||||
|
a question. Instead the agent emits an inline marker whenever it hits a true
|
||||||
|
blocker, **any number of times**, and keeps working on whatever it still can.
|
||||||
|
|
||||||
|
- **Prompt side** (`system.md`): instruct the agent to write
|
||||||
|
`CLAUDEDO_BLOCKED: <one short sentence>` on its own line whenever something
|
||||||
|
genuinely prevents progress (missing credentials, contradictory requirements, a
|
||||||
|
destructive action it won't take unasked) — then continue with the rest of the
|
||||||
|
task. Reserved for true blockers, not routine decisions it can make itself.
|
||||||
|
- **Detection** (`StreamAnalyzer`): as `assistant` messages stream, scan their
|
||||||
|
text content for lines matching `^CLAUDEDO_BLOCKED:` and collect each reason
|
||||||
|
into an ordered list (`Blocks`). This is live and cumulative — multiple problems
|
||||||
|
across one run are all captured, not just the last.
|
||||||
|
- **Result wiring** (`StreamResult` → `RunResult` → run record): carry the
|
||||||
|
collected `Blocks`. Strip the marker lines from the displayed result text.
|
||||||
|
- **Routing**: a run that finishes with blocks still goes to `WaitingForReview`
|
||||||
|
(standalone tasks) — it is "done as far as the agent could get". The review card
|
||||||
|
shows a ⚠ roadblock hint listing the collected problems. The user answers them
|
||||||
|
via the existing reject-rerun feedback path, which resumes the session with the
|
||||||
|
answers as the next-turn prompt — so the agent continues with the problems
|
||||||
|
resolved rather than restarting.
|
||||||
|
|
||||||
|
## The prompts
|
||||||
|
|
||||||
|
### `system.md`
|
||||||
|
```markdown
|
||||||
|
# Working Agreement
|
||||||
|
|
||||||
|
You are completing one well-defined task autonomously in a git repository.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Do exactly what the task asks — no unrequested refactors, renames, dependency
|
||||||
|
changes, or "while I'm here" cleanup.
|
||||||
|
- If intent is ambiguous, state the assumption you're making and proceed with the
|
||||||
|
most reasonable reading. Stop only if you genuinely cannot move forward.
|
||||||
|
- Prefer three similar lines over a premature abstraction. Don't build for
|
||||||
|
hypothetical future needs.
|
||||||
|
|
||||||
|
## Working in the repo
|
||||||
|
- Read a file before editing it. Match the conventions already in this codebase —
|
||||||
|
they override generic defaults.
|
||||||
|
- Prefer editing existing files to creating new ones. Don't write comments that
|
||||||
|
just restate the code.
|
||||||
|
- Validate only at real boundaries (user input, external APIs).
|
||||||
|
|
||||||
|
## Finishing
|
||||||
|
- Before claiming done, verify: run the build and relevant tests, confirm they
|
||||||
|
pass, and report what you ran. If you couldn't verify something, say so plainly.
|
||||||
|
- Make focused commits using the repository's existing commit-message convention.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
- Never force-push, hard-reset, or delete branches/files beyond the task's scope
|
||||||
|
without being asked.
|
||||||
|
- Don't introduce injection/XSS/secret-leak issues. Never commit credentials.
|
||||||
|
|
||||||
|
## You are running unattended
|
||||||
|
You run autonomously with no human watching. There is no one to answer mid-task
|
||||||
|
questions, so never stop to ask — make the most reasonable decision, note the
|
||||||
|
assumption, and continue.
|
||||||
|
|
||||||
|
## When you are blocked
|
||||||
|
If something genuinely prevents you from completing part of the task (missing
|
||||||
|
credentials, contradictory requirements, a destructive action you won't take
|
||||||
|
unasked), do NOT silently give up. Write this marker on its own line, then keep
|
||||||
|
working on whatever else you can:
|
||||||
|
|
||||||
|
CLAUDEDO_BLOCKED: <one short sentence describing what blocked you>
|
||||||
|
|
||||||
|
Emit it as many times as needed — once per distinct blocker. Use it only for true
|
||||||
|
blockers, not for routine decisions you can make yourself.
|
||||||
|
```
|
||||||
|
|
||||||
|
> `system.md` also gains an **"Out-of-scope improvements"** section that tells the
|
||||||
|
> agent to file follow-up work via the `SuggestImprovement` tool. That section is
|
||||||
|
> defined in `2026-06-04-child-tasks-and-improvement-loop-design.md` and lands with
|
||||||
|
> that feature.
|
||||||
|
|
||||||
|
### `planning-system.md`
|
||||||
|
```markdown
|
||||||
|
You are the planning assistant for ClaudeDo. Your job is to break a task into
|
||||||
|
smaller, independently executable subtasks — the session ends by creating those
|
||||||
|
subtasks.
|
||||||
|
|
||||||
|
Start every session by invoking the `superpowers:brainstorming` skill (Skill
|
||||||
|
tool) and follow it end to end: clarifying questions one at a time, then 2–3
|
||||||
|
approaches with a recommendation, then a short design. Do not create any subtasks
|
||||||
|
until the user has approved the design.
|
||||||
|
|
||||||
|
You can ONLY shape this task's plan — you cannot edit files or touch other tasks.
|
||||||
|
The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask,
|
||||||
|
DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else.
|
||||||
|
|
||||||
|
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||||
|
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||||
|
done-state, ordered so dependencies come first.
|
||||||
|
```
|
||||||
|
|
||||||
|
### `planning-initial.md`
|
||||||
|
```markdown
|
||||||
|
# Task to plan: {title}
|
||||||
|
|
||||||
|
{description}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `retry.md`
|
||||||
|
```markdown
|
||||||
|
The task did not complete on the previous attempt — you may have run out of
|
||||||
|
turns, hit an error, or stopped before finishing.
|
||||||
|
|
||||||
|
Review the work already done in this session and the current state of the
|
||||||
|
repository, identify what is still incomplete or broken, and finish the task.
|
||||||
|
Don't restart from scratch or repeat a failed approach. Verify the result
|
||||||
|
(build + tests) before you stop.
|
||||||
|
```
|
||||||
|
Self-contained — no error injection. The runner appends the captured process
|
||||||
|
output **only when it is a genuine error** (i.e. not the generic
|
||||||
|
`"Claude exited with code N and no result."` fallback), since real session errors
|
||||||
|
are already in the resumed context.
|
||||||
|
|
||||||
|
### `daily-prep.md`
|
||||||
|
```markdown
|
||||||
|
You are preparing my workday for {date}.
|
||||||
|
|
||||||
|
1. Call mcp__claudedo__get_daily_prep_candidates.
|
||||||
|
2. Keep tasks already marked MyDay (currentMyDay) — never remove them.
|
||||||
|
3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it.
|
||||||
|
4. Estimate each candidate's effort and pick a feasible mix — not only big items.
|
||||||
|
Prioritize isStarred, due (scheduledFor), and older tasks.
|
||||||
|
5. Place related tasks next to each other using consecutive sortOrder values.
|
||||||
|
6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything
|
||||||
|
outside the candidate list.
|
||||||
|
|
||||||
|
If there are no candidates, do nothing.
|
||||||
|
```
|
||||||
|
|
||||||
|
### `weekly-report.md`
|
||||||
|
```markdown
|
||||||
|
You are generating a concise weekly standup report for a software developer,
|
||||||
|
covering {start} to {end}.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Write the ENTIRE report in German.
|
||||||
|
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||||||
|
activity (German weekday names). Omit days with no activity.
|
||||||
|
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||||||
|
"- Y behoben"). Merge related small work into one bullet.
|
||||||
|
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||||||
|
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||||||
|
bullet list per day. The notes are authoritative — never omit or contradict them.
|
||||||
|
- Name the project/repo when it adds clarity.
|
||||||
|
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||||||
|
|
||||||
|
Two sections follow below: an activity log derived from Claude session history,
|
||||||
|
and the developer's own notes. Base the report on both; the notes are
|
||||||
|
authoritative where they conflict with the derived activity.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Touch points
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Data/PromptFiles.cs` — new `PromptKind` members, new defaults,
|
||||||
|
`Render` helper.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — stop reading `agent.md`; use
|
||||||
|
`retry.md`; conditional stderr append on retry; carry/route `Blocks`.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs` — scan assistant text for
|
||||||
|
`CLAUDEDO_BLOCKED:` markers, collect `Blocks`, strip from result.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/ClaudeProcess.cs` / `RunResult` — carry `Blocks`.
|
||||||
|
- `src/ClaudeDo.Worker/Planning/PlanningSessionManager.cs` — read
|
||||||
|
`planning-system.md` and `planning-initial.md` via `PromptFiles.Render`.
|
||||||
|
- `src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs` — read `daily-prep.md`.
|
||||||
|
- `src/ClaudeDo.Worker/Report/WeekReportPromptBuilder.cs` — read `weekly-report.md`.
|
||||||
|
- UI — review card shows the ⚠ roadblock hint with collected problems.
|
||||||
|
- `src/ClaudeDo.Ui/.../FilesSettingsTabViewModel.cs` — expose the new prompt files.
|
||||||
|
- Tests — `PromptFiles` render/seed; `StreamAnalyzer` marker collection; planning/
|
||||||
|
prep/report builders read from files.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- The in-code task-execution assembly (title/description/subtasks) is unchanged.
|
||||||
|
- `ResultSchema` / `--output-schema` remains untouched.
|
||||||
|
- No change to commit-message templating.
|
||||||
|
```
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Reusable Child Tasks + Agent Improvement Loop — Design
|
||||||
|
|
||||||
|
Date: 2026-06-04
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Let an executing task agent offload out-of-scope improvements it spots into
|
||||||
|
**child tasks** that run automatically, so ClaudeDo can drive a self-improvement
|
||||||
|
loop. Generalize the parent/child machinery that planning uses today into a
|
||||||
|
reusable subsystem not bound to planning.
|
||||||
|
|
||||||
|
Example: while implementing task X, Claude notices "this module should really be
|
||||||
|
refactored, but that's out of scope" — instead of scope-creeping, it calls a tool
|
||||||
|
that files the refactor as a child of X. The child runs on its own; once all of
|
||||||
|
X's children finish, X surfaces for review with its whole tree visible.
|
||||||
|
|
||||||
|
This builds on the bundled-prompts overhaul (`system.md` gains one instruction to
|
||||||
|
use the offload tool). It is otherwise independent.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
A new task status `WaitingForChildren` is added.
|
||||||
|
|
||||||
|
```
|
||||||
|
Running → WaitingForReview standalone success, no children (existing)
|
||||||
|
Running → WaitingForChildren standalone success, ≥1 child (new)
|
||||||
|
Running → Done planning child success (existing)
|
||||||
|
WaitingForChildren → WaitingForReview all children terminal (new)
|
||||||
|
WaitingForChildren → Cancelled cancel (new)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Improvement-children are created `Idle` **during** the parent's run and stay
|
||||||
|
unqueued until the parent's own run finishes — this avoids the parent and a
|
||||||
|
child working the same repo concurrently.
|
||||||
|
- When the parent's run succeeds and it has ≥1 non-terminal child, the parent goes
|
||||||
|
to `WaitingForChildren` and its children are enqueued (they then run under the
|
||||||
|
normal queue, governed by max-parallel — they are independent, not a forced
|
||||||
|
sequential chain like planning).
|
||||||
|
- Children run automatically and reach `Done` on success without their own review
|
||||||
|
gate (a per-child review would stall the loop). Each child still produces its
|
||||||
|
own worktree/commit; those worktrees are surfaced under the parent for merge.
|
||||||
|
- Children emit `CLAUDEDO_BLOCKED:` markers like any run (see the prompt-overhaul
|
||||||
|
spec). Each child's collected problems roll up onto the **parent's** review card,
|
||||||
|
so a parent in `WaitingForReview` shows "child N reported a problem" alongside
|
||||||
|
its own roadblocks.
|
||||||
|
|
||||||
|
## Worktree topology & merge
|
||||||
|
|
||||||
|
The correctness rule that makes this work:
|
||||||
|
|
||||||
|
- **Children base off the parent's worktree HEAD, not the list's base branch.**
|
||||||
|
The parent's code work lives only on `claudedo/{parentId}` until merged, so a
|
||||||
|
child refactoring code the parent just wrote must branch from the parent's HEAD
|
||||||
|
to see it. (Planning children base off the target branch because a planning
|
||||||
|
parent writes no code — improvement parents do, hence the difference.) The
|
||||||
|
per-run worktree setup takes the base commit from the parent task's recorded
|
||||||
|
worktree HEAD when `ParentTaskId` is set and the parent is a non-planning task.
|
||||||
|
- **Fan-out:** all children branch off the same parent HEAD and run independently
|
||||||
|
(parallel allowed). Parent-dependency is always satisfied; sibling overlaps
|
||||||
|
surface later as merge conflicts.
|
||||||
|
- **Merge reuses the planning orchestrator,** generalized into a shared
|
||||||
|
"tree merge": build an integration branch off the target, then sequentially
|
||||||
|
`merge --no-ff` the **parent's own branch** followed by each child branch,
|
||||||
|
pausing on conflict (continue / abort), exactly as `PlanningMergeOrchestrator`
|
||||||
|
/`PlanningAggregator` do today. Approving the parent triggers this one guided
|
||||||
|
flow, merging parent + all children in as few steps as possible. Because
|
||||||
|
children descend from the parent HEAD, the parent's commits are shared ancestors
|
||||||
|
and merge cleanly ahead of the children.
|
||||||
|
- The parent advances to `WaitingForReview` once **all** children are terminal —
|
||||||
|
counting `Done`, `Failed`, and `Cancelled`, so a failed child can't wedge the
|
||||||
|
parent forever. Failed/cancelled children are flagged on the review card.
|
||||||
|
|
||||||
|
Planning parents keep their existing behavior (parent → `Done` when its chain
|
||||||
|
finishes); they do not use `WaitingForChildren`.
|
||||||
|
|
||||||
|
## Consolidating the child subsystem
|
||||||
|
|
||||||
|
Today child handling is planning-coupled. Generalize:
|
||||||
|
|
||||||
|
- **`TaskRepository.CreateChildAsync`** — drop the `parent.PlanningPhase != None`
|
||||||
|
guard. A child can attach to any existing parent. (Planning callers are
|
||||||
|
unaffected; their parents have a planning phase.) The child sets
|
||||||
|
`ParentTaskId = parentId`; the caller decides `CreatedBy`.
|
||||||
|
- **Child-completion coordinator** — generalize planning's
|
||||||
|
`OnChildFinishedAsync` / `TryCompleteParentAsync` into a single component that,
|
||||||
|
on any child reaching a terminal state, checks the parent and applies a
|
||||||
|
**completion policy**:
|
||||||
|
- *planning parent* → finalize/Done (existing chain advancement stays in the
|
||||||
|
planning layer: unblock the next chained child).
|
||||||
|
- *improvement parent* (in `WaitingForChildren`, all children terminal) →
|
||||||
|
`WaitingForReview`.
|
||||||
|
- `TaskStateService` remains the sole writer of `Status` and owns the new
|
||||||
|
transitions (`SubmitForChildrenAsync`, the `WaitingForChildren → WaitingForReview`
|
||||||
|
advance).
|
||||||
|
|
||||||
|
## The offload tool
|
||||||
|
|
||||||
|
A narrow MCP tool exposed only to task runs (not the general external surface):
|
||||||
|
|
||||||
|
```
|
||||||
|
SuggestImprovement(title, description) → { childTaskId }
|
||||||
|
```
|
||||||
|
|
||||||
|
- The **server** stamps everything — the agent cannot choose the parent, the
|
||||||
|
status, or queue anything directly:
|
||||||
|
- `ParentTaskId = <calling task id>`
|
||||||
|
- `CreatedBy = <calling task id>` (unambiguous "agent-suggested improvement"
|
||||||
|
marker — distinct from `null` user/planning tasks and `"mcp"` external tasks)
|
||||||
|
- `Status = Idle`, same `ListId` as the parent.
|
||||||
|
- **One layer deep:** the tool rejects the call if the calling task already has a
|
||||||
|
`ParentTaskId` (a child cannot spawn children).
|
||||||
|
|
||||||
|
### Knowing the caller's identity
|
||||||
|
|
||||||
|
The always-on external `claudedo` MCP is shared and can't tell which task is
|
||||||
|
calling. So task runs get a **per-run MCP identity**, mirroring planning's
|
||||||
|
per-session token:
|
||||||
|
|
||||||
|
- `TaskRunner` mints a per-run token and writes a run-scoped `.mcp.json` (or
|
||||||
|
reuses the global server with a token header) so the offload tool resolves
|
||||||
|
token → calling task id server-side. A `TaskRunMcpContextAccessor` exposes the
|
||||||
|
current task id to the tool, the same way `PlanningMcpContextAccessor` does.
|
||||||
|
- This is the reliable path for both correct provenance and the one-layer-deep
|
||||||
|
guard — the id is never supplied by the model.
|
||||||
|
|
||||||
|
`system.md` gains a short instruction (from the prompt-overhaul spec):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Out-of-scope improvements
|
||||||
|
If you notice worthwhile work that is genuinely outside this task's scope
|
||||||
|
(a refactor, a follow-up, tech debt), do NOT do it here. File it with
|
||||||
|
SuggestImprovement(title, description) and stay focused on the task at hand.
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
- **Collapsible tree:** children group under their parent (by `ParentTaskId`).
|
||||||
|
Improvement-children are visually marked as agent-suggested (via
|
||||||
|
`CreatedBy == parentId`).
|
||||||
|
- **New status chip** for `WaitingForChildren` (e.g. amber "waiting on N
|
||||||
|
improvements") with its own color in `StatusColorConverter`.
|
||||||
|
- **Review card** for a parent in `WaitingForReview` lists child outcomes
|
||||||
|
(done/failed) and their rolled-up `CLAUDEDO_BLOCKED` problems, and drives the
|
||||||
|
shared tree-merge (parent + children) via the planning-style sequential flow
|
||||||
|
with conflict pause/continue/abort.
|
||||||
|
|
||||||
|
## Data / migration
|
||||||
|
|
||||||
|
- Add `WaitingForChildren` to the `TaskStatus` enum and its EF `ValueConverter`.
|
||||||
|
No new columns — `ParentTaskId` and `CreatedBy` already exist. No backfill
|
||||||
|
needed (no existing rows use the new value).
|
||||||
|
|
||||||
|
## Touch points
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Data/Models/TaskStatus` (enum) + `TaskEntityConfiguration` — new value.
|
||||||
|
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — generalize `CreateChildAsync`.
|
||||||
|
- `src/ClaudeDo.Worker/State/TaskStateService.cs` — `WaitingForChildren` transitions.
|
||||||
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — route to `WaitingForChildren` when
|
||||||
|
children exist; enqueue children on parent finish; mint per-run MCP token.
|
||||||
|
- New: child-completion coordinator (generalized from planning) + the offload tool
|
||||||
|
(e.g. `TaskRunMcpService.SuggestImprovement`) + `TaskRunMcpContextAccessor` +
|
||||||
|
token auth (mirrors `PlanningTokenAuth`).
|
||||||
|
- `src/ClaudeDo.Worker/Planning/*` — refactor planning to consume the shared
|
||||||
|
child-completion coordinator and the shared tree-merge; keep chain-specific
|
||||||
|
advancement local. Generalize `PlanningMergeOrchestrator` / `PlanningAggregator`
|
||||||
|
into a reusable tree-merge that also folds in the parent's own branch.
|
||||||
|
- Worktree setup (`TaskRunner` / `WorktreeManager`) — base an improvement-child's
|
||||||
|
worktree on the parent task's recorded worktree HEAD instead of the list base.
|
||||||
|
- UI — tree grouping, `WaitingForChildren` chip/color, parent review card with
|
||||||
|
child outcomes + rolled-up roadblocks + the merge flow.
|
||||||
|
- Tests — offload tool stamps parent/createdBy + rejects nested calls;
|
||||||
|
parent → `WaitingForChildren` → `WaitingForReview` lifecycle; child worktree
|
||||||
|
bases off parent HEAD; tree-merge folds parent + children; planning regression
|
||||||
|
(still reaches Done).
|
||||||
|
|
||||||
|
## Open questions for review
|
||||||
|
|
||||||
|
1. **Failed child:** parent still advances to `WaitingForReview` with the failure
|
||||||
|
flagged (default), vs. parent → `Failed` if any child failed.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Multi-level nesting (only one layer deep by design).
|
||||||
|
- Per-list "disable improvement offload" toggle (could come later; the tool is
|
||||||
|
always available to top-level runs for now).
|
||||||
|
- Changes to how planning sets up its sequential chain.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Debug Logging & Frontend↔Backend Traceability — Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-04
|
||||||
|
**Status:** Approved (pending spec review)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make debug logging rich enough to diagnose problems across the UI↔Worker boundary, while keeping the installed (production) build near-silent. Verbosity is decided by **build configuration, detected at runtime** — no runtime knob, no config field, no `#if DEBUG`:
|
||||||
|
|
||||||
|
- **Debug build** (Rider run button) → verbose, console + file.
|
||||||
|
- **Release build** (installed app) → minimal, file only.
|
||||||
|
|
||||||
|
## Decisions (from brainstorming)
|
||||||
|
|
||||||
|
1. **Mechanism:** runtime build-config detection via the entry assembly's `DebuggableAttribute` (JIT optimizer disabled ⇒ Debug build). A single `BuildConfig.IsDebug` helper drives ordinary `if` branching — no `#if DEBUG` directives. Rider's run button builds `Debug`; the installer ships `-c Release`.
|
||||||
|
2. **Scope:** Worker **and** App/Ui. The desktop side currently has no log sink at all — UI/IPC failures vanish today.
|
||||||
|
3. **Release behavior:** all three log `Warning`+ to file (not silent — capture crashes). Worker drops from its current `Information` to `Warning`.
|
||||||
|
4. **One shared log file** across both processes, unified timeline.
|
||||||
|
5. **Correlation:** TaskId-based (option A). Enrich log lines with `TaskId` when one is in scope. No changes to the SignalR contract (`IWorkerClient`/`WorkerHub` untouched → test fakes untouched).
|
||||||
|
|
||||||
|
## Verbosity matrix
|
||||||
|
|
||||||
|
| Process | Debug build | Release build |
|
||||||
|
|---|---|---|
|
||||||
|
| Worker | `Debug` level, console + shared file | `Warning` level, shared file |
|
||||||
|
| App/Ui | `Debug` level, console + shared file | `Warning` level, shared file |
|
||||||
|
|
||||||
|
## Shared log file
|
||||||
|
|
||||||
|
- Single daily-rolling file: `~/.todo-app/logs/claudedo-.log` (Serilog appends the date).
|
||||||
|
- `shared: true` on both processes' file sinks → Serilog coordinates multi-process writes via a global mutex.
|
||||||
|
- `retainedFileCountLimit: 2`.
|
||||||
|
- Each line is tagged with a `Process` property (`"worker"` / `"app"`) so the two sides are distinguishable in the interleaved timeline.
|
||||||
|
|
||||||
|
> The existing `worker-.log` is replaced by `claudedo-.log`. Task-run NDJSON (`{taskId}_run{n}.ndjson`) and `daily-prep.log` are **out of scope** — they are data streams, not diagnostic logs, and stay exactly as they are.
|
||||||
|
|
||||||
|
## Output template
|
||||||
|
|
||||||
|
```
|
||||||
|
[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `{Process}` — `worker` or `app`.
|
||||||
|
- `{SourceContext}` — the `ILogger<T>` category (the logging class), so you see *which* component spoke.
|
||||||
|
- `{TaskId}` — the correlation key, defaulted to `-` when no task is in scope (see enricher below).
|
||||||
|
|
||||||
|
## Traceability (TaskId correlation)
|
||||||
|
|
||||||
|
Use Serilog's `LogContext` (`.Enrich.FromLogContext()` on both processes) plus a small default enricher so `TaskId` is always present (renders `-` when absent — avoids the raw `{TaskId}` token leaking into output).
|
||||||
|
|
||||||
|
Push the property at the entry points where a task is in scope; all nested `ILogger<T>` calls inherit it automatically:
|
||||||
|
|
||||||
|
- **Worker:** wrap per-task execution in `TaskRunner` (the run/continue entry) with `using (LogContext.PushProperty("TaskId", task.Id))`. This covers the bulk of backend activity (runner, state transitions, worktree, planning) for free.
|
||||||
|
- **App/Ui:** push `TaskId` in `WorkerClient` task-targeted calls (e.g. RunNow / Cancel / Continue / review actions) so the UI side of a task action carries the same key.
|
||||||
|
|
||||||
|
Result: grep one `TaskId` in `claudedo-.log` and read the full UI→Worker→UI story in timestamp order.
|
||||||
|
|
||||||
|
This adds **no parameters** to the SignalR surface — correlation rides on the existing `taskId` arguments already present in those calls.
|
||||||
|
|
||||||
|
## Implementation surface
|
||||||
|
|
||||||
|
A single shared helper keeps the two processes' Serilog setup from drifting.
|
||||||
|
|
||||||
|
- **New project:** `ClaudeDo.Logging` — a small library both `ClaudeDo.App` and `ClaudeDo.Worker` reference (keeps `ClaudeDo.Data` free of any Serilog dependency). Contains:
|
||||||
|
- `BuildConfig.IsDebug` — checks the entry assembly's `DebuggableAttribute` (`IsJITOptimizerDisabled` ⇒ Debug build). Cached static.
|
||||||
|
- The output template and the default-TaskId enricher.
|
||||||
|
- `ConfigureLogger(LoggerConfiguration, processTag, logRoot)` — applies level/sink choices by branching on `BuildConfig.IsDebug` (Debug ⇒ `Debug` level + console + file; Release ⇒ `Warning` level + file only). Both processes call it so level/template/retention stay in sync.
|
||||||
|
- **Worker `Program.cs:34`:** replace the inline `UseSerilog` body with a call into the shared helper (`processTag = "worker"`).
|
||||||
|
- **App `Program.cs`:** add Serilog packages; build a logger via the shared helper (`Process = "app"`) and register it with `sc.AddLogging(b => b.AddSerilog(logger, dispose: true))`. App currently registers **no** logging at all, so this also makes `ILogger<T>` injection actually work UI-side. Remove/keep `.LogToTrace()` as appropriate (Avalonia internal trace, separate concern — leave it).
|
||||||
|
- **App shutdown:** flush/close the logger (`Log.CloseAndFlush()` or dispose via the container's existing `finally`).
|
||||||
|
|
||||||
|
### Packages to add (App project)
|
||||||
|
|
||||||
|
- `Serilog.Extensions.Logging` (bridge `ILogger` → Serilog)
|
||||||
|
- `Serilog.Sinks.File`
|
||||||
|
- `Serilog.Sinks.Console`
|
||||||
|
- (Worker already has Serilog + File sink; add `Serilog.Sinks.Console` for the Debug console output.)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- This is logging wiring; per project policy, no tests that spawn the real Claude CLI and no heavy test scaffolding for log output.
|
||||||
|
- Light verification: a unit-level check that the default enricher yields `-` when no `TaskId` is pushed, and (if practical) that `ConfigureLogger` wires the expected sinks. `BuildConfig.IsDebug` reflects the test assembly's own build config, so it can't be flipped within one run — assert each branch by passing the level/flag explicitly rather than relying on the ambient value, or verify the Release path and smoke-test Debug manually from Rider.
|
||||||
|
- Manual smoke test (documented, not automated): run from Rider, confirm console + `claudedo-.log` show `Debug` lines with `Process`/`SourceContext`; run a task and confirm both `app` and `worker` lines share the same `[TaskId]`.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Runtime/config log-level knob.
|
||||||
|
- Per-call correlation IDs for non-task flows (connect, config edits, prep) — TaskId-only for now; revisit if a non-task flow proves to be a black hole.
|
||||||
|
- Changes to task-run NDJSON capture or `daily-prep.log`.
|
||||||
|
- Any change to `IWorkerClient` / `WorkerHub` signatures.
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Inherited settings display, per-task overrides, and Turns
|
||||||
|
|
||||||
|
**Date:** 2026-06-04
|
||||||
|
**Status:** Approved (design)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Config inheritance is three-tier (Task → List → Global app settings). Today the UI
|
||||||
|
only signals inheritance with a placeholder sentinel (`(inherit)` for tasks,
|
||||||
|
`(default)` for lists) and, for tasks, a faint "Effective if inherited: {value}"
|
||||||
|
hint under Model and Agent. Two gaps:
|
||||||
|
|
||||||
|
1. You can't see the *actual resolved value* an inherited field will use, nor where
|
||||||
|
it comes from (List vs Global).
|
||||||
|
2. **Max turns** is global-only (`AppSettingsEntity.DefaultMaxTurns` = 100). It is not
|
||||||
|
overridable per list or per task, unlike Model / SystemPrompt / AgentPath.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Show the real inherited value in-place, muted, with a **source-aware marker**
|
||||||
|
(`inherited · List` vs `inherited · Global`). Picking a value turns it into an
|
||||||
|
override; a reset affordance clears it back to inherited.
|
||||||
|
- Add **Turns** (max turns) as an overridable field at both List and Task levels,
|
||||||
|
inheriting from the global default. Numeric box; empty = inherit.
|
||||||
|
- Keep SystemPrompt as-is (it is additive, not override) but show what gets prepended.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No change to SystemPrompt merge semantics (stays additive/concatenated).
|
||||||
|
- No new global settings; `DefaultMaxTurns` already exists.
|
||||||
|
- No change to PermissionMode handling.
|
||||||
|
|
||||||
|
## Inheritance semantics (reference)
|
||||||
|
|
||||||
|
Resolved in `TaskRunner.BuildRunConfig` (~line 388):
|
||||||
|
|
||||||
|
| Field | Semantics | Resolution |
|
||||||
|
|--------------|------------|--------------------------------------------------------|
|
||||||
|
| Model | override | `task.Model ?? listConfig?.Model ?? global.DefaultModel` |
|
||||||
|
| AgentPath | override | `task.AgentPath ?? listConfig?.AgentPath` (no global) |
|
||||||
|
| MaxTurns | override | **new:** `task.MaxTurns ?? listConfig?.MaxTurns ?? global.DefaultMaxTurns` |
|
||||||
|
| SystemPrompt | additive | merged: global + list + task + agent (unchanged) |
|
||||||
|
|
||||||
|
Lists inherit only from Global (no tier above them), so a list's inherited marker is
|
||||||
|
always `inherited · Global`.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. Data layer
|
||||||
|
|
||||||
|
- `ListConfigEntity`: add `int? MaxTurns`.
|
||||||
|
- `TaskEntity`: add `int? MaxTurns` (nullable override).
|
||||||
|
- EF Core migration adding `max_turns` column to `list_config` and `tasks`
|
||||||
|
(nullable, no default — null = inherit).
|
||||||
|
- `TaskRunner` BuildRunConfig: `MaxTurns: task.MaxTurns ?? listConfig?.MaxTurns ?? global.DefaultMaxTurns`.
|
||||||
|
`ClaudeRunConfig.MaxTurns` and `ClaudeArgsBuilder` already accept/emit `--max-turns`
|
||||||
|
when `> 0` — no change needed there.
|
||||||
|
- `ListRepository.SetConfigAsync` (upsert) and `TaskRepository.UpdateAgentSettingsAsync`
|
||||||
|
extend to carry `maxTurns`.
|
||||||
|
|
||||||
|
### 2. DTOs / transport
|
||||||
|
|
||||||
|
Add `int? MaxTurns` to (Worker + Ui copies kept in sync):
|
||||||
|
|
||||||
|
- `UpdateListConfigDto`, `ListConfigDto` (WorkerHub.cs + WorkerClient.cs)
|
||||||
|
- `UpdateTaskAgentSettingsDto` (WorkerHub.cs + WorkerClient.cs)
|
||||||
|
- `TaskConfigDto` (ConfigMcpTools.cs)
|
||||||
|
|
||||||
|
`WorkerHub.UpdateListConfig` / `UpdateTaskAgentSettings` persist the new field via the
|
||||||
|
repositories above. MCP `SetListConfig` / `SetTaskConfig` gain an optional `maxTurns`
|
||||||
|
parameter to keep the agent-facing API at parity with the UI.
|
||||||
|
|
||||||
|
### 3. Resolution helper (Ui)
|
||||||
|
|
||||||
|
A small helper that, given `(taskValue, listValue, globalValue)`, returns
|
||||||
|
`(effectiveValue, source)` where `source ∈ { Override, List, Global }`. Drives the
|
||||||
|
marker text and muted/normal styling for Model, Agent, and Turns so the logic isn't
|
||||||
|
duplicated per field or per editor. Lives in the Ui layer beside its consumers.
|
||||||
|
|
||||||
|
### 4. UI rendering — inherited marker (source-aware)
|
||||||
|
|
||||||
|
For **Model**, **Agent**, **Turns** in both `ListSettingsModalView` and the
|
||||||
|
DetailsIsland "Agent settings (overrides)" expander:
|
||||||
|
|
||||||
|
- Remove the `(inherit)` / `(default)` sentinel *row* from the control's item source.
|
||||||
|
- When no override is set: control shows the **resolved value muted/greyed** (dropdown
|
||||||
|
shows e.g. "sonnet" dimmed; Turns box shows e.g. "100" as a muted placeholder), and a
|
||||||
|
small badge beside the field label reads `inherited · List` or `inherited · Global`.
|
||||||
|
- On picking a value / typing a number: it becomes an override — text returns to normal
|
||||||
|
color, the badge flips to `override` (or hides), and a small **"↺ reset to inherited"**
|
||||||
|
affordance appears that clears the value back to null.
|
||||||
|
- List modal: source is always Global → badge reads `inherited · Global`; reset clears
|
||||||
|
to the global default.
|
||||||
|
- Turns: numeric box, empty = inherit (muted resolved number as placeholder); a typed
|
||||||
|
number is the override.
|
||||||
|
|
||||||
|
**Rendering approach:** a small reusable `InheritedFieldHeader` control (label + badge +
|
||||||
|
reset button), fed by the resolution helper's `source`, wraps each field. Keeps the three
|
||||||
|
fields consistent and avoids per-field XAML duplication. Badge / muted styling uses
|
||||||
|
existing design tokens. Visual polish pass is the user's.
|
||||||
|
|
||||||
|
### 5. SystemPrompt (stays plain)
|
||||||
|
|
||||||
|
SystemPrompt keeps its plain multi-line text box (additive, not override). Below it, a
|
||||||
|
small **read-only, collapsed-by-default** hint shows the inherited prompts that will be
|
||||||
|
prepended (global + list), labeled e.g. "Prepended automatically:". No marker, no reset —
|
||||||
|
it never replaces, only appends.
|
||||||
|
|
||||||
|
### 6. Localization
|
||||||
|
|
||||||
|
New keys in `locales/en.json` + `locales/de.json` (parity enforced by Localization.Tests):
|
||||||
|
marker text (`inherited · List`, `inherited · Global`, `override`), reset affordance
|
||||||
|
label, Turns field label, and the SystemPrompt "prepended automatically" hint. Retire the
|
||||||
|
now-unused `vm.details.effectiveIfInherited` key (and its German counterpart) if nothing
|
||||||
|
else references it.
|
||||||
|
|
||||||
|
## Affected files (indicative)
|
||||||
|
|
||||||
|
- `src/ClaudeDo.Data/Models/ListConfigEntity.cs`, `TaskEntity.cs`
|
||||||
|
- `src/ClaudeDo.Data/Migrations/` (new migration)
|
||||||
|
- `src/ClaudeDo.Data/Repositories/ListRepository.cs`, `TaskRepository.cs`
|
||||||
|
- `src/ClaudeDo.Data/Configuration/` (column mapping for `max_turns`)
|
||||||
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
|
||||||
|
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs`
|
||||||
|
- `src/ClaudeDo.Worker/External/ConfigMcpTools.cs`
|
||||||
|
- `src/ClaudeDo.Ui/Services/WorkerClient.cs` (+ `Interfaces/IWorkerClient.cs`)
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` + view
|
||||||
|
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` + `DetailsIslandView.axaml`
|
||||||
|
- `src/ClaudeDo.Ui/Views/Controls/` (new `InheritedFieldHeader`)
|
||||||
|
- `src/ClaudeDo.Ui/` resolution helper
|
||||||
|
- `locales/en.json`, `locales/de.json`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Data: migration applies; `MaxTurns` round-trips through `ListRepository.SetConfigAsync`
|
||||||
|
and `TaskRepository.UpdateAgentSettingsAsync`.
|
||||||
|
- Worker: `BuildRunConfig` resolves MaxTurns via task → list → global precedence
|
||||||
|
(unit test on the resolution). Existing `ClaudeArgsBuilder` `--max-turns` behavior
|
||||||
|
unchanged.
|
||||||
|
- Ui: resolution helper returns correct `(value, source)` for each of the
|
||||||
|
override / list / global cases across Model, Agent, Turns.
|
||||||
|
- Localization: en/de key parity (existing Localization.Tests).
|
||||||
|
- Test fakes: update hand-rolled `IWorkerClient` fakes in both test projects for the new
|
||||||
|
DTO fields (per known gotcha).
|
||||||
|
- Visual verification of the marker / muted styling: flagged for the user (cannot be
|
||||||
|
asserted programmatically).
|
||||||
|
|
||||||
|
## Open risks
|
||||||
|
|
||||||
|
- DTO/ctor changes ripple into hand-rolled test fakes in Worker.Tests and Ui.Tests —
|
||||||
|
must be updated in the same change.
|
||||||
|
- Removing the sentinel row from dropdowns changes selection binding; ensure null/empty
|
||||||
|
override state is represented without a sentinel item (e.g. dropdown `SelectedItem`
|
||||||
|
null when inherited).
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
@@ -10,7 +11,12 @@ namespace ClaudeDo.App;
|
|||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
public static ServiceProvider Services { get; set; } = null!;
|
private readonly IServiceProvider? _services;
|
||||||
|
|
||||||
|
// Parameterless ctor is required by the XAML previewer / designer.
|
||||||
|
public App() { }
|
||||||
|
|
||||||
|
public App(IServiceProvider services) => _services = services;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -21,14 +27,19 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
|
var services = _services
|
||||||
|
?? throw new InvalidOperationException("App was constructed without a service provider.");
|
||||||
|
|
||||||
|
FocusClearing.Install();
|
||||||
|
|
||||||
desktop.MainWindow = new MainWindow
|
desktop.MainWindow = new MainWindow
|
||||||
{
|
{
|
||||||
DataContext = Services.GetRequiredService<IslandsShellViewModel>(),
|
DataContext = services.GetRequiredService<IslandsShellViewModel>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kick off the SignalR retry loop — reconnects indefinitely if the worker
|
// Kick off the SignalR retry loop — reconnects indefinitely if the worker
|
||||||
// is not up yet, or goes down and comes back.
|
// is not up yet, or goes down and comes back.
|
||||||
_ = Services.GetRequiredService<WorkerClient>().StartAsync();
|
_ = services.GetRequiredService<WorkerClient>().StartAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
|||||||
@@ -24,10 +24,12 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
|
<ProjectReference Include="..\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
|
||||||
|
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||||
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
|
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="..\ClaudeDo.Localization\Locales.targets" />
|
<Import Project="..\ClaudeDo.Localization\Locales.targets" />
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ using ClaudeDo.Ui.ViewModels.Modals;
|
|||||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -35,7 +37,6 @@ sealed class Program
|
|||||||
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
|
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
|
||||||
|
|
||||||
var services = BuildServices();
|
var services = BuildServices();
|
||||||
App.Services = services;
|
|
||||||
|
|
||||||
using (var scope = services.CreateScope())
|
using (var scope = services.CreateScope())
|
||||||
{
|
{
|
||||||
@@ -45,7 +46,7 @@ sealed class Program
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
BuildAvaloniaApp()
|
ConfigureAppBuilder(AppBuilder.Configure(() => new App(services)))
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -58,8 +59,12 @@ sealed class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parameterless entry point required by the XAML previewer / designer.
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
=> AppBuilder.Configure<App>()
|
=> ConfigureAppBuilder(AppBuilder.Configure<App>());
|
||||||
|
|
||||||
|
private static AppBuilder ConfigureAppBuilder(AppBuilder builder)
|
||||||
|
=> builder
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
.WithDeveloperTools()
|
.WithDeveloperTools()
|
||||||
@@ -74,6 +79,12 @@ sealed class Program
|
|||||||
|
|
||||||
var sc = new ServiceCollection();
|
var sc = new ServiceCollection();
|
||||||
|
|
||||||
|
var logRoot = Path.Combine(Path.GetDirectoryName(dbPath)!, "logs");
|
||||||
|
var serilogLogger = ClaudeDo.Logging.LoggingSetup
|
||||||
|
.Configure(new LoggerConfiguration(), "app", logRoot)
|
||||||
|
.CreateLogger();
|
||||||
|
sc.AddLogging(b => b.AddSerilog(serilogLogger, dispose: true));
|
||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
sc.AddSingleton(settings);
|
sc.AddSingleton(settings);
|
||||||
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
||||||
@@ -95,7 +106,10 @@ sealed class Program
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
sc.AddSingleton<GitService>();
|
sc.AddSingleton<GitService>();
|
||||||
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
sc.AddSingleton(sp => new WorkerClient(
|
||||||
|
sp.GetRequiredService<AppSettings>().SignalRUrl,
|
||||||
|
sp.GetRequiredService<ILogger<WorkerClient>>()));
|
||||||
|
sc.AddSingleton<IWorkerClient>(sp => sp.GetRequiredService<WorkerClient>());
|
||||||
|
|
||||||
// Release check + installer update
|
// Release check + installer update
|
||||||
sc.AddSingleton<HttpClient>(_ => new HttpClient { Timeout = TimeSpan.FromSeconds(10) });
|
sc.AddSingleton<HttpClient>(_ => new HttpClient { Timeout = TimeSpan.FromSeconds(10) });
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
|||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath (nullable overrides), IsStarred, IsMyDay, Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||||
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
|
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
|
||||||
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath (all nullable)
|
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns (all nullable)
|
||||||
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
||||||
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
||||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`) and `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`)
|
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay)
|
||||||
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
||||||
|
|
||||||
## Repositories
|
## Repositories
|
||||||
@@ -39,7 +39,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
|||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
|
|
||||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns.
|
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables).
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
|||||||
builder.Property(s => s.StandupWeekday).HasColumnName("standup_weekday")
|
builder.Property(s => s.StandupWeekday).HasColumnName("standup_weekday")
|
||||||
.IsRequired().HasDefaultValue((int)DayOfWeek.Wednesday);
|
.IsRequired().HasDefaultValue((int)DayOfWeek.Wednesday);
|
||||||
|
|
||||||
|
builder.Property(s => s.DailyPrepMaxTasks)
|
||||||
|
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||||
|
|
||||||
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
|||||||
builder.Property(c => c.Model).HasColumnName("model");
|
builder.Property(c => c.Model).HasColumnName("model");
|
||||||
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
||||||
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
||||||
|
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
TaskStatus.Idle => "idle",
|
TaskStatus.Idle => "idle",
|
||||||
TaskStatus.Queued => "queued",
|
TaskStatus.Queued => "queued",
|
||||||
TaskStatus.Running => "running",
|
TaskStatus.Running => "running",
|
||||||
TaskStatus.WaitingForReview => "waiting_for_review",
|
TaskStatus.WaitingForReview => "waiting_for_review",
|
||||||
TaskStatus.Done => "done",
|
TaskStatus.WaitingForChildren => "waiting_for_children",
|
||||||
|
TaskStatus.Done => "done",
|
||||||
TaskStatus.Failed => "failed",
|
TaskStatus.Failed => "failed",
|
||||||
TaskStatus.Cancelled => "cancelled",
|
TaskStatus.Cancelled => "cancelled",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||||
@@ -27,8 +28,9 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
"idle" => TaskStatus.Idle,
|
"idle" => TaskStatus.Idle,
|
||||||
"queued" => TaskStatus.Queued,
|
"queued" => TaskStatus.Queued,
|
||||||
"running" => TaskStatus.Running,
|
"running" => TaskStatus.Running,
|
||||||
"waiting_for_review" => TaskStatus.WaitingForReview,
|
"waiting_for_review" => TaskStatus.WaitingForReview,
|
||||||
"done" => TaskStatus.Done,
|
"waiting_for_children" => TaskStatus.WaitingForChildren,
|
||||||
|
"done" => TaskStatus.Done,
|
||||||
"failed" => TaskStatus.Failed,
|
"failed" => TaskStatus.Failed,
|
||||||
"cancelled" => TaskStatus.Cancelled,
|
"cancelled" => TaskStatus.Cancelled,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||||
@@ -75,6 +77,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
||||||
builder.Property(t => t.Result).HasColumnName("result");
|
builder.Property(t => t.Result).HasColumnName("result");
|
||||||
builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback");
|
builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback");
|
||||||
|
builder.Property(t => t.RoadblockCount).HasColumnName("roadblock_count").HasDefaultValue(0);
|
||||||
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
||||||
builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired();
|
builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired();
|
||||||
builder.Property(t => t.StartedAt).HasColumnName("started_at");
|
builder.Property(t => t.StartedAt).HasColumnName("started_at");
|
||||||
@@ -83,6 +86,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.Model).HasColumnName("model");
|
builder.Property(t => t.Model).HasColumnName("model");
|
||||||
builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt");
|
builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt");
|
||||||
builder.Property(t => t.AgentPath).HasColumnName("agent_path");
|
builder.Property(t => t.AgentPath).HasColumnName("agent_path");
|
||||||
|
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
||||||
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
||||||
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
||||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||||
|
|||||||
682
src/ClaudeDo.Data/Migrations/20260603141020_DailyPrepMaxTasks.Designer.cs
generated
Normal file
682
src/ClaudeDo.Data/Migrations/20260603141020_DailyPrepMaxTasks.Designer.cs
generated
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260603141020_DailyPrepMaxTasks")]
|
||||||
|
partial class DailyPrepMaxTasks
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DailyPrepMaxTasks : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "daily_prep_max_tasks",
|
||||||
|
table: "app_settings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 5);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "daily_prep_max_tasks",
|
||||||
|
value: 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "daily_prep_max_tasks",
|
||||||
|
table: "app_settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
690
src/ClaudeDo.Data/Migrations/20260604101453_InheritableMaxTurns.Designer.cs
generated
Normal file
690
src/ClaudeDo.Data/Migrations/20260604101453_InheritableMaxTurns.Designer.cs
generated
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260604101453_InheritableMaxTurns")]
|
||||||
|
partial class InheritableMaxTurns
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InheritableMaxTurns : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "max_turns",
|
||||||
|
table: "tasks",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "max_turns",
|
||||||
|
table: "list_config",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "max_turns",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "max_turns",
|
||||||
|
table: "list_config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
696
src/ClaudeDo.Data/Migrations/20260604125720_AddRoadblockCount.Designer.cs
generated
Normal file
696
src/ClaudeDo.Data/Migrations/20260604125720_AddRoadblockCount.Designer.cs
generated
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260604125720_AddRoadblockCount")]
|
||||||
|
partial class AddRoadblockCount
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 100,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRoadblockCount : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "roadblock_count",
|
||||||
|
table: "tasks",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "roadblock_count",
|
||||||
|
table: "tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("central_worktree_root");
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
b.Property<string>("DefaultClaudeInstructions")
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -101,6 +107,7 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
DefaultClaudeInstructions = "",
|
DefaultClaudeInstructions = "",
|
||||||
DefaultMaxTurns = 100,
|
DefaultMaxTurns = 100,
|
||||||
DefaultModel = "sonnet",
|
DefaultModel = "sonnet",
|
||||||
@@ -153,6 +160,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("agent_path");
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
b.Property<string>("Model")
|
b.Property<string>("Model")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("model");
|
.HasColumnName("model");
|
||||||
@@ -341,6 +352,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("log_path");
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
b.Property<string>("Model")
|
b.Property<string>("Model")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("model");
|
.HasColumnName("model");
|
||||||
@@ -380,6 +395,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("review_feedback");
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
b.Property<DateTime?>("ScheduledFor")
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("scheduled_for");
|
.HasColumnName("scheduled_for");
|
||||||
|
|||||||
@@ -24,4 +24,7 @@ public sealed class AppSettingsEntity
|
|||||||
public string? ReportExcludedPaths { get; set; }
|
public string? ReportExcludedPaths { get; set; }
|
||||||
// DayOfWeek the standup happens on; default Wednesday. Drives the report's default range.
|
// DayOfWeek the standup happens on; default Wednesday. Drives the report's default range.
|
||||||
public int StandupWeekday { get; set; } = (int)DayOfWeek.Wednesday;
|
public int StandupWeekday { get; set; } = (int)DayOfWeek.Wednesday;
|
||||||
|
|
||||||
|
// Max number of open tasks the daily prep ("Prime Claude") may place in MyDay.
|
||||||
|
public int DailyPrepMaxTasks { get; set; } = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public sealed class ListConfigEntity
|
|||||||
public string? Model { get; set; }
|
public string? Model { get; set; }
|
||||||
public string? SystemPrompt { get; set; }
|
public string? SystemPrompt { get; set; }
|
||||||
public string? AgentPath { get; set; }
|
public string? AgentPath { get; set; }
|
||||||
|
public int? MaxTurns { get; set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public ListEntity List { get; set; } = null!;
|
public ListEntity List { get; set; } = null!;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public enum TaskStatus
|
|||||||
Queued,
|
Queued,
|
||||||
Running,
|
Running,
|
||||||
WaitingForReview,
|
WaitingForReview,
|
||||||
|
WaitingForChildren,
|
||||||
Done,
|
Done,
|
||||||
Failed,
|
Failed,
|
||||||
Cancelled,
|
Cancelled,
|
||||||
@@ -30,6 +31,7 @@ public sealed class TaskEntity
|
|||||||
public DateTime? ScheduledFor { get; set; }
|
public DateTime? ScheduledFor { get; set; }
|
||||||
public string? Result { get; set; }
|
public string? Result { get; set; }
|
||||||
public string? ReviewFeedback { get; set; }
|
public string? ReviewFeedback { get; set; }
|
||||||
|
public int RoadblockCount { get; set; }
|
||||||
public string? LogPath { get; set; }
|
public string? LogPath { get; set; }
|
||||||
public required DateTime CreatedAt { get; init; }
|
public required DateTime CreatedAt { get; init; }
|
||||||
public DateTime? StartedAt { get; set; }
|
public DateTime? StartedAt { get; set; }
|
||||||
@@ -38,6 +40,7 @@ public sealed class TaskEntity
|
|||||||
public string? Model { get; set; }
|
public string? Model { get; set; }
|
||||||
public string? SystemPrompt { get; set; }
|
public string? SystemPrompt { get; set; }
|
||||||
public string? AgentPath { get; set; }
|
public string? AgentPath { get; set; }
|
||||||
|
public int? MaxTurns { get; set; }
|
||||||
public bool IsStarred { get; set; }
|
public bool IsStarred { get; set; }
|
||||||
public bool IsMyDay { get; set; }
|
public bool IsMyDay { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace ClaudeDo.Data;
|
namespace ClaudeDo.Data;
|
||||||
|
|
||||||
public enum PromptKind { System, Planning, Agent }
|
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild }
|
||||||
|
|
||||||
public static class PromptFiles
|
public static class PromptFiles
|
||||||
{
|
{
|
||||||
@@ -9,8 +11,12 @@ public static class PromptFiles
|
|||||||
public static string PathFor(PromptKind kind) => kind switch
|
public static string PathFor(PromptKind kind) => kind switch
|
||||||
{
|
{
|
||||||
PromptKind.System => Path.Combine(Root, "system.md"),
|
PromptKind.System => Path.Combine(Root, "system.md"),
|
||||||
PromptKind.Planning => Path.Combine(Root, "planning.md"),
|
PromptKind.Planning => Path.Combine(Root, "planning-system.md"),
|
||||||
PromptKind.Agent => Path.Combine(Root, "agent.md"),
|
PromptKind.PlanningInitial => Path.Combine(Root, "planning-initial.md"),
|
||||||
|
PromptKind.Retry => Path.Combine(Root, "retry.md"),
|
||||||
|
PromptKind.DailyPrep => Path.Combine(Root, "daily-prep.md"),
|
||||||
|
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
||||||
|
PromptKind.ImprovementChild => Path.Combine(Root, "improvement-child.md"),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,29 +36,169 @@ public static class PromptFiles
|
|||||||
return string.IsNullOrEmpty(content) ? null : content;
|
return string.IsNullOrEmpty(content) ? null : content;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string DefaultFor(PromptKind kind) => kind switch
|
/// <summary>File content if present and non-empty, otherwise the bundled default.</summary>
|
||||||
|
public static string ReadOrDefault(PromptKind kind) => ReadOrNull(kind) ?? DefaultFor(kind);
|
||||||
|
|
||||||
|
/// <summary>Render a prompt: read file-or-default, then substitute named tokens.</summary>
|
||||||
|
public static string Render(PromptKind kind, IReadOnlyDictionary<string, string> values)
|
||||||
|
=> RenderTemplate(ReadOrDefault(kind), values);
|
||||||
|
|
||||||
|
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.</summary>
|
||||||
|
public static string RenderTemplate(string template, IReadOnlyDictionary<string, string> values)
|
||||||
{
|
{
|
||||||
PromptKind.System =>
|
var sb = new StringBuilder(template);
|
||||||
"# System Prompt\n\n" +
|
foreach (var (key, val) in values)
|
||||||
"Baseline instructions appended to every task run.\n" +
|
sb.Replace("{" + key + "}", val);
|
||||||
"Edit this file to inject project-wide rules (style, conventions, hard constraints).\n",
|
return sb.ToString();
|
||||||
PromptKind.Planning =>
|
}
|
||||||
"You are a planning assistant for ClaudeDo.\n" +
|
|
||||||
"Your role is to help break down a task into smaller, actionable subtasks.\n" +
|
public static string DefaultFor(PromptKind kind) => kind switch
|
||||||
"Your final goal WILL ALWAYS be the creation of Subtasks.\n\n" +
|
{
|
||||||
"ALWAYS invoke the `superpowers:brainstorming` skill via the Skill tool at the\n" +
|
PromptKind.System => SystemDefault,
|
||||||
"start of every planning session, and follow its process end-to-end. It guides\n" +
|
PromptKind.Planning => PlanningSystemDefault,
|
||||||
"you through clarifying questions, approach exploration, and design approval\n" +
|
PromptKind.PlanningInitial => PlanningInitialDefault,
|
||||||
"BEFORE any subtasks are created. Do not create child tasks until the user has\n" +
|
PromptKind.Retry => RetryDefault,
|
||||||
"approved a design.\n\n" +
|
PromptKind.DailyPrep => DailyPrepDefault,
|
||||||
"NEVER change files yourself.\n\n" +
|
PromptKind.WeeklyReport => WeeklyReportDefault,
|
||||||
"ALWAYS use the available MCP tools (mcp__claudedo__*) to create child tasks once\n" +
|
PromptKind.ImprovementChild => ImprovementChildDefault,
|
||||||
"the design is approved. When you are done planning, finalize the session.\n\n" +
|
|
||||||
"Be concise and focused. Each subtask should be independently executable.\n",
|
|
||||||
PromptKind.Agent =>
|
|
||||||
"# Agent Prompt\n\n" +
|
|
||||||
"Appended to the system prompt for tasks tagged \"agent\" (auto-queued runs).\n" +
|
|
||||||
"Use this for autonomous-execution rules that don't apply to manual runs.\n",
|
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private const string SystemDefault = """
|
||||||
|
# Working Agreement
|
||||||
|
|
||||||
|
You are completing one well-defined task autonomously in a git repository.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Do exactly what the task asks — no unrequested refactors, renames, dependency
|
||||||
|
changes, or "while I'm here" cleanup.
|
||||||
|
- If intent is ambiguous, state the assumption you're making and proceed with the
|
||||||
|
most reasonable reading. Stop only if you genuinely cannot move forward.
|
||||||
|
- Prefer three similar lines over a premature abstraction. Don't build for
|
||||||
|
hypothetical future needs.
|
||||||
|
|
||||||
|
## Out-of-scope improvements
|
||||||
|
If you notice worthwhile work that is genuinely outside this task's scope
|
||||||
|
(a refactor, a follow-up, tech debt), do NOT do it here. File it with
|
||||||
|
SuggestImprovement(title, description) and stay focused on the task at hand.
|
||||||
|
|
||||||
|
## Working in the repo
|
||||||
|
- Read a file before editing it. Match the conventions already in this codebase —
|
||||||
|
they override generic defaults.
|
||||||
|
- Prefer editing existing files to creating new ones. Don't write comments that
|
||||||
|
just restate the code.
|
||||||
|
- Validate only at real boundaries (user input, external APIs).
|
||||||
|
|
||||||
|
## Finishing
|
||||||
|
- Before claiming done, verify: run the build and relevant tests, confirm they
|
||||||
|
pass, and report what you ran. If you couldn't verify something, say so plainly.
|
||||||
|
- Make focused commits using the repository's existing commit-message convention.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
- Never force-push, hard-reset, or delete branches/files beyond the task's scope
|
||||||
|
without being asked.
|
||||||
|
- Don't introduce injection/XSS/secret-leak issues. Never commit credentials.
|
||||||
|
|
||||||
|
## You are running unattended
|
||||||
|
You run autonomously with no human watching. There is no one to answer mid-task
|
||||||
|
questions, so never stop to ask — make the most reasonable decision, note the
|
||||||
|
assumption, and continue.
|
||||||
|
|
||||||
|
## When you are blocked
|
||||||
|
If something genuinely prevents you from completing part of the task (missing
|
||||||
|
credentials, contradictory requirements, a destructive action you won't take
|
||||||
|
unasked), do NOT silently give up. Write this marker on its own line, then keep
|
||||||
|
working on whatever else you can:
|
||||||
|
|
||||||
|
CLAUDEDO_BLOCKED: <one short sentence describing what blocked you>
|
||||||
|
|
||||||
|
Emit it as many times as needed — once per distinct blocker. Use it only for true
|
||||||
|
blockers, not for routine decisions you can make yourself.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string ImprovementChildDefault = """
|
||||||
|
# Out-of-scope follow-up
|
||||||
|
|
||||||
|
You are an improvement follow-up that another task filed via SuggestImprovement.
|
||||||
|
It was deliberately scoped narrow. Do EXACTLY what this task's title and
|
||||||
|
description ask — nothing more.
|
||||||
|
|
||||||
|
- Make the smallest change that satisfies the task. No opportunistic refactors,
|
||||||
|
renames, reformatting, or "while I'm here" cleanup beyond what is asked.
|
||||||
|
- Touch as few files as possible. Do not restructure unrelated code.
|
||||||
|
- Do NOT file further improvements — improvements are one layer deep.
|
||||||
|
- Verify the build and relevant tests before finishing, and report what you ran.
|
||||||
|
- Make one focused commit using the repository's commit-message convention.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string PlanningSystemDefault = """
|
||||||
|
You are the planning assistant for ClaudeDo. Your job is to break a task into
|
||||||
|
smaller, independently executable subtasks — the session ends by creating those
|
||||||
|
subtasks.
|
||||||
|
|
||||||
|
Start every session by invoking the `superpowers:brainstorming` skill (Skill
|
||||||
|
tool) and follow it end to end: clarifying questions one at a time, then 2–3
|
||||||
|
approaches with a recommendation, then a short design. Do not create any subtasks
|
||||||
|
until the user has approved the design.
|
||||||
|
|
||||||
|
You can ONLY shape this task's plan — you cannot edit files or touch other tasks.
|
||||||
|
The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask,
|
||||||
|
DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else.
|
||||||
|
|
||||||
|
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||||
|
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||||
|
done-state, ordered so dependencies come first.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string PlanningInitialDefault = """
|
||||||
|
# Task to plan: {title}
|
||||||
|
|
||||||
|
{description}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string RetryDefault = """
|
||||||
|
The task did not complete on the previous attempt — you may have run out of
|
||||||
|
turns, hit an error, or stopped before finishing.
|
||||||
|
|
||||||
|
Review the work already done in this session and the current state of the
|
||||||
|
repository, identify what is still incomplete or broken, and finish the task.
|
||||||
|
Don't restart from scratch or repeat a failed approach. Verify the result
|
||||||
|
(build + tests) before you stop.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string DailyPrepDefault = """
|
||||||
|
You are preparing my workday for {date}.
|
||||||
|
|
||||||
|
1. Call mcp__claudedo__get_daily_prep_candidates.
|
||||||
|
2. Keep tasks already marked MyDay (currentMyDay) — never remove them.
|
||||||
|
3. Fill MyDay to at most {maxTasks} open tasks TOTAL (currentMyDay counts). Never exceed it.
|
||||||
|
4. Estimate each candidate's effort and pick a feasible mix — not only big items.
|
||||||
|
Prioritize isStarred, due (scheduledFor), and older tasks.
|
||||||
|
5. Place related tasks next to each other using consecutive sortOrder values.
|
||||||
|
6. Apply via mcp__claudedo__set_my_day(taskId, true, sortOrder). Never mark anything
|
||||||
|
outside the candidate list.
|
||||||
|
|
||||||
|
If there are no candidates, do nothing.
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string WeeklyReportDefault = """
|
||||||
|
You are generating a concise weekly standup report for a software developer,
|
||||||
|
covering {start} to {end}.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Write the ENTIRE report in German.
|
||||||
|
- Group by day. One "## {Wochentag}, {dd.MM.yyyy}" section per day that has
|
||||||
|
activity (German weekday names). Omit days with no activity.
|
||||||
|
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
||||||
|
"- Y behoben"). Merge related small work into one bullet.
|
||||||
|
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
||||||
|
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
||||||
|
bullet list per day. The notes are authoritative — never omit or contradict them.
|
||||||
|
- Name the project/repo when it adds clarity.
|
||||||
|
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
||||||
|
|
||||||
|
Two sections follow below: an activity log derived from Claude session history,
|
||||||
|
and the developer's own notes. Base the report on both; the notes are
|
||||||
|
authoritative where they conflict with the derived activity.
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public sealed class AppSettingsRepository
|
|||||||
row.ReportExcludedPaths = string.IsNullOrWhiteSpace(updated.ReportExcludedPaths)
|
row.ReportExcludedPaths = string.IsNullOrWhiteSpace(updated.ReportExcludedPaths)
|
||||||
? null : updated.ReportExcludedPaths;
|
? null : updated.ReportExcludedPaths;
|
||||||
row.StandupWeekday = updated.StandupWeekday;
|
row.StandupWeekday = updated.StandupWeekday;
|
||||||
|
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||||
|
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public sealed class ListRepository
|
|||||||
existing.Model = config.Model;
|
existing.Model = config.Model;
|
||||||
existing.SystemPrompt = config.SystemPrompt;
|
existing.SystemPrompt = config.SystemPrompt;
|
||||||
existing.AgentPath = config.AgentPath;
|
existing.AgentPath = config.AgentPath;
|
||||||
|
existing.MaxTurns = config.MaxTurns;
|
||||||
}
|
}
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,13 @@ public sealed class TaskRepository
|
|||||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.LogPath, logPath), ct);
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.LogPath, logPath), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetRoadblockCountAsync(string taskId, int count, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _context.Tasks
|
||||||
|
.Where(t => t.Id == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.RoadblockCount, count), ct);
|
||||||
|
}
|
||||||
|
|
||||||
internal async Task<int> FlipAllRunningToFailedAsync(string reason, CancellationToken ct = default)
|
internal async Task<int> FlipAllRunningToFailedAsync(string reason, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var resultText = "[stale] " + reason;
|
var resultText = "[stale] " + reason;
|
||||||
@@ -159,6 +166,7 @@ public sealed class TaskRepository
|
|||||||
string? model,
|
string? model,
|
||||||
string? systemPrompt,
|
string? systemPrompt,
|
||||||
string? agentPath,
|
string? agentPath,
|
||||||
|
int? maxTurns = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _context.Tasks
|
await _context.Tasks
|
||||||
@@ -166,7 +174,8 @@ public sealed class TaskRepository
|
|||||||
.ExecuteUpdateAsync(s => s
|
.ExecuteUpdateAsync(s => s
|
||||||
.SetProperty(t => t.Model, model)
|
.SetProperty(t => t.Model, model)
|
||||||
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
||||||
.SetProperty(t => t.AgentPath, agentPath), ct);
|
.SetProperty(t => t.AgentPath, agentPath)
|
||||||
|
.SetProperty(t => t.MaxTurns, maxTurns), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -187,6 +196,7 @@ public sealed class TaskRepository
|
|||||||
string title,
|
string title,
|
||||||
string? description,
|
string? description,
|
||||||
string? commitType,
|
string? commitType,
|
||||||
|
string? createdBy = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
||||||
@@ -195,9 +205,6 @@ public sealed class TaskRepository
|
|||||||
.FirstOrDefaultAsync(t => t.Id == parentId, ct);
|
.FirstOrDefaultAsync(t => t.Id == parentId, ct);
|
||||||
if (parent is null)
|
if (parent is null)
|
||||||
throw new InvalidOperationException($"Parent task {parentId} not found.");
|
throw new InvalidOperationException($"Parent task {parentId} not found.");
|
||||||
if (parent.PlanningPhase == PlanningPhase.None)
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Parent task {parentId} is not in a planning phase; cannot attach children.");
|
|
||||||
|
|
||||||
var maxSort = await _context.Tasks
|
var maxSort = await _context.Tasks
|
||||||
.Where(t => t.ListId == parent.ListId)
|
.Where(t => t.ListId == parent.ListId)
|
||||||
@@ -215,6 +222,7 @@ public sealed class TaskRepository
|
|||||||
CommitType = string.IsNullOrEmpty(commitType) ? parent.CommitType : commitType,
|
CommitType = string.IsNullOrEmpty(commitType) ? parent.CommitType : commitType,
|
||||||
ParentTaskId = parentId,
|
ParentTaskId = parentId,
|
||||||
SortOrder = (maxSort ?? -1) + 1,
|
SortOrder = (maxSort ?? -1) + 1,
|
||||||
|
CreatedBy = createdBy,
|
||||||
};
|
};
|
||||||
_context.Tasks.Add(child);
|
_context.Tasks.Add(child);
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
@@ -385,6 +393,9 @@ public sealed class TaskRepository
|
|||||||
{
|
{
|
||||||
var orphanIds = await _context.Tasks
|
var orphanIds = await _context.Tasks
|
||||||
.Where(t => t.ParentTaskId != null && t.Status == TaskStatus.Queued)
|
.Where(t => t.ParentTaskId != null && t.Status == TaskStatus.Queued)
|
||||||
|
// Agent-suggested improvement children (CreatedBy == ParentTaskId) legitimately
|
||||||
|
// queue under a non-planning parent — they are not orphaned planning-chain members.
|
||||||
|
.Where(t => t.CreatedBy == null || t.CreatedBy != t.ParentTaskId)
|
||||||
.Where(t => !_context.Tasks.Any(p =>
|
.Where(t => !_context.Tasks.Any(p =>
|
||||||
p.Id == t.ParentTaskId && p.PlanningPhase != PlanningPhase.None))
|
p.Id == t.ParentTaskId && p.PlanningPhase != PlanningPhase.None))
|
||||||
.Select(t => t.Id)
|
.Select(t => t.Id)
|
||||||
|
|||||||
110
src/ClaudeDo.Installer/CLAUDE.md
Normal file
110
src/ClaudeDo.Installer/CLAUDE.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# ClaudeDo.Installer
|
||||||
|
|
||||||
|
WPF GUI installer, updater, and configuration tool for ClaudeDo. Not WiX/NSIS — the app is its own installer.
|
||||||
|
|
||||||
|
Note: this is the one project where `System.Windows` is correct (WPF, not Avalonia).
|
||||||
|
|
||||||
|
## Project Facts
|
||||||
|
|
||||||
|
- `<UseWPF>true</UseWPF>`, `WinExe`, `net8.0-windows`
|
||||||
|
- `<EnableWindowsTargeting>true</EnableWindowsTargeting>` — allows Linux CI to cross-compile
|
||||||
|
- Single-file framework-dependent publish: `dotnet publish -r win-x64 -p:PublishSingleFile=true` (needs .NET 8 Desktop Runtime)
|
||||||
|
- Entry point: `App.xaml` / `App.xaml.cs` (no `Program.cs`)
|
||||||
|
- References: `ClaudeDo.Data`, `ClaudeDo.Releases`, `ClaudeDo.Localization`
|
||||||
|
- Manifests: `app.manifest` (requireAdministrator, Release) / `app.debug.manifest` (asInvoker, Debug)
|
||||||
|
- Only CLI arg: `--replace-self <old-path>` (self-update handoff)
|
||||||
|
|
||||||
|
## Startup Sequence (`App.OnStartup`)
|
||||||
|
|
||||||
|
1. Load locale
|
||||||
|
2. Self-update preflight — `SelfUpdater.DecideUpdateAsync` checks Gitea API; if a newer installer exists, download + checksum verify + relaunch with `--replace-self <old-path>`
|
||||||
|
3. Detect mode — `InstallModeDetector` reads `install.json` + Gitea API
|
||||||
|
4. Open `WizardWindow` (FreshInstall / Update) or `SettingsWindow` (Config)
|
||||||
|
|
||||||
|
## Modes (`Core/InstallerMode.cs`)
|
||||||
|
|
||||||
|
| Mode | Condition | Window |
|
||||||
|
|---|---|---|
|
||||||
|
| `FreshInstall` | No `install.json` | Full wizard (all pages) |
|
||||||
|
| `Update` | `install.json` present + newer release available | Wizard — Welcome + Install pages only |
|
||||||
|
| `Config` | Current version, or Gitea API unreachable | `SettingsWindow` (settings / repair / uninstall) |
|
||||||
|
|
||||||
|
## Install Pipelines
|
||||||
|
|
||||||
|
Each step implements `IInstallStep`; `InstallerService` runs them sequentially, stops on failure.
|
||||||
|
|
||||||
|
**FreshInstall:**
|
||||||
|
`DownloadAndExtractStep` → `WriteConfigStep` → `InitDatabaseStep` → `RegisterMcpStep` (optional) → `RegisterAutostartStep` → `CreateShortcutsStep` → `WriteUninstallRegistryStep` → `WriteInstallManifestStep` → `StartWorkerStep`
|
||||||
|
|
||||||
|
**Update:**
|
||||||
|
`StopWorkerStep` → `DownloadAndExtractStep` → `RegisterAutostartStep` → `RegisterMcpStep` → `StartWorkerStep` → `WriteInstallManifestStep` → `WriteUninstallRegistryStep`
|
||||||
|
|
||||||
|
**Repair** (via `SettingsViewModel`):
|
||||||
|
`StopWorkerStep` → `DownloadAndExtractStep` → `RegisterAutostartStep` → `StartWorkerStep`
|
||||||
|
|
||||||
|
**Uninstall** (`UninstallRunner`):
|
||||||
|
Stop worker → remove legacy task/service → delete HKLM uninstall key + shortcuts → delete install dir (cmd.exe trampoline if uninstaller exe is inside it) → optionally delete `~/.todo-app`
|
||||||
|
|
||||||
|
## Folder Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Installer/
|
||||||
|
Steps/ — one class per action (see pipeline lists above)
|
||||||
|
Core/ — InstallContext, InstallerMode, InstallModeDetector, InstallManifest(+Store),
|
||||||
|
ConfigModels, InstallerService, UninstallRunner, PageResolver,
|
||||||
|
AutostartShortcut, ShortcutFactory, ProcessRunner, DarkTitleBar
|
||||||
|
Interfaces/ — IInstallStep + StepResult/StepStatus/StepProgress, IInstallerPage
|
||||||
|
Pages/ — WelcomePage, PathsPage, ServicePage, UiSettingsPage, InstallPage
|
||||||
|
(each: ViewModel + View.xaml)
|
||||||
|
Views/ — WizardWindow(+WizardViewModel), SettingsWindow(+SettingsViewModel),
|
||||||
|
SelfUpdatePromptWindow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Step Behaviors
|
||||||
|
|
||||||
|
**`RegisterMcpStep`** — registers the external MCP endpoint with the Claude CLI:
|
||||||
|
```
|
||||||
|
claude mcp remove --scope user claudedo
|
||||||
|
claude mcp add --transport http --scope user claudedo http://127.0.0.1:{ExternalMcpPort}/mcp
|
||||||
|
```
|
||||||
|
Non-fatal if `claude` CLI is missing or too old (prints the manual command). Server name: `claudedo`.
|
||||||
|
|
||||||
|
**`RegisterAutostartStep`** — creates a per-user Startup-folder shortcut `ClaudeDo Worker.lnk` (`Environment.SpecialFolder.Startup`). Also migrates away from legacy mechanisms:
|
||||||
|
- Deletes legacy Windows service: `sc.exe stop/delete ClaudeDoWorker`
|
||||||
|
- Deletes legacy scheduled task: `schtasks /Delete /TN ClaudeDoWorker`
|
||||||
|
|
||||||
|
No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works.
|
||||||
|
|
||||||
|
## `InstallContext` Defaults
|
||||||
|
|
||||||
|
| Property | Default |
|
||||||
|
|---|---|
|
||||||
|
| `InstallDirectory` | `C:\Program Files\ClaudeDo` |
|
||||||
|
| `DbPath` | `~/.todo-app/todo.db` |
|
||||||
|
| `LogRoot` | `~/.todo-app/logs` |
|
||||||
|
| `SandboxRoot` | `~/.todo-app/sandbox` |
|
||||||
|
| `WorktreeRootStrategy` | `sibling` |
|
||||||
|
| `SignalRPort` | `47821` |
|
||||||
|
| `ExternalMcpPort` | `47822` |
|
||||||
|
| `QueueBackstopIntervalMs` | `30000` |
|
||||||
|
| `ClaudeBin` | `claude` |
|
||||||
|
| `AutoStart` | `true` |
|
||||||
|
| `SignalRUrl` | `http://127.0.0.1:47821/hub` |
|
||||||
|
|
||||||
|
## Files Written by Install
|
||||||
|
|
||||||
|
| Path | Content |
|
||||||
|
|---|---|
|
||||||
|
| `~/.todo-app/worker.config.json` | Worker config |
|
||||||
|
| `~/.todo-app/ui.config.json` | UI config |
|
||||||
|
| `~/.todo-app/todo.db` | SQLite DB (EF migrations) |
|
||||||
|
| `<InstallDir>\install.json` | Install manifest |
|
||||||
|
| `<InstallDir>\app\` | UI binaries |
|
||||||
|
| `<InstallDir>\worker\` | Worker binaries |
|
||||||
|
| `<InstallDir>\uninstaller\ClaudeDo.Installer.exe` | Uninstaller copy |
|
||||||
|
| `HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo` | Uninstall registry key |
|
||||||
|
| Start Menu shortcut | `ClaudeDo.lnk` |
|
||||||
|
| Desktop shortcut (optional) | `ClaudeDo.lnk` |
|
||||||
|
| `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\ClaudeDo Worker.lnk` | Worker autostart |
|
||||||
|
|
||||||
|
The Apps & Features uninstall string and "Rerun Installer" both point at `<InstallDir>\uninstaller\ClaudeDo.Installer.exe` with no `/uninstall` flag — Config mode is detected from `install.json`.
|
||||||
@@ -43,13 +43,17 @@
|
|||||||
"restoreDefaultAgents": "Standard-Agenten wiederherstellen",
|
"restoreDefaultAgents": "Standard-Agenten wiederherstellen",
|
||||||
"promptsSection": "PROMPTS",
|
"promptsSection": "PROMPTS",
|
||||||
"systemPrompt": "System",
|
"systemPrompt": "System",
|
||||||
"planningPrompt": "Planung",
|
"planningPrompt": "Planung (System)",
|
||||||
"agentPrompt": "Agent",
|
"planningInitialPrompt": "Planungs-Start",
|
||||||
|
"retryPrompt": "Wiederholung",
|
||||||
|
"dailyPrepPrompt": "Tagesplanung",
|
||||||
|
"weeklyReportPrompt": "Wochenbericht",
|
||||||
"openInEditor": "Im Editor öffnen"
|
"openInEditor": "Im Editor öffnen"
|
||||||
},
|
},
|
||||||
"prime": {
|
"prime": {
|
||||||
"description": "Bereite dein Claude-Nutzungsfenster vor, indem an den von dir gewählten Tagen zu einer bestimmten Zeit ein einzelner nicht-interaktiver Ping ausgelöst wird. Läuft nur, solange ClaudeDo geöffnet ist. Wenn die App innerhalb von 30 Minuten vor der Zielzeit startet, wird der Ping sofort ausgelöst.",
|
"description": "Bereite dein Claude-Nutzungsfenster vor, indem an den von dir gewählten Tagen zu einer bestimmten Zeit ein einzelner nicht-interaktiver Ping ausgelöst wird. Läuft nur, solange ClaudeDo geöffnet ist. Wenn die App innerhalb von 30 Minuten vor der Zielzeit startet, wird der Ping sofort ausgelöst.",
|
||||||
"addSchedule": "+ Zeitplan hinzufügen",
|
"addSchedule": "+ Zeitplan hinzufügen",
|
||||||
|
"dailyPrepMaxTasks": "Max. Aufgaben pro Tag",
|
||||||
"dayMo": "Mo",
|
"dayMo": "Mo",
|
||||||
"dayTu": "Di",
|
"dayTu": "Di",
|
||||||
"dayWe": "Mi",
|
"dayWe": "Mi",
|
||||||
@@ -57,15 +61,22 @@
|
|||||||
"dayFr": "Fr",
|
"dayFr": "Fr",
|
||||||
"daySa": "Sa",
|
"daySa": "Sa",
|
||||||
"daySu": "So"
|
"daySu": "So"
|
||||||
|
},
|
||||||
|
"inherit": {
|
||||||
|
"inheritedFromList": "geerbt · Liste",
|
||||||
|
"inheritedFromGlobal": "geerbt · Global",
|
||||||
|
"overrideBadge": "überschrieben",
|
||||||
|
"resetToInherited": "Auf geerbt zurücksetzen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"sortTip": "Sortieren",
|
|
||||||
"showCompletedTip": "Abgeschlossene anzeigen",
|
"showCompletedTip": "Abgeschlossene anzeigen",
|
||||||
"listSettingsTip": "Listeneinstellungen",
|
"listSettingsTip": "Listeneinstellungen",
|
||||||
"addPlaceholder": "Aufgabe hinzufügen…",
|
"addPlaceholder": "Aufgabe hinzufügen…",
|
||||||
"enterKey": "ENTER",
|
"enterKey": "ENTER",
|
||||||
"notesPinnedRow": "Notizen (Tagesnotizen)",
|
"notesPinnedRow": "Notizen (Tagesnotizen)",
|
||||||
|
"clearDayTip": "Tag leeren",
|
||||||
|
"planMyDayTip": "Meinen Tag planen",
|
||||||
"overdue": "ÜBERFÄLLIG",
|
"overdue": "ÜBERFÄLLIG",
|
||||||
"tasks": "AUFGABEN",
|
"tasks": "AUFGABEN",
|
||||||
"clearCompletedTip": "Alle abgeschlossenen löschen",
|
"clearCompletedTip": "Alle abgeschlossenen löschen",
|
||||||
@@ -97,6 +108,7 @@
|
|||||||
"scheduleWhen": "WANN",
|
"scheduleWhen": "WANN",
|
||||||
"scheduleConfirm": "Planen",
|
"scheduleConfirm": "Planen",
|
||||||
"rejectRerunTitle": "Ablehnen & erneut ausführen",
|
"rejectRerunTitle": "Ablehnen & erneut ausführen",
|
||||||
|
"reviewTitle": "Review",
|
||||||
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
"feedbackLabel": "FEEDBACK FÜR DEN AGENTEN",
|
||||||
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
"feedbackPlaceholder": "Was soll der Agent korrigieren?",
|
||||||
"rerun": "Erneut ausführen"
|
"rerun": "Erneut ausführen"
|
||||||
@@ -123,12 +135,15 @@
|
|||||||
"agentSettingsTip": "Agent-Einstellungen",
|
"agentSettingsTip": "Agent-Einstellungen",
|
||||||
"agentSettingsHeading": "Agent-Einstellungen (Überschreibungen)",
|
"agentSettingsHeading": "Agent-Einstellungen (Überschreibungen)",
|
||||||
"modelLabel": "Modell",
|
"modelLabel": "Modell",
|
||||||
|
"maxTurnsLabel": "Max. Durchläufe",
|
||||||
"systemPromptLabel": "System-Prompt (angehängt)",
|
"systemPromptLabel": "System-Prompt (angehängt)",
|
||||||
|
"systemPromptPrepended": "Wird automatisch vorangestellt:",
|
||||||
"agentFileLabel": "Agent-Datei",
|
"agentFileLabel": "Agent-Datei",
|
||||||
"mergeLabel": "MERGE",
|
"mergeLabel": "MERGE",
|
||||||
"mergeTargetLabel": "Merge-Ziel",
|
"mergeTargetLabel": "Merge-Ziel",
|
||||||
"reviewCombinedDiff": "Kombiniertes Diff prüfen",
|
"reviewCombinedDiff": "Kombiniertes Diff prüfen",
|
||||||
"mergeAllSubtasks": "Alle Teilaufgaben mergen",
|
"mergeAllSubtasks": "Alle Teilaufgaben mergen",
|
||||||
|
"childOutcomesLabel": "VERBESSERUNGEN",
|
||||||
"stepsLabel": "SCHRITTE",
|
"stepsLabel": "SCHRITTE",
|
||||||
"addStepPlaceholder": "Schritt hinzufügen...",
|
"addStepPlaceholder": "Schritt hinzufügen...",
|
||||||
"detailsLabel": "DETAILS",
|
"detailsLabel": "DETAILS",
|
||||||
@@ -136,7 +151,10 @@
|
|||||||
"toggleEditPreviewTip": "Bearbeiten/Vorschau umschalten",
|
"toggleEditPreviewTip": "Bearbeiten/Vorschau umschalten",
|
||||||
"previewBtn": "Vorschau",
|
"previewBtn": "Vorschau",
|
||||||
"editBtn": "Bearbeiten",
|
"editBtn": "Bearbeiten",
|
||||||
"descriptionPlaceholder": "Aufgabendetails hinzufügen (Markdown unterstützt)..."
|
"descriptionPlaceholder": "Aufgabendetails hinzufügen (Markdown unterstützt)...",
|
||||||
|
"prepTitle": "Tagesvorbereitung",
|
||||||
|
"planDay": "Tag planen",
|
||||||
|
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen"
|
||||||
},
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"stopTip": "Agent stoppen",
|
"stopTip": "Agent stoppen",
|
||||||
@@ -195,6 +213,7 @@
|
|||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
|
||||||
"model": "Modell",
|
"model": "Modell",
|
||||||
|
"maxTurns": "Max. Durchläufe",
|
||||||
"systemPrompt": "System-Prompt (angehängt)",
|
"systemPrompt": "System-Prompt (angehängt)",
|
||||||
"agentFile": "Agent-Datei"
|
"agentFile": "Agent-Datei"
|
||||||
},
|
},
|
||||||
@@ -367,8 +386,8 @@
|
|||||||
"vm": {
|
"vm": {
|
||||||
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
|
||||||
"shell": { "restartingWorker": "Worker wird neu gestartet…" },
|
"shell": { "restartingWorker": "Worker wird neu gestartet…" },
|
||||||
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
||||||
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Verbesserungen", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
||||||
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
||||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}" },
|
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}" },
|
||||||
@@ -382,7 +401,6 @@
|
|||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt." },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt." },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen." },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen." },
|
||||||
"listSettings": { "untitled": "Unbenannt" },
|
"listSettings": { "untitled": "Unbenannt" },
|
||||||
"details": { "effectiveIfInherited": "Effektiv bei Vererbung: {0}" },
|
|
||||||
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,17 @@
|
|||||||
"restoreDefaultAgents": "Restore default agents",
|
"restoreDefaultAgents": "Restore default agents",
|
||||||
"promptsSection": "PROMPTS",
|
"promptsSection": "PROMPTS",
|
||||||
"systemPrompt": "System",
|
"systemPrompt": "System",
|
||||||
"planningPrompt": "Planning",
|
"planningPrompt": "Planning (system)",
|
||||||
"agentPrompt": "Agent",
|
"planningInitialPrompt": "Planning kickoff",
|
||||||
|
"retryPrompt": "Retry",
|
||||||
|
"dailyPrepPrompt": "Daily prep",
|
||||||
|
"weeklyReportPrompt": "Weekly report",
|
||||||
"openInEditor": "Open in editor"
|
"openInEditor": "Open in editor"
|
||||||
},
|
},
|
||||||
"prime": {
|
"prime": {
|
||||||
"description": "Prime your Claude usage window by firing a single non-interactive ping on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately.",
|
"description": "Prime your Claude usage window by firing a single non-interactive ping on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately.",
|
||||||
"addSchedule": "+ Add schedule",
|
"addSchedule": "+ Add schedule",
|
||||||
|
"dailyPrepMaxTasks": "Max tasks per day",
|
||||||
"dayMo": "Mo",
|
"dayMo": "Mo",
|
||||||
"dayTu": "Tu",
|
"dayTu": "Tu",
|
||||||
"dayWe": "We",
|
"dayWe": "We",
|
||||||
@@ -57,15 +61,22 @@
|
|||||||
"dayFr": "Fr",
|
"dayFr": "Fr",
|
||||||
"daySa": "Sa",
|
"daySa": "Sa",
|
||||||
"daySu": "Su"
|
"daySu": "Su"
|
||||||
|
},
|
||||||
|
"inherit": {
|
||||||
|
"inheritedFromList": "inherited · List",
|
||||||
|
"inheritedFromGlobal": "inherited · Global",
|
||||||
|
"overrideBadge": "override",
|
||||||
|
"resetToInherited": "Reset to inherited"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"sortTip": "Sort",
|
|
||||||
"showCompletedTip": "Show completed",
|
"showCompletedTip": "Show completed",
|
||||||
"listSettingsTip": "List settings",
|
"listSettingsTip": "List settings",
|
||||||
"addPlaceholder": "Add a task…",
|
"addPlaceholder": "Add a task…",
|
||||||
"enterKey": "ENTER",
|
"enterKey": "ENTER",
|
||||||
"notesPinnedRow": "Notes (daily notes)",
|
"notesPinnedRow": "Notes (daily notes)",
|
||||||
|
"clearDayTip": "Clear day",
|
||||||
|
"planMyDayTip": "Plan My Day",
|
||||||
"overdue": "OVERDUE",
|
"overdue": "OVERDUE",
|
||||||
"tasks": "TASKS",
|
"tasks": "TASKS",
|
||||||
"clearCompletedTip": "Clear all completed",
|
"clearCompletedTip": "Clear all completed",
|
||||||
@@ -97,6 +108,7 @@
|
|||||||
"scheduleWhen": "WHEN",
|
"scheduleWhen": "WHEN",
|
||||||
"scheduleConfirm": "Schedule",
|
"scheduleConfirm": "Schedule",
|
||||||
"rejectRerunTitle": "Reject & re-run",
|
"rejectRerunTitle": "Reject & re-run",
|
||||||
|
"reviewTitle": "Review",
|
||||||
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
"feedbackLabel": "FEEDBACK FOR THE AGENT",
|
||||||
"feedbackPlaceholder": "What should the agent fix?",
|
"feedbackPlaceholder": "What should the agent fix?",
|
||||||
"rerun": "Re-run"
|
"rerun": "Re-run"
|
||||||
@@ -123,12 +135,15 @@
|
|||||||
"agentSettingsTip": "Agent settings",
|
"agentSettingsTip": "Agent settings",
|
||||||
"agentSettingsHeading": "Agent settings (overrides)",
|
"agentSettingsHeading": "Agent settings (overrides)",
|
||||||
"modelLabel": "Model",
|
"modelLabel": "Model",
|
||||||
|
"maxTurnsLabel": "Max turns",
|
||||||
"systemPromptLabel": "System prompt (appended)",
|
"systemPromptLabel": "System prompt (appended)",
|
||||||
|
"systemPromptPrepended": "Prepended automatically:",
|
||||||
"agentFileLabel": "Agent file",
|
"agentFileLabel": "Agent file",
|
||||||
"mergeLabel": "MERGE",
|
"mergeLabel": "MERGE",
|
||||||
"mergeTargetLabel": "Merge target",
|
"mergeTargetLabel": "Merge target",
|
||||||
"reviewCombinedDiff": "Review combined diff",
|
"reviewCombinedDiff": "Review combined diff",
|
||||||
"mergeAllSubtasks": "Merge all subtasks",
|
"mergeAllSubtasks": "Merge all subtasks",
|
||||||
|
"childOutcomesLabel": "IMPROVEMENTS",
|
||||||
"stepsLabel": "STEPS",
|
"stepsLabel": "STEPS",
|
||||||
"addStepPlaceholder": "Add a step...",
|
"addStepPlaceholder": "Add a step...",
|
||||||
"detailsLabel": "DETAILS",
|
"detailsLabel": "DETAILS",
|
||||||
@@ -136,7 +151,10 @@
|
|||||||
"toggleEditPreviewTip": "Toggle edit/preview",
|
"toggleEditPreviewTip": "Toggle edit/preview",
|
||||||
"previewBtn": "Preview",
|
"previewBtn": "Preview",
|
||||||
"editBtn": "Edit",
|
"editBtn": "Edit",
|
||||||
"descriptionPlaceholder": "Add task details (markdown supported)..."
|
"descriptionPlaceholder": "Add task details (markdown supported)...",
|
||||||
|
"prepTitle": "Daily prep",
|
||||||
|
"planDay": "Plan day",
|
||||||
|
"prepEmpty": "No prep run today yet — click Plan day"
|
||||||
},
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"stopTip": "Stop agent",
|
"stopTip": "Stop agent",
|
||||||
@@ -195,6 +213,7 @@
|
|||||||
"sectionAgent": "AGENT",
|
"sectionAgent": "AGENT",
|
||||||
"resetAgentSettings": "Reset agent settings",
|
"resetAgentSettings": "Reset agent settings",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
|
"maxTurns": "Max turns",
|
||||||
"systemPrompt": "System prompt (appended)",
|
"systemPrompt": "System prompt (appended)",
|
||||||
"agentFile": "Agent file"
|
"agentFile": "Agent file"
|
||||||
},
|
},
|
||||||
@@ -367,8 +386,8 @@
|
|||||||
"vm": {
|
"vm": {
|
||||||
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
|
||||||
"shell": { "restartingWorker": "Restarting worker…" },
|
"shell": { "restartingWorker": "Restarting worker…" },
|
||||||
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
||||||
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Improvements", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
||||||
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
||||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" },
|
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" },
|
||||||
@@ -382,7 +401,6 @@
|
|||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed." },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed." },
|
||||||
"listSettings": { "untitled": "Untitled" },
|
"listSettings": { "untitled": "Untitled" },
|
||||||
"details": { "effectiveIfInherited": "Effective if inherited: {0}" },
|
|
||||||
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/ClaudeDo.Logging/BuildConfig.cs
Normal file
14
src/ClaudeDo.Logging/BuildConfig.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
/// <summary>Runtime build-configuration detection — the replacement for #if DEBUG.
|
||||||
|
/// Debug builds compile with the JIT optimizer disabled; Release builds enable it.</summary>
|
||||||
|
public static class BuildConfig
|
||||||
|
{
|
||||||
|
public static bool IsDebug { get; } =
|
||||||
|
Assembly.GetEntryAssembly()
|
||||||
|
?.GetCustomAttribute<DebuggableAttribute>()
|
||||||
|
?.IsJITOptimizerDisabled ?? false;
|
||||||
|
}
|
||||||
19
src/ClaudeDo.Logging/ClaudeDo.Logging.csproj
Normal file
19
src/ClaudeDo.Logging/ClaudeDo.Logging.csproj
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
15
src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs
Normal file
15
src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
/// <summary>Ensures every log event carries a TaskId property (defaulting to "-")
|
||||||
|
/// so the output template's [{TaskId}] column never renders the raw token.</summary>
|
||||||
|
public sealed class DefaultTaskIdEnricher : ILogEventEnricher
|
||||||
|
{
|
||||||
|
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||||
|
{
|
||||||
|
if (!logEvent.Properties.ContainsKey("TaskId"))
|
||||||
|
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-"));
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/ClaudeDo.Logging/LoggingSetup.cs
Normal file
44
src/ClaudeDo.Logging/LoggingSetup.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Logging;
|
||||||
|
|
||||||
|
public static class LoggingSetup
|
||||||
|
{
|
||||||
|
private const string OutputTemplate =
|
||||||
|
"[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}";
|
||||||
|
|
||||||
|
public static LoggerConfiguration Configure(LoggerConfiguration cfg, string processTag, string logRoot)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(logRoot);
|
||||||
|
var logFile = Path.Combine(logRoot, "claudedo-.log");
|
||||||
|
|
||||||
|
cfg.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithProperty("Process", processTag)
|
||||||
|
.Enrich.With(new DefaultTaskIdEnricher());
|
||||||
|
|
||||||
|
if (BuildConfig.IsDebug)
|
||||||
|
{
|
||||||
|
cfg.MinimumLevel.Debug()
|
||||||
|
.WriteTo.Console(outputTemplate: OutputTemplate)
|
||||||
|
.WriteTo.File(
|
||||||
|
logFile,
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 2,
|
||||||
|
shared: true,
|
||||||
|
outputTemplate: OutputTemplate);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cfg.MinimumLevel.Warning()
|
||||||
|
.WriteTo.File(
|
||||||
|
logFile,
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 2,
|
||||||
|
shared: true,
|
||||||
|
outputTemplate: OutputTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,14 @@ MVVM with CommunityToolkit.Mvvm source generators:
|
|||||||
- **TaskEditorView** — Modal dialog for task create/edit
|
- **TaskEditorView** — Modal dialog for task create/edit
|
||||||
- **ListEditorView** — Modal dialog for list create/edit
|
- **ListEditorView** — Modal dialog for list create/edit
|
||||||
- **StatusBarView** — Connection status indicator, active task display
|
- **StatusBarView** — Connection status indicator, active task display
|
||||||
- **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath; also deletes the list (and its tasks) via a confirmed "Delete list" button. Opened via context menu or gear button on a list row.
|
- **ListSettingsModalView** — edits list name, working dir, default commit type, and per-list Model/SystemPrompt/AgentPath/MaxTurns, each showing the inherited (global) value with a source-aware "inherited · Global / override" badge and a reset-to-inherited button; also deletes the list (and its tasks) via a confirmed "Delete list" button. Opened via context menu or gear button on a list row.
|
||||||
- **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)".
|
- **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)".
|
||||||
- **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/SystemPrompt/AgentPath, showing inherited effective values. Disabled while task is running. When notes mode is active (`IsNotesMode`), it hosts **NotesEditorView** instead of the task detail.
|
- **DetailsIslandView** — contains an "Agent settings (overrides)" expander with per-task Model/MaxTurns/AgentPath (override semantics, each showing the resolved inherited value as a placeholder plus a source-aware "inherited · List / inherited · Global / override" badge and reset button via the reusable `InheritedBadge` control + `InheritanceResolver`) and a SystemPrompt text box (additive — shows the inherited prompt as a "prepended automatically" note). Disabled while task is running. When notes mode is active (`IsNotesMode`), it hosts **NotesEditorView** instead of the task detail. When prep mode is active (`IsPrepMode`), it hosts the daily-prep panel (Plan day button, empty-state hint, embedded **SessionTerminalView**). The task header, metadata footer (delete/close), and **AgentStripView** are gated on `IsTaskDetailVisible` — they are hidden in both notes and prep mode.
|
||||||
- **WeeklyReportModalView** — opened from Help menu ("Wochenbericht…"); date-range pickers default to "since last standup weekday → today"; Generate/Regenerate button; renders markdown via MarkdownView; reports are cached per range.
|
- **WeeklyReportModalView** — opened from Help menu ("Wochenbericht…"); date-range pickers default to "since last standup weekday → today"; Generate/Regenerate button; renders markdown via MarkdownView; reports are cached per range.
|
||||||
- **NotesEditorView** — day navigator (prev/next/date-picker/Today), bullet add/edit/delete for daily notes.
|
- **NotesEditorView** — day navigator (prev/next/date-picker/Today), bullet add/edit/delete for daily notes.
|
||||||
|
- **SessionTerminalView** — reusable log terminal; exposes StyledProperties `Entries`, `Label`, `IsRunning`, `IsDone`, `IsFailed`. Used for both the task `Log` and the prep `PrepLog`.
|
||||||
|
- **SettingsModalView** — Prime Claude tab contains a `DailyPrepMaxTasks` numeric editor.
|
||||||
|
- **TasksIslandView** (MyDay header) — icon buttons visible only when `IsMyDayList`: broom icon = `ClearDayCommand`, stroked-sun icon ("Plan My Day") = `ShowPrepLogCommand`.
|
||||||
|
|
||||||
All views use compiled bindings (`x:DataType`).
|
All views use compiled bindings (`x:DataType`).
|
||||||
|
|
||||||
@@ -35,12 +38,12 @@ All views use compiled bindings (`x:DataType`).
|
|||||||
- **StatusBarViewModel** — connection state and active tasks
|
- **StatusBarViewModel** — connection state and active tasks
|
||||||
- **WeeklyReportModalViewModel** — drives the weekly report modal
|
- **WeeklyReportModalViewModel** — drives the weekly report modal
|
||||||
- **NotesEditorViewModel** — manages daily note bullet CRUD for the selected day
|
- **NotesEditorViewModel** — manages daily note bullet CRUD for the selected day
|
||||||
- **DetailsIslandViewModel** gains `IsNotesMode`, `ShowNotes()`, and hosts `NotesEditorViewModel`
|
- **DetailsIslandViewModel** gains `IsNotesMode`, `ShowNotes()`, and hosts `NotesEditorViewModel`. Also gains daily-prep mode: `IsPrepMode`, `PrepLog` (`ObservableCollection<LogLineViewModel>`), `ShowPrep()`, `PlanDayCommand` (calls `RunDailyPrepNowAsync`), `ShowPrepEmptyState`, and computed `IsTaskDetailVisible` (= `!IsNotesMode && !IsPrepMode`). Subscribes to `PrepStartedEvent`, `PrepLineEvent`, `PrepFinishedEvent`; streams lines into `PrepLog` via `StreamLineFormatter` (same path as task logs). On open, if log is empty and no run is in progress, loads persisted last run via `GetLastPrepLogAsync`.
|
||||||
- **TasksIslandViewModel** gains a pinned "Notes" pseudo-row (`ShowNotesRow`, `OpenNotesCommand`, `NotesRequested` event) that switches the Details island to notes mode
|
- **TasksIslandViewModel** gains a pinned "Notes" pseudo-row (`ShowNotesRow`, `OpenNotesCommand`, `NotesRequested` event) that switches the Details island to notes mode. Also gains `IsMyDayList` (true when selected list is `smart:my-day`), `ShowPrepLogCommand` (raises `PrepRequested` event → shell calls `Details.ShowPrep()`), and `ClearDayCommand` (calls `ClearMyDayAsync`).
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync, ContinueTaskAsync, ResetTaskAsync, GetAgentsAsync, UpdateAppSettingsAsync, UpdateListAsync, UpdateListConfigAsync, GetListConfigAsync, UpdateTaskAgentSettingsAsync, `GetWeekReportAsync`, `GenerateWeekReportAsync`, `GetDailyNotesAsync`, `AddDailyNoteAsync`, `UpdateDailyNoteAsync`, `DeleteDailyNoteAsync`. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated, ListUpdated
|
- **WorkerClient** / **IWorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync, ContinueTaskAsync, ResetTaskAsync, GetAgentsAsync, UpdateAppSettingsAsync, UpdateListAsync, UpdateListConfigAsync, GetListConfigAsync, UpdateTaskAgentSettingsAsync, `GetWeekReportAsync`, `GenerateWeekReportAsync`, `GetDailyNotesAsync`, `AddDailyNoteAsync`, `UpdateDailyNoteAsync`, `DeleteDailyNoteAsync`, `RunDailyPrepNowAsync`, `ClearMyDayAsync`, `GetLastPrepLogAsync`. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated, ListUpdated, `PrepStartedEvent`, `PrepLineEvent`, `PrepFinishedEvent`
|
||||||
- **INotesApi** / **WorkerNotesApi** — thin wrapper over the daily-note WorkerClient methods (mirrors `IPrimeScheduleApi`). UI DTO: `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
- **INotesApi** / **WorkerNotesApi** — thin wrapper over the daily-note WorkerClient methods (mirrors `IPrimeScheduleApi`). UI DTO: `DailyNoteDto(Id, Date, Text, SortOrder)`.
|
||||||
|
|
||||||
## Converters
|
## Converters
|
||||||
@@ -57,3 +60,4 @@ Editor dialogs use `TaskCompletionSource<bool>` — the dialog sets the result o
|
|||||||
- Context menus are on both list items and task items
|
- Context menus are on both list items and task items
|
||||||
- Right-click selects the item before showing the context menu
|
- Right-click selects the item before showing the context menu
|
||||||
- "Run Now" CanExecute re-evaluates when worker connection state changes
|
- "Run Now" CanExecute re-evaluates when worker connection state changes
|
||||||
|
- Icon gotcha: `PathIcon` fills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style); a pure stroke path used with `PathIcon` is invisible.
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class StatusColorConverter : IValueConverter
|
|||||||
"running" => Brushes.Orange,
|
"running" => Brushes.Orange,
|
||||||
"waitingforreview" => Brushes.MediumPurple,
|
"waitingforreview" => Brushes.MediumPurple,
|
||||||
"waiting_for_review" => Brushes.MediumPurple,
|
"waiting_for_review" => Brushes.MediumPurple,
|
||||||
|
"waitingforchildren" => Brushes.DarkOrange,
|
||||||
"done" => Brushes.Green,
|
"done" => Brushes.Green,
|
||||||
"failed" => Brushes.Red,
|
"failed" => Brushes.Red,
|
||||||
"manual" => Brushes.Gray,
|
"manual" => Brushes.Gray,
|
||||||
|
|||||||
@@ -70,8 +70,11 @@
|
|||||||
<!-- Icon.Trash -->
|
<!-- Icon.Trash -->
|
||||||
<StreamGeometry x:Key="Icon.Trash">M4 7h16M10 11v6M14 11v6M5 7l1 13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1l1-13M9 7V4h6v3</StreamGeometry>
|
<StreamGeometry x:Key="Icon.Trash">M4 7h16M10 11v6M14 11v6M5 7l1 13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1l1-13M9 7V4h6v3</StreamGeometry>
|
||||||
|
|
||||||
<!-- Icon.Sort -->
|
<!-- Icon.Broom (filled: handle + binding band + flared bristles) -->
|
||||||
<StreamGeometry x:Key="Icon.Sort">M7 4v16M7 20l-3-3M7 4l-3 3M14 8h7M14 12h5M14 16h3</StreamGeometry>
|
<StreamGeometry x:Key="Icon.Broom">M11 3 H13 V10 H11 Z M8.5 10 H15.5 V12 H8.5 Z M9 12 H15 L17 21 H7 Z</StreamGeometry>
|
||||||
|
|
||||||
|
<!-- Icon.PlanDay (stroke-rendered via Path.plan-icon — sun over horizon) -->
|
||||||
|
<StreamGeometry x:Key="Icon.PlanDay">M3,20 L21,20 M8.4,11 a3.6,3.6 0 1,0 7.2,0 a3.6,3.6 0 1,0 -7.2,0 M12,4.5 L12,3 M6,11 L4.5,11 M18,11 L19.5,11 M7.5,6.5 L6.4,5.4 M16.5,6.5 L17.6,5.4</StreamGeometry>
|
||||||
|
|
||||||
<!-- Icon.X -->
|
<!-- Icon.X -->
|
||||||
<StreamGeometry x:Key="Icon.X">M6 6l12 12M18 6L6 18</StreamGeometry>
|
<StreamGeometry x:Key="Icon.X">M6 6l12 12M18 6L6 18</StreamGeometry>
|
||||||
@@ -82,6 +85,12 @@
|
|||||||
<!-- Icon.ArrowOut — filled arrow for "open external" button -->
|
<!-- Icon.ArrowOut — filled arrow for "open external" button -->
|
||||||
<StreamGeometry x:Key="Icon.ArrowOut">M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z</StreamGeometry>
|
<StreamGeometry x:Key="Icon.ArrowOut">M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z</StreamGeometry>
|
||||||
|
|
||||||
|
<!-- Icon.Warning — filled triangle with exclamation (roadblock badge) -->
|
||||||
|
<StreamGeometry x:Key="Icon.Warning">F0 M12 3 L22 20 H2 Z M11 9 H13 V14 H11 Z M11 16 H13 V18 H11 Z</StreamGeometry>
|
||||||
|
|
||||||
|
<!-- Icon.AgentSuggested — filled diamond (agent-suggested child badge) -->
|
||||||
|
<StreamGeometry x:Key="Icon.AgentSuggested">M12 3 L20 12 L12 21 L4 12 Z</StreamGeometry>
|
||||||
|
|
||||||
<!-- Icon.Settings (gear) -->
|
<!-- Icon.Settings (gear) -->
|
||||||
<StreamGeometry x:Key="Icon.Settings">M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z</StreamGeometry>
|
<StreamGeometry x:Key="Icon.Settings">M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z</StreamGeometry>
|
||||||
|
|
||||||
@@ -168,6 +177,14 @@
|
|||||||
<Setter Property="Foreground" Value="{StaticResource StatusReviewBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource StatusReviewBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.chip.children">
|
||||||
|
<Setter Property="Background" Value="#332A1A" />
|
||||||
|
<Setter Property="BorderBrush" Value="#4D3A1A" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.chip.children > TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#E0A030" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.chip.error">
|
<Style Selector="Border.chip.error">
|
||||||
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
|
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
|
||||||
@@ -244,6 +261,17 @@
|
|||||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Stroke-rendered icon (for line-art geometries that PathIcon would fill away) -->
|
||||||
|
<Style Selector="Button.icon-btn Path.plan-icon">
|
||||||
|
<Setter Property="Stroke" Value="{StaticResource TextBrush}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="1.7" />
|
||||||
|
<Setter Property="StrokeLineCap" Value="Round" />
|
||||||
|
<Setter Property="StrokeJoin" Value="Round" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-btn:pointerover Path.plan-icon">
|
||||||
|
<Setter Property="Stroke" Value="{StaticResource AccentBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- INPUTS -->
|
<!-- INPUTS -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
@@ -371,6 +399,10 @@
|
|||||||
<Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
|
<Setter Property="Background" Value="{StaticResource ReviewTintBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource ReviewTintBorderBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="Border.agent-strip.children">
|
||||||
|
<Setter Property="Background" Value="#332A1A" />
|
||||||
|
<Setter Property="BorderBrush" Value="#4D3A1A" />
|
||||||
|
</Style>
|
||||||
<Style Selector="Border.agent-strip.error">
|
<Style Selector="Border.agent-strip.error">
|
||||||
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
|
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{StaticResource ErrorTintBorderBrush}" />
|
||||||
|
|||||||
34
src/ClaudeDo.Ui/Services/FocusClearing.cs
Normal file
34
src/ClaudeDo.Ui/Services/FocusClearing.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears focus from a TextBox when the user clicks outside of any text box, so input
|
||||||
|
/// fields behave like the user expects. Registered once for every window in the app.
|
||||||
|
/// </summary>
|
||||||
|
public static class FocusClearing
|
||||||
|
{
|
||||||
|
public static void Install()
|
||||||
|
{
|
||||||
|
InputElement.PointerPressedEvent.AddClassHandler<TopLevel>(
|
||||||
|
OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnPointerPressed(TopLevel topLevel, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (topLevel.FocusManager is not { } focusManager)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (focusManager.GetFocusedElement() is not TextBox)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (e.Source is Visual v && v.FindAncestorOfType<TextBox>(includeSelf: true) is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
focusManager.Focus(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/ClaudeDo.Ui/Services/InheritanceResolver.cs
Normal file
23
src/ClaudeDo.Ui/Services/InheritanceResolver.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
public enum InheritSource { Override, List, Global }
|
||||||
|
|
||||||
|
public static class InheritanceResolver
|
||||||
|
{
|
||||||
|
// Task-scope fields: task -> list -> global.
|
||||||
|
public static (string Value, InheritSource Source) Resolve(
|
||||||
|
string? taskValue, string? listValue, string? globalValue)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(taskValue)) return (taskValue!, InheritSource.Override);
|
||||||
|
if (!string.IsNullOrWhiteSpace(listValue)) return (listValue!, InheritSource.List);
|
||||||
|
return (globalValue ?? "", InheritSource.Global);
|
||||||
|
}
|
||||||
|
|
||||||
|
// List-scope fields: list -> global (lists have no tier above them).
|
||||||
|
public static (string Value, InheritSource Source) ResolveList(
|
||||||
|
string? listValue, string? globalValue)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(listValue)) return (listValue!, InheritSource.Override);
|
||||||
|
return (globalValue ?? "", InheritSource.Global);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
event Action<string>? ListUpdatedEvent;
|
event Action<string>? ListUpdatedEvent;
|
||||||
event Action<string, string>? TaskMessageEvent;
|
event Action<string, string>? TaskMessageEvent;
|
||||||
|
|
||||||
|
event Action? PrepStartedEvent;
|
||||||
|
event Action<string>? PrepLineEvent;
|
||||||
|
event Action<bool>? PrepFinishedEvent;
|
||||||
|
|
||||||
event Action<string, string>? PlanningMergeStartedEvent;
|
event Action<string, string>? PlanningMergeStartedEvent;
|
||||||
event Action<string, string>? PlanningSubtaskMergedEvent;
|
event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||||
event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
||||||
@@ -52,9 +56,12 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
||||||
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
|
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
|
||||||
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
|
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
|
||||||
|
Task<bool> RunDailyPrepNowAsync();
|
||||||
|
Task ClearMyDayAsync();
|
||||||
Task<AppSettingsDto?> GetAppSettingsAsync();
|
Task<AppSettingsDto?> GetAppSettingsAsync();
|
||||||
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
|
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
|
||||||
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
|
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
|
||||||
Task UpdateDailyNoteAsync(string id, string text);
|
Task UpdateDailyNoteAsync(string id, string text);
|
||||||
Task DeleteDailyNoteAsync(string id);
|
Task DeleteDailyNoteAsync(string id);
|
||||||
|
Task<string> GetLastPrepLogAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog.Context;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ sealed class IndefiniteRetryPolicy : IRetryPolicy
|
|||||||
public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient
|
public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient
|
||||||
{
|
{
|
||||||
private readonly HubConnection _hub;
|
private readonly HubConnection _hub;
|
||||||
|
private readonly ILogger<WorkerClient> _logger;
|
||||||
private CancellationTokenSource? _startCts;
|
private CancellationTokenSource? _startCts;
|
||||||
private Task _retryLoopTask = Task.CompletedTask;
|
private Task _retryLoopTask = Task.CompletedTask;
|
||||||
private readonly object _startLock = new();
|
private readonly object _startLock = new();
|
||||||
@@ -51,6 +54,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public event Action<string>? ListUpdatedEvent;
|
public event Action<string>? ListUpdatedEvent;
|
||||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||||
|
|
||||||
|
public event Action? PrepStartedEvent;
|
||||||
|
public event Action<string>? PrepLineEvent;
|
||||||
|
public event Action<bool>? PrepFinishedEvent;
|
||||||
|
|
||||||
public event Action<string, string>? PlanningMergeStartedEvent;
|
public event Action<string, string>? PlanningMergeStartedEvent;
|
||||||
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||||
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
public event Action<string, string, IReadOnlyList<string>>? PlanningMergeConflictEvent;
|
||||||
@@ -61,8 +68,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
|
|
||||||
public string? LastMergeAllTarget { get; private set; }
|
public string? LastMergeAllTarget { get; private set; }
|
||||||
|
|
||||||
public WorkerClient(string signalRUrl)
|
public WorkerClient(string signalRUrl, ILogger<WorkerClient> logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_hub = new HubConnectionBuilder()
|
_hub = new HubConnectionBuilder()
|
||||||
.WithUrl(signalRUrl)
|
.WithUrl(signalRUrl)
|
||||||
.WithAutomaticReconnect(new IndefiniteRetryPolicy())
|
.WithAutomaticReconnect(new IndefiniteRetryPolicy())
|
||||||
@@ -171,6 +179,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() => PrimeFired?.Invoke(new PrimeFiredEvent(id, ok, msg, when)));
|
Dispatcher.UIThread.Post(() => PrimeFired?.Invoke(new PrimeFiredEvent(id, ok, msg, when)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
|
||||||
|
_hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
|
||||||
|
_hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync()
|
public Task StartAsync()
|
||||||
@@ -232,20 +244,24 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
catch { return default; }
|
catch { return default; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RunNowAsync(string taskId)
|
/// <summary>Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line.</summary>
|
||||||
|
private async Task InvokeForTaskAsync(string taskId, string method, params object?[] args)
|
||||||
{
|
{
|
||||||
await _hub.InvokeAsync("RunNow", taskId);
|
using (LogContext.PushProperty("TaskId", taskId))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("UI invoking {Method} for task {TaskId}", method, taskId);
|
||||||
|
await _hub.InvokeCoreAsync(method, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ContinueTaskAsync(string taskId, string followUpPrompt)
|
public Task RunNowAsync(string taskId)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "RunNow", taskId);
|
||||||
await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ResetTaskAsync(string taskId)
|
public Task ContinueTaskAsync(string taskId, string followUpPrompt)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt);
|
||||||
await _hub.InvokeAsync("ResetTask", taskId);
|
|
||||||
}
|
public Task ResetTaskAsync(string taskId)
|
||||||
|
=> InvokeForTaskAsync(taskId, "ResetTask", taskId);
|
||||||
|
|
||||||
public async Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage)
|
public async Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage)
|
||||||
{
|
{
|
||||||
@@ -256,10 +272,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
|
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
|
||||||
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
|
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
|
||||||
|
|
||||||
public async Task CancelTaskAsync(string taskId)
|
public Task CancelTaskAsync(string taskId)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "CancelTask", taskId);
|
||||||
await _hub.InvokeAsync("CancelTask", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WakeQueueAsync()
|
public async Task WakeQueueAsync()
|
||||||
{
|
{
|
||||||
@@ -334,6 +348,12 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end)
|
public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end)
|
||||||
=> _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end));
|
=> _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end));
|
||||||
|
|
||||||
|
public Task<bool> RunDailyPrepNowAsync()
|
||||||
|
=> _hub.InvokeAsync<bool>("RunDailyPrepNow");
|
||||||
|
|
||||||
|
public Task ClearMyDayAsync()
|
||||||
|
=> _hub.InvokeAsync("ClearMyDay");
|
||||||
|
|
||||||
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
||||||
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
||||||
|
|
||||||
@@ -346,6 +366,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task DeleteDailyNoteAsync(string id)
|
public async Task DeleteDailyNoteAsync(string id)
|
||||||
=> await _hub.InvokeAsync("DeleteDailyNote", id);
|
=> await _hub.InvokeAsync("DeleteDailyNote", id);
|
||||||
|
|
||||||
|
public async Task<string> GetLastPrepLogAsync()
|
||||||
|
=> await TryInvokeAsync<string>("GetLastPrepLog") ?? string.Empty;
|
||||||
|
|
||||||
public async Task UpdateListAsync(UpdateListDto dto)
|
public async Task UpdateListAsync(UpdateListDto dto)
|
||||||
{
|
{
|
||||||
await _hub.InvokeAsync("UpdateList", dto);
|
await _hub.InvokeAsync("UpdateList", dto);
|
||||||
@@ -369,25 +392,17 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ApproveReviewAsync(string taskId)
|
public Task ApproveReviewAsync(string taskId)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "ApproveReview", taskId);
|
||||||
await _hub.InvokeAsync("ApproveReview", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RejectReviewToQueueAsync(string taskId, string feedback)
|
public Task RejectReviewToQueueAsync(string taskId, string feedback)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback);
|
||||||
await _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RejectReviewToIdleAsync(string taskId)
|
public Task RejectReviewToIdleAsync(string taskId)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId);
|
||||||
await _hub.InvokeAsync("RejectReviewToIdle", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CancelReviewAsync(string taskId)
|
public Task CancelReviewAsync(string taskId)
|
||||||
{
|
=> InvokeForTaskAsync(taskId, "CancelReview", taskId);
|
||||||
await _hub.InvokeAsync("CancelReview", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||||
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||||
@@ -496,16 +511,17 @@ public sealed record AppSettingsDto(
|
|||||||
bool WorktreeAutoCleanupEnabled,
|
bool WorktreeAutoCleanupEnabled,
|
||||||
int WorktreeAutoCleanupDays,
|
int WorktreeAutoCleanupDays,
|
||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday);
|
int StandupWeekday,
|
||||||
|
int DailyPrepMaxTasks);
|
||||||
|
|
||||||
public sealed record WorktreeCleanupDto(int Removed);
|
public sealed record WorktreeCleanupDto(int Removed);
|
||||||
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
|
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
|
||||||
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
||||||
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
||||||
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath);
|
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath);
|
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath);
|
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public sealed record SeedResultDto(int Copied, int Skipped);
|
public sealed record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public sealed record WorktreeOverviewDto(
|
public sealed record WorktreeOverviewDto(
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
private readonly INotesApi _notesApi;
|
private readonly INotesApi _notesApi;
|
||||||
|
|
||||||
[ObservableProperty] private bool _isNotesMode;
|
[ObservableProperty] private bool _isNotesMode;
|
||||||
|
[ObservableProperty] private bool _isPrepMode;
|
||||||
|
[ObservableProperty] private bool _isPrepRunning;
|
||||||
|
public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();
|
||||||
|
public bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;
|
||||||
|
|
||||||
|
partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||||
|
partial void OnIsPrepModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||||
|
|
||||||
public NotesEditorViewModel Notes { get; private set; } = null!;
|
public NotesEditorViewModel Notes { get; private set; } = null!;
|
||||||
|
|
||||||
// Current task row (set by IslandsShellViewModel via Bind)
|
// Current task row (set by IslandsShellViewModel via Bind)
|
||||||
@@ -102,12 +110,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
|
||||||
private string _agentState = "idle";
|
private string _agentState = "idle";
|
||||||
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
|
||||||
public bool IsIdle => AgentState == "idle";
|
public bool IsIdle => AgentState == "idle";
|
||||||
public bool IsQueued => AgentState == "queued";
|
public bool IsQueued => AgentState == "queued";
|
||||||
public bool IsRunning => AgentState == "running";
|
public bool IsRunning => AgentState == "running";
|
||||||
public bool IsDone => AgentState == "done";
|
public bool IsWaitingForReview => AgentState == "review";
|
||||||
public bool IsFailed => AgentState == "failed";
|
public bool IsWaitingForChildren => AgentState == "children";
|
||||||
public bool IsCancelled => AgentState == "cancelled";
|
public bool IsDone => AgentState == "done";
|
||||||
|
public bool IsFailed => AgentState == "failed";
|
||||||
|
public bool IsCancelled => AgentState == "cancelled";
|
||||||
|
|
||||||
// Recovery actions: Continue (resume session) for Failed/Cancelled.
|
// Recovery actions: Continue (resume session) for Failed/Cancelled.
|
||||||
public bool ShowContinue => IsFailed || IsCancelled;
|
public bool ShowContinue => IsFailed || IsCancelled;
|
||||||
@@ -124,34 +134,68 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(IsIdle));
|
OnPropertyChanged(nameof(IsIdle));
|
||||||
OnPropertyChanged(nameof(IsQueued));
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
OnPropertyChanged(nameof(IsRunning));
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForReview));
|
||||||
|
OnPropertyChanged(nameof(IsWaitingForChildren));
|
||||||
OnPropertyChanged(nameof(IsDone));
|
OnPropertyChanged(nameof(IsDone));
|
||||||
OnPropertyChanged(nameof(IsFailed));
|
OnPropertyChanged(nameof(IsFailed));
|
||||||
OnPropertyChanged(nameof(IsCancelled));
|
OnPropertyChanged(nameof(IsCancelled));
|
||||||
OnPropertyChanged(nameof(ShowContinue));
|
OnPropertyChanged(nameof(ShowContinue));
|
||||||
OnPropertyChanged(nameof(ShowResetAndRetry));
|
OnPropertyChanged(nameof(ShowResetAndRetry));
|
||||||
OnPropertyChanged(nameof(IsAgentSectionEnabled));
|
OnPropertyChanged(nameof(IsAgentSectionEnabled));
|
||||||
OnPropertyChanged(nameof(EffectiveModelLabel));
|
|
||||||
OnPropertyChanged(nameof(EffectiveAgentLabel));
|
|
||||||
}
|
}
|
||||||
[ObservableProperty] private string? _model;
|
[ObservableProperty] private string? _model;
|
||||||
|
|
||||||
// Agent settings overrides
|
// Agent settings overrides
|
||||||
[ObservableProperty] private string _taskModelSelection = ModelRegistry.TaskInheritSentinel;
|
[ObservableProperty] private string? _taskModelSelection; // null = inherit
|
||||||
[ObservableProperty] private string _taskSystemPrompt = "";
|
[ObservableProperty] private string _taskSystemPrompt = "";
|
||||||
[ObservableProperty] private AgentInfo? _taskSelectedAgent;
|
[ObservableProperty] private AgentInfo? _taskSelectedAgent;
|
||||||
|
[ObservableProperty] private decimal? _taskMaxTurns; // null = inherit
|
||||||
[ObservableProperty] private string _effectiveModelHint = "";
|
[ObservableProperty] private string _modelBadge = "";
|
||||||
|
[ObservableProperty] private string _modelInheritedHint = "";
|
||||||
|
[ObservableProperty] private string _turnsBadge = "";
|
||||||
|
[ObservableProperty] private string _turnsInheritedHint = "";
|
||||||
|
[ObservableProperty] private string _agentBadge = "";
|
||||||
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
||||||
[ObservableProperty] private string _effectiveAgentHint = "";
|
|
||||||
|
|
||||||
public string EffectiveModelLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveModelHint);
|
private string _globalModel = ModelRegistry.DefaultAlias;
|
||||||
public string EffectiveAgentLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveAgentHint);
|
private int _globalMaxTurns = 100;
|
||||||
|
private string? _listModel;
|
||||||
|
private int? _listMaxTurns;
|
||||||
|
private string? _listAgentName;
|
||||||
|
|
||||||
partial void OnEffectiveModelHintChanged(string value) => OnPropertyChanged(nameof(EffectiveModelLabel));
|
partial void OnTaskModelSelectionChanged(string? value) { RecomputeModelBadge(); QueueAgentSave(); }
|
||||||
partial void OnEffectiveAgentHintChanged(string value) => OnPropertyChanged(nameof(EffectiveAgentLabel));
|
partial void OnTaskMaxTurnsChanged(decimal? value) { RecomputeTurnsBadge(); QueueAgentSave(); }
|
||||||
|
|
||||||
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(
|
private void RecomputeModelBadge()
|
||||||
new[] { ModelRegistry.TaskInheritSentinel }.Concat(ModelRegistry.Aliases));
|
{
|
||||||
|
var (value, source) = InheritanceResolver.Resolve(TaskModelSelection, _listModel, _globalModel);
|
||||||
|
ModelInheritedHint = value;
|
||||||
|
ModelBadge = BadgeFor(source, !string.IsNullOrWhiteSpace(TaskModelSelection));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeTurnsBadge()
|
||||||
|
{
|
||||||
|
var (value, source) = InheritanceResolver.Resolve(
|
||||||
|
TaskMaxTurns?.ToString(), _listMaxTurns?.ToString(), _globalMaxTurns.ToString());
|
||||||
|
TurnsInheritedHint = value;
|
||||||
|
TurnsBadge = BadgeFor(source, TaskMaxTurns is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeAgentBadge()
|
||||||
|
{
|
||||||
|
var taskSet = TaskSelectedAgent is not null && !string.IsNullOrWhiteSpace(TaskSelectedAgent.Path);
|
||||||
|
var (_, source) = InheritanceResolver.Resolve(
|
||||||
|
taskSet ? TaskSelectedAgent!.Path : null, _listAgentName, null);
|
||||||
|
AgentBadge = BadgeFor(source, taskSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BadgeFor(InheritSource source, bool taskSet) => taskSet
|
||||||
|
? Loc.T("settings.inherit.overrideBadge")
|
||||||
|
: source == InheritSource.List
|
||||||
|
? Loc.T("settings.inherit.inheritedFromList")
|
||||||
|
: Loc.T("settings.inherit.inheritedFromGlobal");
|
||||||
|
|
||||||
|
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(ModelRegistry.Aliases);
|
||||||
|
|
||||||
public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();
|
public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();
|
||||||
|
|
||||||
@@ -193,6 +237,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
public ObservableCollection<LogLineViewModel> Log { get; } = new();
|
||||||
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
|
||||||
|
|
||||||
|
// Agent-suggested improvement children of a non-planning parent, surfaced on its
|
||||||
|
// review card with each child's outcome and rolled-up roadblock count.
|
||||||
|
public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();
|
||||||
|
public bool HasChildOutcomes => ChildOutcomes.Count > 0;
|
||||||
|
|
||||||
[ObservableProperty] private string _newSubtaskTitle = "";
|
[ObservableProperty] private string _newSubtaskTitle = "";
|
||||||
|
|
||||||
// Planning merge controls
|
// Planning merge controls
|
||||||
@@ -207,6 +256,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
// Claude CLI stream-json parser + buffer for partial text deltas
|
// Claude CLI stream-json parser + buffer for partial text deltas
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
private readonly StringBuilder _claudeBuf = new();
|
private readonly StringBuilder _claudeBuf = new();
|
||||||
|
private readonly StringBuilder _prepClaudeBuf = new();
|
||||||
|
|
||||||
// The task ID we are currently subscribed to for live log messages
|
// The task ID we are currently subscribed to for live log messages
|
||||||
private string? _subscribedTaskId;
|
private string? _subscribedTaskId;
|
||||||
@@ -238,7 +288,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
|
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Running => "running",
|
ClaudeDo.Data.Models.TaskStatus.Running => "running",
|
||||||
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "running",
|
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "review",
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.WaitingForChildren => "children",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Done => "done",
|
ClaudeDo.Data.Models.TaskStatus.Done => "done",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
|
ClaudeDo.Data.Models.TaskStatus.Failed => "failed",
|
||||||
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
|
ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled",
|
||||||
@@ -250,7 +301,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
"done" => "done",
|
"done" => "done",
|
||||||
"failed" => "failed",
|
"failed" => "failed",
|
||||||
"cancelled" => "cancelled",
|
"cancelled" => "cancelled",
|
||||||
"waiting_for_review" => "running",
|
"waiting_for_review" => "review",
|
||||||
|
"waiting_for_children" => "children",
|
||||||
_ => status.ToLowerInvariant(),
|
_ => status.ToLowerInvariant(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,12 +331,16 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
Loc.LanguageChanged += (_, _) =>
|
Loc.LanguageChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(AgentStatusLabel));
|
OnPropertyChanged(nameof(AgentStatusLabel));
|
||||||
OnPropertyChanged(nameof(EffectiveModelLabel));
|
RecomputeModelBadge();
|
||||||
OnPropertyChanged(nameof(EffectiveAgentLabel));
|
RecomputeTurnsBadge();
|
||||||
|
RecomputeAgentBadge();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subscribe once; filter by current task id inside the handler
|
// Subscribe once; filter by current task id inside the handler
|
||||||
_worker.TaskMessageEvent += OnTaskMessage;
|
_worker.TaskMessageEvent += OnTaskMessage;
|
||||||
|
_worker.PrepStartedEvent += OnPrepStarted;
|
||||||
|
_worker.PrepLineEvent += OnPrepLine;
|
||||||
|
_worker.PrepFinishedEvent += OnPrepFinished;
|
||||||
|
|
||||||
// Re-evaluate CanExecute when worker connection flips.
|
// Re-evaluate CanExecute when worker connection flips.
|
||||||
_worker.PropertyChanged += (_, e) =>
|
_worker.PropertyChanged += (_, e) =>
|
||||||
@@ -302,6 +358,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
_worker.TaskStartedEvent += (slot, taskId, startedAt) =>
|
_worker.TaskStartedEvent += (slot, taskId, startedAt) =>
|
||||||
{
|
{
|
||||||
if (Task?.Id == taskId) AgentState = "running";
|
if (Task?.Id == taskId) AgentState = "running";
|
||||||
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
_worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
|
_worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
|
||||||
{
|
{
|
||||||
@@ -315,18 +372,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
AgentState = FinishedStatusToStateKey(status);
|
AgentState = FinishedStatusToStateKey(status);
|
||||||
// Re-query to pick up worktree created during the run.
|
// Re-query to pick up worktree created during the run.
|
||||||
_ = RefreshWorktreeAsync(taskId);
|
_ = RefreshWorktreeAsync(taskId);
|
||||||
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
|
|
||||||
_worker.WorktreeUpdatedEvent += taskId =>
|
_worker.WorktreeUpdatedEvent += taskId =>
|
||||||
{
|
{
|
||||||
if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId);
|
if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId);
|
||||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||||
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
|
|
||||||
_worker.TaskUpdatedEvent += taskId =>
|
_worker.TaskUpdatedEvent += taskId =>
|
||||||
{
|
{
|
||||||
if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId);
|
if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId);
|
||||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||||
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
|
|
||||||
Subtasks.CollectionChanged += (_, _) =>
|
Subtasks.CollectionChanged += (_, _) =>
|
||||||
@@ -334,6 +394,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
RecomputeCanMergeAll();
|
RecomputeCanMergeAll();
|
||||||
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ChildOutcomes.CollectionChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
RecomputeCanMergeAll();
|
||||||
|
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTaskMessage(string taskId, string line)
|
private void OnTaskMessage(string taskId, string line)
|
||||||
@@ -345,9 +413,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
|
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var body = line["[stdout]".Length..].TrimStart();
|
var body = line["[stdout]".Length..].TrimStart();
|
||||||
var formatted = _formatter.FormatLine(body);
|
AppendStdoutLine(Log, body);
|
||||||
if (formatted is null) return; // filter noise (message_start, etc.)
|
|
||||||
AppendClaudeText(formatted);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,20 +429,52 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
Log.Add(new LogLineViewModel { Kind = kind, Text = line });
|
Log.Add(new LogLineViewModel { Kind = kind, Text = line });
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendClaudeText(string chunk)
|
private void AppendStdoutLine(ObservableCollection<LogLineViewModel> target, string line)
|
||||||
{
|
{
|
||||||
_claudeBuf.Append(chunk);
|
var formatted = _formatter.FormatLine(line);
|
||||||
|
if (formatted is null) return;
|
||||||
|
var buf = ReferenceEquals(target, Log) ? _claudeBuf : _prepClaudeBuf;
|
||||||
|
AppendClaudeText(formatted, target, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task PlanDayAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
try { await _worker.RunDailyPrepNowAsync(); }
|
||||||
|
catch { /* worker offline; PrepStarted/PrepLine will reconcile */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowPrepEmptyState => !IsPrepRunning && PrepLog.Count == 0;
|
||||||
|
|
||||||
|
partial void OnIsPrepRunningChanged(bool value) => OnPropertyChanged(nameof(ShowPrepEmptyState));
|
||||||
|
|
||||||
|
private void OnPrepStarted()
|
||||||
|
{
|
||||||
|
PrepLog.Clear();
|
||||||
|
IsPrepRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrepLine(string line) => AppendStdoutLine(PrepLog, line);
|
||||||
|
|
||||||
|
private void OnPrepFinished(bool success) => IsPrepRunning = false;
|
||||||
|
|
||||||
|
private void AppendClaudeText(string chunk) => AppendClaudeText(chunk, Log, _claudeBuf);
|
||||||
|
|
||||||
|
private static void AppendClaudeText(string chunk, ObservableCollection<LogLineViewModel> target, StringBuilder buf)
|
||||||
|
{
|
||||||
|
buf.Append(chunk);
|
||||||
// Emit a log entry for every completed line; keep the trailing remainder buffered.
|
// Emit a log entry for every completed line; keep the trailing remainder buffered.
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var text = _claudeBuf.ToString();
|
var text = buf.ToString();
|
||||||
var nl = text.IndexOf('\n');
|
var nl = text.IndexOf('\n');
|
||||||
if (nl < 0) break;
|
if (nl < 0) break;
|
||||||
var piece = text[..nl].TrimEnd('\r');
|
var piece = text[..nl].TrimEnd('\r');
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
if (!string.IsNullOrWhiteSpace(piece))
|
||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
target.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
_claudeBuf.Clear();
|
buf.Clear();
|
||||||
_claudeBuf.Append(text[(nl + 1)..]);
|
buf.Append(text[(nl + 1)..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,9 +487,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnTaskModelSelectionChanged(string value) => QueueAgentSave();
|
|
||||||
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
|
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
|
||||||
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();
|
partial void OnTaskSelectedAgentChanged(AgentInfo? value) { RecomputeAgentBadge(); QueueAgentSave(); }
|
||||||
|
|
||||||
partial void OnEditableDescriptionChanged(string value)
|
partial void OnEditableDescriptionChanged(string value)
|
||||||
{
|
{
|
||||||
@@ -434,13 +531,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
await System.Threading.Tasks.Task.Delay(300, ct);
|
await System.Threading.Tasks.Task.Delay(300, ct);
|
||||||
if (Task is null) return;
|
if (Task is null) return;
|
||||||
|
|
||||||
var model = TaskModelSelection == ModelRegistry.TaskInheritSentinel ? null : TaskModelSelection;
|
var model = string.IsNullOrWhiteSpace(TaskModelSelection) ? null : TaskModelSelection;
|
||||||
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
|
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
|
||||||
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
|
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
|
||||||
? null : TaskSelectedAgent.Path;
|
? null : TaskSelectedAgent.Path;
|
||||||
|
var turns = TaskMaxTurns is decimal d ? (int?)d : null;
|
||||||
|
|
||||||
await _worker.UpdateTaskAgentSettingsAsync(
|
await _worker.UpdateTaskAgentSettingsAsync(
|
||||||
new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap));
|
new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap, turns));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
catch { }
|
catch { }
|
||||||
@@ -453,21 +551,31 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
TaskAgentOptions.Clear();
|
TaskAgentOptions.Clear();
|
||||||
TaskAgentOptions.Add(new AgentInfo(ModelRegistry.TaskInheritSentinel, "", ""));
|
TaskAgentOptions.Add(new AgentInfo("(inherited)", "", ""));
|
||||||
var agents = await _worker.GetAgentsAsync();
|
var agents = await _worker.GetAgentsAsync();
|
||||||
foreach (var a in agents) TaskAgentOptions.Add(a);
|
foreach (var a in agents) TaskAgentOptions.Add(a);
|
||||||
|
|
||||||
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? ModelRegistry.TaskInheritSentinel : entity.Model!;
|
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? null : entity.Model!;
|
||||||
|
TaskMaxTurns = entity.MaxTurns is int tmt ? tmt : (decimal?)null;
|
||||||
TaskSystemPrompt = entity.SystemPrompt ?? "";
|
TaskSystemPrompt = entity.SystemPrompt ?? "";
|
||||||
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
|
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
|
||||||
? TaskAgentOptions[0]
|
? TaskAgentOptions[0]
|
||||||
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
|
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
|
||||||
|
|
||||||
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
|
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
|
||||||
EffectiveModelHint = string.IsNullOrWhiteSpace(listCfg?.Model) ? "(global default)" : listCfg!.Model!;
|
var app = await _worker.GetAppSettingsAsync();
|
||||||
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "(none)" : listCfg!.SystemPrompt!;
|
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
||||||
EffectiveAgentHint = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
|
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
|
||||||
? "(none)" : System.IO.Path.GetFileName(listCfg!.AgentPath!);
|
_listModel = listCfg?.Model;
|
||||||
|
_listMaxTurns = listCfg?.MaxTurns;
|
||||||
|
_listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
|
||||||
|
? null : System.IO.Path.GetFileName(listCfg!.AgentPath!);
|
||||||
|
|
||||||
|
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "" : listCfg!.SystemPrompt!;
|
||||||
|
|
||||||
|
RecomputeModelBadge();
|
||||||
|
RecomputeTurnsBadge();
|
||||||
|
RecomputeAgentBadge();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -478,13 +586,37 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
public void ShowNotes()
|
public void ShowNotes()
|
||||||
{
|
{
|
||||||
Bind(null);
|
Bind(null);
|
||||||
|
IsPrepMode = false;
|
||||||
IsNotesMode = true;
|
IsNotesMode = true;
|
||||||
_ = Notes.LoadDayAsync(DateOnly.FromDateTime(DateTime.Today));
|
_ = Notes.LoadDayAsync(DateOnly.FromDateTime(DateTime.Today));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowPrep()
|
||||||
|
{
|
||||||
|
Bind(null);
|
||||||
|
IsNotesMode = false;
|
||||||
|
IsPrepMode = true;
|
||||||
|
_ = LoadLastPrepLogIfEmptyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadLastPrepLogIfEmptyAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null || IsPrepRunning || PrepLog.Count > 0) return;
|
||||||
|
string text;
|
||||||
|
try { text = await _worker.GetLastPrepLogAsync(); }
|
||||||
|
catch { return; }
|
||||||
|
if (IsPrepRunning || PrepLog.Count > 0) return;
|
||||||
|
foreach (var line in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.TrimEnd('\r');
|
||||||
|
if (trimmed.Length > 0) AppendStdoutLine(PrepLog, trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Bind(TaskRowViewModel? row)
|
public void Bind(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
IsNotesMode = false;
|
IsNotesMode = false;
|
||||||
|
IsPrepMode = false;
|
||||||
_loadCts?.Cancel();
|
_loadCts?.Cancel();
|
||||||
_loadCts?.Dispose();
|
_loadCts?.Dispose();
|
||||||
_loadCts = new CancellationTokenSource();
|
_loadCts = new CancellationTokenSource();
|
||||||
@@ -494,6 +626,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(TaskIdBadge));
|
OnPropertyChanged(nameof(TaskIdBadge));
|
||||||
Log.Clear();
|
Log.Clear();
|
||||||
Subtasks.Clear();
|
Subtasks.Clear();
|
||||||
|
ChildOutcomes.Clear();
|
||||||
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
MergeTargetBranches.Clear();
|
MergeTargetBranches.Clear();
|
||||||
SelectedMergeTarget = null;
|
SelectedMergeTarget = null;
|
||||||
CanMergeAll = false;
|
CanMergeAll = false;
|
||||||
@@ -510,12 +644,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
WorktreePath = null;
|
WorktreePath = null;
|
||||||
WorktreeStateLabel = null;
|
WorktreeStateLabel = null;
|
||||||
BranchLine = null;
|
BranchLine = null;
|
||||||
|
DiffAdditions = 0;
|
||||||
|
DiffDeletions = 0;
|
||||||
AgentState = "idle";
|
AgentState = "idle";
|
||||||
LatestRunSessionId = null;
|
LatestRunSessionId = null;
|
||||||
_suppressAgentSave = true;
|
_suppressAgentSave = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
TaskModelSelection = ModelRegistry.TaskInheritSentinel;
|
TaskModelSelection = null;
|
||||||
|
TaskMaxTurns = null;
|
||||||
TaskSystemPrompt = "";
|
TaskSystemPrompt = "";
|
||||||
TaskSelectedAgent = null;
|
TaskSelectedAgent = null;
|
||||||
}
|
}
|
||||||
@@ -523,9 +660,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_suppressAgentSave = false;
|
_suppressAgentSave = false;
|
||||||
}
|
}
|
||||||
EffectiveModelHint = "";
|
|
||||||
EffectiveSystemPromptHint = "";
|
EffectiveSystemPromptHint = "";
|
||||||
EffectiveAgentHint = "";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,6 +691,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
|
||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
|
||||||
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
|
DiffAdditions = add;
|
||||||
|
DiffDeletions = del;
|
||||||
AgentState = StatusToStateKey(entity.Status);
|
AgentState = StatusToStateKey(entity.Status);
|
||||||
await LoadAgentSettingsAsync(entity, ct);
|
await LoadAgentSettingsAsync(entity, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
@@ -580,10 +718,64 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
await LoadPlanningChildrenAsync(row.Id, ct);
|
await LoadPlanningChildrenAsync(row.Id, ct);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await LoadChildOutcomesAsync(row.Id, ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Improvement parents (non-planning) surface their children's outcomes + roadblocks
|
||||||
|
// on the review card, and reuse the planning merge controls to fold the tree in.
|
||||||
|
private async System.Threading.Tasks.Task LoadChildOutcomesAsync(string parentTaskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var children = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Worktree)
|
||||||
|
.Where(t => t.ParentTaskId == parentTaskId)
|
||||||
|
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (children.Count == 0) return;
|
||||||
|
|
||||||
|
ChildOutcomes.Clear();
|
||||||
|
foreach (var c in children)
|
||||||
|
ChildOutcomes.Add(new ChildOutcomeRowViewModel
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
Title = c.Title,
|
||||||
|
Status = c.Status,
|
||||||
|
RoadblockCount = c.RoadblockCount,
|
||||||
|
WorktreeState = c.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active,
|
||||||
|
});
|
||||||
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
|
|
||||||
|
if (MergeTargetBranches.Count == 0)
|
||||||
|
{
|
||||||
|
var childWithWorktree = children.FirstOrDefault(c => c.Worktree != null);
|
||||||
|
if (childWithWorktree != null)
|
||||||
|
{
|
||||||
|
var targets = await _worker.GetMergeTargetsAsync(childWithWorktree.Id);
|
||||||
|
if (targets != null)
|
||||||
|
{
|
||||||
|
MergeTargetBranches.Clear();
|
||||||
|
foreach (var b in targets.LocalBranches)
|
||||||
|
MergeTargetBranches.Add(b);
|
||||||
|
SelectedMergeTarget = targets.DefaultBranch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecomputeCanMergeAll();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(logPath)) return;
|
if (string.IsNullOrWhiteSpace(logPath)) return;
|
||||||
@@ -705,8 +897,51 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
catch { /* best-effort */ }
|
catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Live-update a single improvement child's outcome row from a task event. No-op if the
|
||||||
|
// updated task isn't one of this parent's children.
|
||||||
|
private async System.Threading.Tasks.Task RefreshChildOutcomeAsync(string childTaskId)
|
||||||
|
{
|
||||||
|
var row = ChildOutcomes.FirstOrDefault(c => c.Id == childTaskId);
|
||||||
|
if (row is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var child = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Worktree)
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == childTaskId);
|
||||||
|
if (child is null) return;
|
||||||
|
row.Status = child.Status;
|
||||||
|
row.RoadblockCount = child.RoadblockCount;
|
||||||
|
row.WorktreeState = child.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active;
|
||||||
|
RecomputeCanMergeAll();
|
||||||
|
MergeAllCommand.NotifyCanExecuteChanged();
|
||||||
|
ReviewCombinedDiffCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
catch { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
internal void RecomputeCanMergeAll()
|
internal void RecomputeCanMergeAll()
|
||||||
{
|
{
|
||||||
|
// Improvement parent: merge is allowed once every child is terminal. The
|
||||||
|
// orchestrator folds the parent's own branch and skips failed/cancelled children.
|
||||||
|
if (ChildOutcomes.Count > 0)
|
||||||
|
{
|
||||||
|
var unfinished = ChildOutcomes.Count(c =>
|
||||||
|
c.Status != ClaudeDo.Data.Models.TaskStatus.Done
|
||||||
|
&& c.Status != ClaudeDo.Data.Models.TaskStatus.Failed
|
||||||
|
&& c.Status != ClaudeDo.Data.Models.TaskStatus.Cancelled);
|
||||||
|
if (unfinished > 0)
|
||||||
|
{
|
||||||
|
CanMergeAll = false;
|
||||||
|
MergeAllDisabledReason = $"{unfinished} improvement(s) not finished";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CanMergeAll = true;
|
||||||
|
MergeAllDisabledReason = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var notDone = Subtasks.Count(c => c.Status != ClaudeDo.Data.Models.TaskStatus.Done);
|
var notDone = Subtasks.Count(c => c.Status != ClaudeDo.Data.Models.TaskStatus.Done);
|
||||||
if (notDone > 0)
|
if (notDone > 0)
|
||||||
{
|
{
|
||||||
@@ -736,7 +971,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
await ShowPlanningDiffModal(vm);
|
await ShowPlanningDiffModal(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanReviewDiff() => Task?.IsPlanningParent == true && Subtasks.Any();
|
private bool CanReviewDiff() => (Task?.IsPlanningParent == true && Subtasks.Any()) || HasChildOutcomes;
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanMergeAll))]
|
[RelayCommand(CanExecute = nameof(CanMergeAll))]
|
||||||
private async System.Threading.Tasks.Task MergeAllAsync()
|
private async System.Threading.Tasks.Task MergeAllAsync()
|
||||||
@@ -770,6 +1005,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
AgentState = StatusToStateKey(entity.Status);
|
AgentState = StatusToStateKey(entity.Status);
|
||||||
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
|
||||||
row.DiffStat = stat;
|
row.DiffStat = stat;
|
||||||
|
var (add, del) = ParseDiffStat(entity.Worktree?.DiffStat);
|
||||||
|
DiffAdditions = add;
|
||||||
|
DiffDeletions = del;
|
||||||
}
|
}
|
||||||
catch { /* best-effort refresh */ }
|
catch { /* best-effort refresh */ }
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1255,63 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
|
|
||||||
private bool CanResetAndRetry() =>
|
private bool CanResetAndRetry() =>
|
||||||
Task != null && _worker.IsConnected && ShowResetAndRetry;
|
Task != null && _worker.IsConnected && ShowResetAndRetry;
|
||||||
|
|
||||||
|
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
|
||||||
|
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
|
||||||
|
[RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
|
||||||
|
|
||||||
|
// ── Review actions ──────────────────────────────────────────────────────────
|
||||||
|
[ObservableProperty] private string _reviewFeedback = "";
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task ApproveReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
// The hub rejects (HubException) if the task is no longer WaitingForReview
|
||||||
|
// — e.g. after "Merge all" folded the parent. Swallow it; the TaskUpdated
|
||||||
|
// broadcast reconciles the UI. An unhandled command exception would crash.
|
||||||
|
try { await _worker.ApproveReviewAsync(Task.Id); }
|
||||||
|
catch { /* stale review action; broadcast reconciles */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task RejectReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
var feedback = ReviewFeedback;
|
||||||
|
if (string.IsNullOrWhiteSpace(feedback)) return;
|
||||||
|
try { await _worker.RejectReviewToQueueAsync(Task.Id, feedback); }
|
||||||
|
catch { /* stale review action; broadcast reconciles */ return; }
|
||||||
|
ReviewFeedback = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task ParkReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
try { await _worker.RejectReviewToIdleAsync(Task.Id); }
|
||||||
|
catch { /* stale review action; broadcast reconciles */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async System.Threading.Tasks.Task CancelReviewAsync()
|
||||||
|
{
|
||||||
|
if (Task is null || !_worker.IsConnected) return;
|
||||||
|
try { await _worker.CancelReviewAsync(Task.Id); }
|
||||||
|
catch { /* stale review action; broadcast reconciles */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Diff meter parser ───────────────────────────────────────────────────────
|
||||||
|
internal static (int Additions, int Deletions) ParseDiffStat(string? stat)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(stat)) return (0, 0);
|
||||||
|
int add = 0, del = 0;
|
||||||
|
var m1 = System.Text.RegularExpressions.Regex.Match(stat, @"(\d+)\s+insertion");
|
||||||
|
var m2 = System.Text.RegularExpressions.Regex.Match(stat, @"(\d+)\s+deletion");
|
||||||
|
if (m1.Success) int.TryParse(m1.Groups[1].Value, out add);
|
||||||
|
if (m2.Success) int.TryParse(m2.Groups[1].Value, out del);
|
||||||
|
return (add, del);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed partial class SubtaskRowViewModel : ViewModelBase
|
public sealed partial class SubtaskRowViewModel : ViewModelBase
|
||||||
@@ -1028,3 +1323,38 @@ public sealed partial class SubtaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status;
|
[ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _status;
|
||||||
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
|
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A suggested child's outcome on an improvement parent's review card. Observable so the
|
||||||
|
// row reflects the child's live status (Idle → Running → Done/Failed) as it executes.
|
||||||
|
public sealed partial class ChildOutcomeRowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(StatusLabel))]
|
||||||
|
private ClaudeDo.Data.Models.TaskStatus _status;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasRoadblock))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(RoadblockText))]
|
||||||
|
private int _roadblockCount;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ClaudeDo.Data.Models.WorktreeState _worktreeState = ClaudeDo.Data.Models.WorktreeState.Active;
|
||||||
|
|
||||||
|
public string StatusLabel => Status switch
|
||||||
|
{
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Done => Loc.T("vm.taskStatus.done"),
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Failed => Loc.T("vm.taskStatus.failed"),
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Cancelled => Loc.T("vm.taskStatus.cancelled"),
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Running => Loc.T("vm.taskStatus.running"),
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.WaitingForReview => Loc.T("vm.taskStatus.waitingForReview"),
|
||||||
|
ClaudeDo.Data.Models.TaskStatus.WaitingForChildren => Loc.T("vm.taskStatus.waitingForChildren"),
|
||||||
|
_ => Loc.T("vm.taskStatus.idle"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool HasRoadblock => RoadblockCount > 0;
|
||||||
|
public string RoadblockText => RoadblockCount == 1 ? "1 roadblock" : $"{RoadblockCount} roadblocks";
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _dropHintAbove;
|
[ObservableProperty] private bool _dropHintAbove;
|
||||||
[ObservableProperty] private bool _dropHintBelow;
|
[ObservableProperty] private bool _dropHintBelow;
|
||||||
[ObservableProperty] private string? _parentTaskId;
|
[ObservableProperty] private string? _parentTaskId;
|
||||||
|
[ObservableProperty] private string? _createdBy;
|
||||||
[ObservableProperty] private string? _blockedByTaskId;
|
[ObservableProperty] private string? _blockedByTaskId;
|
||||||
[ObservableProperty] private bool _isExpanded = true;
|
[ObservableProperty] private bool _isExpanded = true;
|
||||||
[ObservableProperty] private bool _hasPlanningChildren;
|
[ObservableProperty] private bool _hasPlanningChildren;
|
||||||
[ObservableProperty] private bool _hasQueuedSubtasks;
|
[ObservableProperty] private bool _hasQueuedSubtasks;
|
||||||
[ObservableProperty] private bool _showListChip = true;
|
[ObservableProperty] private bool _showListChip = true;
|
||||||
[ObservableProperty] private bool _parentFinalized;
|
[ObservableProperty] private bool _parentFinalized;
|
||||||
|
[ObservableProperty] private int _roadblockCount;
|
||||||
|
|
||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
|
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
|
||||||
@@ -37,7 +39,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
public int StepsCount { get; init; }
|
public int StepsCount { get; init; }
|
||||||
public int StepsCompleted { get; init; }
|
public int StepsCompleted { get; init; }
|
||||||
|
|
||||||
public bool IsChild => !string.IsNullOrEmpty(ParentTaskId);
|
public bool IsChild => !string.IsNullOrEmpty(ParentTaskId);
|
||||||
|
public bool IsAgentSuggested => IsChild && !string.IsNullOrEmpty(CreatedBy) && CreatedBy == ParentTaskId;
|
||||||
public bool IsPlanningParent => PlanningPhase != PlanningPhase.None
|
public bool IsPlanningParent => PlanningPhase != PlanningPhase.None
|
||||||
|| HasPlanningChildren;
|
|| HasPlanningChildren;
|
||||||
// A subtask is Draft until its planning parent is finalized, then Planned (queueable).
|
// A subtask is Draft until its planning parent is finalized, then Planned (queueable).
|
||||||
@@ -75,6 +78,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
&& PlanningPhase == PlanningPhase.Finalized
|
&& PlanningPhase == PlanningPhase.Finalized
|
||||||
&& !HasQueuedSubtasks;
|
&& !HasQueuedSubtasks;
|
||||||
public bool HasSchedule => ScheduledFor.HasValue;
|
public bool HasSchedule => ScheduledFor.HasValue;
|
||||||
|
public bool HasRoadblock => RoadblockCount > 0;
|
||||||
|
public string RoadblockTooltip => RoadblockCount == 1
|
||||||
|
? "1 roadblock reported during the run — see details"
|
||||||
|
: $"{RoadblockCount} roadblocks reported during the run — see details";
|
||||||
|
|
||||||
public string DiffAdditionsText => $"+{DiffAdditions}";
|
public string DiffAdditionsText => $"+{DiffAdditions}";
|
||||||
public string DiffDeletionsText => $"−{DiffDeletions}";
|
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||||||
@@ -85,7 +92,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
|
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
|
||||||
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
||||||
TaskStatus.Running => Loc.T("vm.taskStatus.running"),
|
TaskStatus.Running => Loc.T("vm.taskStatus.running"),
|
||||||
TaskStatus.WaitingForReview => Loc.T("vm.taskStatus.waitingForReview"),
|
TaskStatus.WaitingForReview => Loc.T("vm.taskStatus.waitingForReview"),
|
||||||
|
TaskStatus.WaitingForChildren => Loc.T("vm.taskStatus.waitingForChildren"),
|
||||||
TaskStatus.Done => Loc.T("vm.taskStatus.done"),
|
TaskStatus.Done => Loc.T("vm.taskStatus.done"),
|
||||||
TaskStatus.Failed => Loc.T("vm.taskStatus.failed"),
|
TaskStatus.Failed => Loc.T("vm.taskStatus.failed"),
|
||||||
TaskStatus.Cancelled => Loc.T("vm.taskStatus.cancelled"),
|
TaskStatus.Cancelled => Loc.T("vm.taskStatus.cancelled"),
|
||||||
@@ -94,8 +102,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string StatusChipClass => (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
|
public string StatusChipClass => (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
|
||||||
{
|
{
|
||||||
(TaskStatus.Running, _) => "running",
|
(TaskStatus.Running, _) => "running",
|
||||||
(TaskStatus.WaitingForReview, _) => "review",
|
(TaskStatus.WaitingForReview, _) => "review",
|
||||||
|
(TaskStatus.WaitingForChildren, _) => "children",
|
||||||
(TaskStatus.Failed, _) => "error",
|
(TaskStatus.Failed, _) => "error",
|
||||||
(TaskStatus.Done, _) => "done",
|
(TaskStatus.Done, _) => "done",
|
||||||
(TaskStatus.Queued, true) => "waiting",
|
(TaskStatus.Queued, true) => "waiting",
|
||||||
@@ -121,12 +130,15 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
partial void OnParentTaskIdChanged(string? value)
|
partial void OnParentTaskIdChanged(string? value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsChild));
|
OnPropertyChanged(nameof(IsChild));
|
||||||
|
OnPropertyChanged(nameof(IsAgentSuggested));
|
||||||
OnPropertyChanged(nameof(IsDraft));
|
OnPropertyChanged(nameof(IsDraft));
|
||||||
OnPropertyChanged(nameof(IsPlanned));
|
OnPropertyChanged(nameof(IsPlanned));
|
||||||
OnPropertyChanged(nameof(CanSendToQueue));
|
OnPropertyChanged(nameof(CanSendToQueue));
|
||||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnCreatedByChanged(string? value) => OnPropertyChanged(nameof(IsAgentSuggested));
|
||||||
|
|
||||||
partial void OnParentFinalizedChanged(bool value)
|
partial void OnParentFinalizedChanged(bool value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsDraft));
|
OnPropertyChanged(nameof(IsDraft));
|
||||||
@@ -174,6 +186,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
|
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
|
||||||
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
||||||
|
partial void OnRoadblockCountChanged(int value) { OnPropertyChanged(nameof(HasRoadblock)); OnPropertyChanged(nameof(RoadblockTooltip)); }
|
||||||
|
|
||||||
public void RefreshLocalized()
|
public void RefreshLocalized()
|
||||||
{
|
{
|
||||||
@@ -206,7 +219,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
DiffAdditions = add;
|
DiffAdditions = add;
|
||||||
DiffDeletions = del;
|
DiffDeletions = del;
|
||||||
ParentTaskId = t.ParentTaskId;
|
ParentTaskId = t.ParentTaskId;
|
||||||
|
CreatedBy = t.CreatedBy;
|
||||||
BlockedByTaskId = t.BlockedByTaskId;
|
BlockedByTaskId = t.BlockedByTaskId;
|
||||||
|
RoadblockCount = t.RoadblockCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
public event EventHandler? FocusAddTaskRequested;
|
public event EventHandler? FocusAddTaskRequested;
|
||||||
public event EventHandler? TasksChanged;
|
public event EventHandler? TasksChanged;
|
||||||
public event Action? NotesRequested;
|
public event Action? NotesRequested;
|
||||||
|
public event Action? PrepRequested;
|
||||||
public void RequestFocusAddTask() => FocusAddTaskRequested?.Invoke(this, EventArgs.Empty);
|
public void RequestFocusAddTask() => FocusAddTaskRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -36,6 +37,17 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
NotesRequested?.Invoke();
|
NotesRequested?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowPrepLog() => PrepRequested?.Invoke();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ClearDayAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
try { await _worker.ClearMyDayAsync(); }
|
||||||
|
catch { /* worker offline; broadcast will reconcile on return */ }
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
|
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
|
||||||
public ObservableCollection<TaskRowViewModel> OverdueItems { get; } = new();
|
public ObservableCollection<TaskRowViewModel> OverdueItems { get; } = new();
|
||||||
public ObservableCollection<TaskRowViewModel> OpenItems { get; } = new();
|
public ObservableCollection<TaskRowViewModel> OpenItems { get; } = new();
|
||||||
@@ -55,6 +67,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _showOpenLabel;
|
[ObservableProperty] private bool _showOpenLabel;
|
||||||
[ObservableProperty] private string _completedHeader = "";
|
[ObservableProperty] private string _completedHeader = "";
|
||||||
[ObservableProperty] private bool _showNotesRow;
|
[ObservableProperty] private bool _showNotesRow;
|
||||||
|
[ObservableProperty] private bool _isMyDayList;
|
||||||
|
|
||||||
public Func<UnfinishedPlanningModalViewModel, Task>? ShowUnfinishedPlanningModal { get; set; }
|
public Func<UnfinishedPlanningModalViewModel, Task>? ShowUnfinishedPlanningModal { get; set; }
|
||||||
|
|
||||||
@@ -201,6 +214,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
HeaderTitle = list.Name;
|
HeaderTitle = list.Name;
|
||||||
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd", CultureInfo.InvariantCulture).ToUpperInvariant();
|
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd", CultureInfo.InvariantCulture).ToUpperInvariant();
|
||||||
ShowNotesRow = list.Id == "smart:my-day";
|
ShowNotesRow = list.Id == "smart:my-day";
|
||||||
|
IsMyDayList = list.Id == "smart:my-day";
|
||||||
|
|
||||||
_ = LoadForListAsync(list, ct);
|
_ = LoadForListAsync(list, ct);
|
||||||
}
|
}
|
||||||
@@ -683,9 +697,6 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleShowCompleted() => IsShowingCompleted = !IsShowingCompleted;
|
private void ToggleShowCompleted() => IsShowingCompleted = !IsShowingCompleted;
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Sort() { /* placeholder — UI-only */ }
|
|
||||||
|
|
||||||
public event EventHandler? OpenListSettingsRequested;
|
public event EventHandler? OpenListSettingsRequested;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
|||||||
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
||||||
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
|
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
|
||||||
Tasks.NotesRequested += () => Details.ShowNotes();
|
Tasks.NotesRequested += () => Details.ShowNotes();
|
||||||
|
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||||
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
|
Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync();
|
||||||
Tasks.OpenListSettingsRequested += (_, _) =>
|
Tasks.OpenListSettingsRequested += (_, _) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using ClaudeDo.Ui.Localization;
|
|||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
public enum DiffLineKind { Add, Del, Ctx }
|
public enum DiffLineKind { Add, Del, Ctx, File }
|
||||||
|
|
||||||
public sealed class DiffLineViewModel
|
public sealed class DiffLineViewModel
|
||||||
{
|
{
|
||||||
@@ -16,9 +16,10 @@ public sealed class DiffLineViewModel
|
|||||||
public required string Text { get; init; }
|
public required string Text { get; init; }
|
||||||
public string ClassName => Kind switch
|
public string ClassName => Kind switch
|
||||||
{
|
{
|
||||||
DiffLineKind.Add => "add",
|
DiffLineKind.Add => "add",
|
||||||
DiffLineKind.Del => "del",
|
DiffLineKind.Del => "del",
|
||||||
_ => "ctx",
|
DiffLineKind.File => "file",
|
||||||
|
_ => "ctx",
|
||||||
};
|
};
|
||||||
|
|
||||||
public string Sign => Kind switch
|
public string Sign => Kind switch
|
||||||
@@ -102,90 +103,10 @@ public sealed partial class DiffModalViewModel : ViewModelBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse unified diff — state machine over lines
|
foreach (var file in UnifiedDiffParser.Parse(raw))
|
||||||
DiffFileViewModel? current = null;
|
Files.Add(file);
|
||||||
int oldLine = 0, newLine = 0;
|
|
||||||
|
|
||||||
foreach (var line in raw.Split('\n'))
|
|
||||||
{
|
|
||||||
if (line.StartsWith("diff --git ", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
// e.g. "diff --git a/src/Foo.cs b/src/Foo.cs"
|
|
||||||
var parts = line.Split(' ');
|
|
||||||
var path = parts.Length >= 4 ? parts[3][2..] : line;
|
|
||||||
current = new DiffFileViewModel { Path = path };
|
|
||||||
Files.Add(current);
|
|
||||||
oldLine = 0; newLine = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current == null) continue;
|
|
||||||
|
|
||||||
if (line.StartsWith("@@ ", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
// e.g. "@@ -10,7 +10,9 @@"
|
|
||||||
ParseHunkHeader(line, out oldLine, out newLine);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip diff metadata lines
|
|
||||||
if (line.StartsWith("--- ", StringComparison.Ordinal) ||
|
|
||||||
line.StartsWith("+++ ", StringComparison.Ordinal) ||
|
|
||||||
line.StartsWith("index ", StringComparison.Ordinal) ||
|
|
||||||
line.StartsWith("new file", StringComparison.Ordinal) ||
|
|
||||||
line.StartsWith("deleted file", StringComparison.Ordinal) ||
|
|
||||||
line.StartsWith("Binary ", StringComparison.Ordinal))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (line.StartsWith('+'))
|
|
||||||
{
|
|
||||||
current.Lines.Add(new DiffLineViewModel
|
|
||||||
{
|
|
||||||
Kind = DiffLineKind.Add,
|
|
||||||
NewNo = newLine++,
|
|
||||||
Text = line.Length > 1 ? line[1..] : "",
|
|
||||||
});
|
|
||||||
current.Additions++;
|
|
||||||
}
|
|
||||||
else if (line.StartsWith('-'))
|
|
||||||
{
|
|
||||||
current.Lines.Add(new DiffLineViewModel
|
|
||||||
{
|
|
||||||
Kind = DiffLineKind.Del,
|
|
||||||
OldNo = oldLine++,
|
|
||||||
Text = line.Length > 1 ? line[1..] : "",
|
|
||||||
});
|
|
||||||
current.Deletions++;
|
|
||||||
}
|
|
||||||
else if (line.StartsWith(' '))
|
|
||||||
{
|
|
||||||
current.Lines.Add(new DiffLineViewModel
|
|
||||||
{
|
|
||||||
Kind = DiffLineKind.Ctx,
|
|
||||||
OldNo = oldLine++,
|
|
||||||
NewNo = newLine++,
|
|
||||||
Text = line.Length > 1 ? line[1..] : "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedFile = Files.Count > 0 ? Files[0] : null;
|
SelectedFile = Files.Count > 0 ? Files[0] : null;
|
||||||
if (Files.Count == 0) StatusMessage = Loc.T("vm.diff.noChanges");
|
if (Files.Count == 0) StatusMessage = Loc.T("vm.diff.noChanges");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ParseHunkHeader(string header, out int oldStart, out int newStart)
|
|
||||||
{
|
|
||||||
oldStart = 1; newStart = 1;
|
|
||||||
// Format: @@ -<old>,<count> +<new>,<count> @@
|
|
||||||
var at = header.IndexOf("@@", 3, StringComparison.Ordinal);
|
|
||||||
var inner = at > 0 ? header[3..at].Trim() : header;
|
|
||||||
var segs = inner.Split(' ');
|
|
||||||
foreach (var seg in segs)
|
|
||||||
{
|
|
||||||
if (seg.StartsWith('-') && int.TryParse(seg[1..].Split(',')[0], out var o))
|
|
||||||
oldStart = o;
|
|
||||||
else if (seg.StartsWith('+') && int.TryParse(seg[1..].Split(',')[0], out var n))
|
|
||||||
newStart = n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,21 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _workingDir = "";
|
[ObservableProperty] private string _workingDir = "";
|
||||||
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
|
[ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType;
|
||||||
|
|
||||||
[ObservableProperty] private string _selectedModel = ModelRegistry.ListDefaultSentinel;
|
[ObservableProperty] private string? _selectedModel; // null = inherit from global
|
||||||
|
[ObservableProperty] private decimal? _maxTurns; // null = inherit from global
|
||||||
|
[ObservableProperty] private string _modelInheritedHint = ""; // resolved value placeholder, e.g. "sonnet"
|
||||||
|
[ObservableProperty] private string _modelBadge = "";
|
||||||
|
[ObservableProperty] private string _turnsInheritedHint = "";
|
||||||
|
[ObservableProperty] private string _turnsBadge = "";
|
||||||
|
[ObservableProperty] private string _agentBadge = "";
|
||||||
|
|
||||||
[ObservableProperty] private string _systemPrompt = "";
|
[ObservableProperty] private string _systemPrompt = "";
|
||||||
[ObservableProperty] private AgentInfo? _selectedAgent;
|
[ObservableProperty] private AgentInfo? _selectedAgent;
|
||||||
|
|
||||||
public ObservableCollection<string> ModelOptions { get; } = new(
|
private string _globalModel = ModelRegistry.DefaultAlias;
|
||||||
new[] { ModelRegistry.ListDefaultSentinel }.Concat(ModelRegistry.Aliases));
|
private int _globalMaxTurns = 100;
|
||||||
|
|
||||||
|
public ObservableCollection<string> ModelOptions { get; } = new(ModelRegistry.Aliases);
|
||||||
|
|
||||||
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
public ObservableCollection<string> CommitTypeOptions { get; } = new(CommitTypeRegistry.Types);
|
||||||
|
|
||||||
@@ -47,6 +56,34 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedModelChanged(string? value) => RecomputeModelBadge();
|
||||||
|
partial void OnMaxTurnsChanged(decimal? value) => RecomputeTurnsBadge();
|
||||||
|
partial void OnSelectedAgentChanged(AgentInfo? value) => RecomputeAgentBadge();
|
||||||
|
|
||||||
|
private void RecomputeModelBadge()
|
||||||
|
{
|
||||||
|
ModelInheritedHint = _globalModel;
|
||||||
|
ModelBadge = !string.IsNullOrWhiteSpace(SelectedModel)
|
||||||
|
? Loc.T("settings.inherit.overrideBadge")
|
||||||
|
: Loc.T("settings.inherit.inheritedFromGlobal");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeTurnsBadge()
|
||||||
|
{
|
||||||
|
TurnsInheritedHint = _globalMaxTurns.ToString();
|
||||||
|
TurnsBadge = MaxTurns is not null
|
||||||
|
? Loc.T("settings.inherit.overrideBadge")
|
||||||
|
: Loc.T("settings.inherit.inheritedFromGlobal");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeAgentBadge()
|
||||||
|
{
|
||||||
|
var overridden = SelectedAgent is not null && !string.IsNullOrWhiteSpace(SelectedAgent.Path);
|
||||||
|
AgentBadge = overridden
|
||||||
|
? Loc.T("settings.inherit.overrideBadge")
|
||||||
|
: Loc.T("settings.inherit.inheritedFromGlobal");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task LoadAsync(
|
public async Task LoadAsync(
|
||||||
string listId,
|
string listId,
|
||||||
string name,
|
string name,
|
||||||
@@ -65,19 +102,30 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
foreach (var a in agents) Agents.Add(a);
|
foreach (var a in agents) Agents.Add(a);
|
||||||
|
|
||||||
var config = await _worker.GetListConfigAsync(listId);
|
var config = await _worker.GetListConfigAsync(listId);
|
||||||
SelectedModel = string.IsNullOrWhiteSpace(config?.Model) ? ModelRegistry.ListDefaultSentinel : config!.Model!;
|
|
||||||
|
var app = await _worker.GetAppSettingsAsync();
|
||||||
|
_globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
|
||||||
|
_globalMaxTurns = app?.DefaultMaxTurns ?? 100;
|
||||||
|
|
||||||
|
SelectedModel = string.IsNullOrWhiteSpace(config?.Model) ? null : config!.Model!;
|
||||||
|
MaxTurns = config?.MaxTurns is int mt ? mt : (decimal?)null;
|
||||||
SystemPrompt = config?.SystemPrompt ?? "";
|
SystemPrompt = config?.SystemPrompt ?? "";
|
||||||
SelectedAgent = string.IsNullOrWhiteSpace(config?.AgentPath)
|
SelectedAgent = string.IsNullOrWhiteSpace(config?.AgentPath)
|
||||||
? Agents[0]
|
? Agents[0]
|
||||||
: (Agents.FirstOrDefault(a => a.Path == config!.AgentPath) ?? Agents[0]);
|
: (Agents.FirstOrDefault(a => a.Path == config!.AgentPath) ?? Agents[0]);
|
||||||
|
|
||||||
|
RecomputeModelBadge();
|
||||||
|
RecomputeTurnsBadge();
|
||||||
|
RecomputeAgentBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SaveAsync()
|
private async Task SaveAsync()
|
||||||
{
|
{
|
||||||
var model = SelectedModel == ModelRegistry.ListDefaultSentinel ? null : SelectedModel;
|
var model = string.IsNullOrWhiteSpace(SelectedModel) ? null : SelectedModel;
|
||||||
var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
|
var sp = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt;
|
||||||
var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;
|
var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path;
|
||||||
|
var turns = MaxTurns is decimal d ? (int?)d : null;
|
||||||
|
|
||||||
await _worker.UpdateListAsync(new UpdateListDto(
|
await _worker.UpdateListAsync(new UpdateListDto(
|
||||||
ListId,
|
ListId,
|
||||||
@@ -85,8 +133,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
|
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
|
||||||
DefaultCommitType));
|
DefaultCommitType));
|
||||||
|
|
||||||
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(
|
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(ListId, model, sp, ap, turns));
|
||||||
ListId, model, sp, ap));
|
|
||||||
|
|
||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
}
|
}
|
||||||
@@ -125,10 +172,15 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Cancel() => CloseAction?.Invoke();
|
private void Cancel() => CloseAction?.Invoke();
|
||||||
|
|
||||||
|
[RelayCommand] private void ResetModel() => SelectedModel = null;
|
||||||
|
[RelayCommand] private void ResetTurns() => MaxTurns = null;
|
||||||
|
[RelayCommand] private void ResetAgent() => SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ResetAgentSettings()
|
private void ResetAgentSettings()
|
||||||
{
|
{
|
||||||
SelectedModel = ModelRegistry.ListDefaultSentinel;
|
SelectedModel = null;
|
||||||
|
MaxTurns = null;
|
||||||
SystemPrompt = "";
|
SystemPrompt = "";
|
||||||
SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
|
SelectedAgent = Agents.Count > 0 ? Agents[0] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
||||||
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
||||||
public string AgentPromptPath { get; } = PromptFiles.PathFor(PromptKind.Agent);
|
public string PlanningInitialPromptPath { get; } = PromptFiles.PathFor(PromptKind.PlanningInitial);
|
||||||
|
public string RetryPromptPath { get; } = PromptFiles.PathFor(PromptKind.Retry);
|
||||||
|
public string DailyPrepPromptPath { get; } = PromptFiles.PathFor(PromptKind.DailyPrep);
|
||||||
|
public string WeeklyReportPromptPath { get; } = PromptFiles.PathFor(PromptKind.WeeklyReport);
|
||||||
|
|
||||||
public FilesSettingsTabViewModel(WorkerClient worker) => _worker = worker;
|
public FilesSettingsTabViewModel(WorkerClient worker) => _worker = worker;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
|
|||||||
private readonly IPrimeScheduleApi _api;
|
private readonly IPrimeScheduleApi _api;
|
||||||
private readonly HashSet<Guid> _initialIds = new();
|
private readonly HashSet<Guid> _initialIds = new();
|
||||||
|
|
||||||
|
[ObservableProperty] private int _dailyPrepMaxTasks = 5;
|
||||||
|
|
||||||
public ObservableCollection<PrimeScheduleRowViewModel> Rows { get; } = new();
|
public ObservableCollection<PrimeScheduleRowViewModel> Rows { get; } = new();
|
||||||
|
|
||||||
public PrimeClaudeTabViewModel(IPrimeScheduleApi api) => _api = api;
|
public PrimeClaudeTabViewModel(IPrimeScheduleApi api) => _api = api;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
@@ -23,8 +24,26 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
||||||
|
|
||||||
|
private static readonly string[] TimeFormats = { @"h\:mm", @"hh\:mm" };
|
||||||
|
|
||||||
|
public string TimeText
|
||||||
|
{
|
||||||
|
get => TimeOfDay.ToString(@"hh\:mm", CultureInfo.InvariantCulture);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (TimeSpan.TryParseExact(value, TimeFormats, CultureInfo.InvariantCulture, out var t)
|
||||||
|
&& t >= TimeSpan.Zero && t < TimeSpan.FromDays(1))
|
||||||
|
{
|
||||||
|
TimeOfDay = t;
|
||||||
|
}
|
||||||
|
OnPropertyChanged(nameof(TimeText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
|
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
|
||||||
|
|
||||||
|
partial void OnTimeOfDayChanged(TimeSpan value) => OnPropertyChanged(nameof(TimeText));
|
||||||
|
|
||||||
public PrimeScheduleRowViewModel(PrimeScheduleDto dto, bool isExisting)
|
public PrimeScheduleRowViewModel(PrimeScheduleDto dto, bool isExisting)
|
||||||
{
|
{
|
||||||
Id = dto.Id;
|
Id = dto.Id;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
: string.Join(Environment.NewLine,
|
: string.Join(Environment.NewLine,
|
||||||
System.Text.Json.JsonSerializer.Deserialize<List<string>>(dto.ReportExcludedPaths) ?? new());
|
System.Text.Json.JsonSerializer.Deserialize<List<string>>(dto.ReportExcludedPaths) ?? new());
|
||||||
General.StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday;
|
General.StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday;
|
||||||
|
Prime.DailyPrepMaxTasks = dto.DailyPrepMaxTasks < 1 ? 5 : dto.DailyPrepMaxTasks;
|
||||||
}
|
}
|
||||||
else StatusMessage = Loc.T("vm.settingsModal.workerOffline");
|
else StatusMessage = Loc.T("vm.settingsModal.workerOffline");
|
||||||
|
|
||||||
@@ -91,7 +92,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
System.Text.Json.JsonSerializer.Serialize(
|
System.Text.Json.JsonSerializer.Serialize(
|
||||||
General.ReportExcludedPaths
|
General.ReportExcludedPaths
|
||||||
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
||||||
General.StandupWeekday);
|
General.StandupWeekday,
|
||||||
|
Prime.DailyPrepMaxTasks);
|
||||||
await _worker.UpdateAppSettingsAsync(dto);
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
await Prime.SaveAsync();
|
await Prime.SaveAsync();
|
||||||
CloseAction?.Invoke();
|
CloseAction?.Invoke();
|
||||||
|
|||||||
111
src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs
Normal file
111
src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
/// Shared unified-diff parser used by both the per-task diff viewer and the
|
||||||
|
/// combined (planning) diff viewer so they render identically.
|
||||||
|
public static class UnifiedDiffParser
|
||||||
|
{
|
||||||
|
public static List<DiffFileViewModel> Parse(string? raw)
|
||||||
|
{
|
||||||
|
var files = new List<DiffFileViewModel>();
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return files;
|
||||||
|
|
||||||
|
DiffFileViewModel? current = null;
|
||||||
|
int oldLine = 0, newLine = 0;
|
||||||
|
|
||||||
|
foreach (var line in raw.Split('\n'))
|
||||||
|
{
|
||||||
|
if (line.StartsWith("diff --git ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// e.g. "diff --git a/src/Foo.cs b/src/Foo.cs"
|
||||||
|
var parts = line.Split(' ');
|
||||||
|
var path = parts.Length >= 4 ? parts[3][2..] : line;
|
||||||
|
current = new DiffFileViewModel { Path = path };
|
||||||
|
files.Add(current);
|
||||||
|
oldLine = 0; newLine = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == null) continue;
|
||||||
|
|
||||||
|
if (line.StartsWith("@@ ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// e.g. "@@ -10,7 +10,9 @@"
|
||||||
|
ParseHunkHeader(line, out oldLine, out newLine);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip diff metadata lines
|
||||||
|
if (line.StartsWith("--- ", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("+++ ", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("index ", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("new file", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("deleted file", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("Binary ", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (line.StartsWith('+'))
|
||||||
|
{
|
||||||
|
current.Lines.Add(new DiffLineViewModel
|
||||||
|
{
|
||||||
|
Kind = DiffLineKind.Add,
|
||||||
|
NewNo = newLine++,
|
||||||
|
Text = line.Length > 1 ? line[1..] : "",
|
||||||
|
});
|
||||||
|
current.Additions++;
|
||||||
|
}
|
||||||
|
else if (line.StartsWith('-'))
|
||||||
|
{
|
||||||
|
current.Lines.Add(new DiffLineViewModel
|
||||||
|
{
|
||||||
|
Kind = DiffLineKind.Del,
|
||||||
|
OldNo = oldLine++,
|
||||||
|
Text = line.Length > 1 ? line[1..] : "",
|
||||||
|
});
|
||||||
|
current.Deletions++;
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(' '))
|
||||||
|
{
|
||||||
|
current.Lines.Add(new DiffLineViewModel
|
||||||
|
{
|
||||||
|
Kind = DiffLineKind.Ctx,
|
||||||
|
OldNo = oldLine++,
|
||||||
|
NewNo = newLine++,
|
||||||
|
Text = line.Length > 1 ? line[1..] : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flattens multiple parsed files into a single line stream, inserting a
|
||||||
|
/// file-header row before each file so boundaries are visible in a
|
||||||
|
/// single-pane (combined) view.
|
||||||
|
public static List<DiffLineViewModel> Flatten(IEnumerable<DiffFileViewModel> files)
|
||||||
|
{
|
||||||
|
var lines = new List<DiffLineViewModel>();
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
lines.Add(new DiffLineViewModel { Kind = DiffLineKind.File, Text = file.Path });
|
||||||
|
foreach (var line in file.Lines)
|
||||||
|
lines.Add(line);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseHunkHeader(string header, out int oldStart, out int newStart)
|
||||||
|
{
|
||||||
|
oldStart = 1; newStart = 1;
|
||||||
|
// Format: @@ -<old>,<count> +<new>,<count> @@
|
||||||
|
var at = header.IndexOf("@@", 3, StringComparison.Ordinal);
|
||||||
|
var inner = at > 0 ? header[3..at].Trim() : header;
|
||||||
|
var segs = inner.Split(' ');
|
||||||
|
foreach (var seg in segs)
|
||||||
|
{
|
||||||
|
if (seg.StartsWith('-') && int.TryParse(seg[1..].Split(',')[0], out var o))
|
||||||
|
oldStart = o;
|
||||||
|
else if (seg.StartsWith('+') && int.TryParse(seg[1..].Split(',')[0], out var n))
|
||||||
|
newStart = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Planning;
|
namespace ClaudeDo.Ui.ViewModels.Planning;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject
|
|||||||
private readonly string _targetBranch;
|
private readonly string _targetBranch;
|
||||||
|
|
||||||
public ObservableCollection<SubtaskDiffRow> Subtasks { get; } = new();
|
public ObservableCollection<SubtaskDiffRow> Subtasks { get; } = new();
|
||||||
|
public ObservableCollection<DiffLineViewModel> DiffLines { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private SubtaskDiffRow? _selectedSubtask;
|
[ObservableProperty] private SubtaskDiffRow? _selectedSubtask;
|
||||||
[ObservableProperty] private string _displayedDiff = "";
|
[ObservableProperty] private string _displayedDiff = "";
|
||||||
@@ -87,6 +89,13 @@ public sealed partial class PlanningDiffViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsCombinedModeChanged(bool value) => ToggleCombinedCommand.Execute(null);
|
partial void OnIsCombinedModeChanged(bool value) => ToggleCombinedCommand.Execute(null);
|
||||||
|
|
||||||
|
partial void OnDisplayedDiffChanged(string value)
|
||||||
|
{
|
||||||
|
DiffLines.Clear();
|
||||||
|
foreach (var line in UnifiedDiffParser.Flatten(UnifiedDiffParser.Parse(value)))
|
||||||
|
DiffLines.Add(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record SubtaskDiffRow(string Id, string Title, string? DiffStat, string UnifiedDiff);
|
public sealed record SubtaskDiffRow(string Id, string Title, string? DiffStat, string UnifiedDiff);
|
||||||
|
|||||||
82
src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml
Normal file
82
src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Controls.DiffLinesView"
|
||||||
|
x:Name="Root">
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<!-- diff line row tints via Tag selector (compiled-binding-friendly) -->
|
||||||
|
<Style Selector="Border.diff-line[Tag=add]">
|
||||||
|
<Setter Property="Background" Value="{StaticResource RunningTintBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=del]">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=ctx]">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=file]">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Surface3Brush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=add] TextBlock.diff-sign">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=del] TextBlock.diff-sign">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=ctx] TextBlock.diff-sign">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=add] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=del] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=ctx] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=file] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding #Root.Lines}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:DiffLineViewModel">
|
||||||
|
<Border Classes="diff-line"
|
||||||
|
Tag="{Binding ClassName}"
|
||||||
|
Padding="4,1">
|
||||||
|
<Grid ColumnDefinitions="48,48,16,*">
|
||||||
|
<!-- Old line number -->
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding OldNo}"
|
||||||
|
Classes="diff-lineno"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<!-- New line number -->
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding NewNo}"
|
||||||
|
Classes="diff-lineno"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<!-- Sign -->
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
Classes="diff-sign"
|
||||||
|
Text="{Binding Sign}"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="{StaticResource FontSizeMono}"/>
|
||||||
|
<!-- Line text -->
|
||||||
|
<TextBlock Grid.Column="3"
|
||||||
|
Classes="diff-text"
|
||||||
|
Text="{Binding Text}"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
|
TextWrapping="NoWrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</UserControl>
|
||||||
19
src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml.cs
Normal file
19
src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Controls;
|
||||||
|
|
||||||
|
public partial class DiffLinesView : UserControl
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<IEnumerable?> LinesProperty =
|
||||||
|
AvaloniaProperty.Register<DiffLinesView, IEnumerable?>(nameof(Lines));
|
||||||
|
|
||||||
|
public IEnumerable? Lines
|
||||||
|
{
|
||||||
|
get => GetValue(LinesProperty);
|
||||||
|
set => SetValue(LinesProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiffLinesView() => InitializeComponent();
|
||||||
|
}
|
||||||
13
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml
Normal file
13
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Controls.InheritedBadge"
|
||||||
|
x:Name="Root">
|
||||||
|
<Border Background="{DynamicResource Surface3Brush}"
|
||||||
|
CornerRadius="4" Padding="6,1"
|
||||||
|
IsVisible="{Binding #Root.BadgeText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Left">
|
||||||
|
<TextBlock FontSize="11"
|
||||||
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
|
Text="{Binding #Root.BadgeText}"/>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
18
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs
Normal file
18
src/ClaudeDo.Ui/Views/Controls/InheritedBadge.axaml.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Controls;
|
||||||
|
|
||||||
|
public partial class InheritedBadge : UserControl
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<string?> BadgeTextProperty =
|
||||||
|
AvaloniaProperty.Register<InheritedBadge, string?>(nameof(BadgeText));
|
||||||
|
|
||||||
|
public string? BadgeText
|
||||||
|
{
|
||||||
|
get => GetValue(BadgeTextProperty);
|
||||||
|
set => SetValue(BadgeTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InheritedBadge() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:DataType="vm:DetailsIslandViewModel">
|
||||||
<Border Classes="agent-strip"
|
<Border Classes="agent-strip"
|
||||||
Classes.running="{Binding IsRunning}"
|
Classes.running="{Binding IsRunning}"
|
||||||
|
Classes.children="{Binding IsWaitingForChildren}"
|
||||||
Margin="18,8,18,0">
|
Margin="18,8,18,0">
|
||||||
<StackPanel Margin="12,10" Spacing="6">
|
<StackPanel Margin="12,10" Spacing="6">
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:DataType="vm:DetailsIslandViewModel">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|
||||||
<!-- ── Metadata footer (sticky bottom) ── -->
|
<!-- ── Metadata footer (sticky bottom) — task detail only ── -->
|
||||||
<Border DockPanel.Dock="Bottom"
|
<Border DockPanel.Dock="Bottom"
|
||||||
|
IsVisible="{Binding IsTaskDetailVisible}"
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
BorderThickness="0,1,0,0"
|
BorderThickness="0,1,0,0"
|
||||||
Padding="14,8">
|
Padding="14,8">
|
||||||
@@ -35,8 +36,9 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ── Header (sticky top): check · eyebrow · title · status · star · gear ── -->
|
<!-- ── Header (sticky top): check · eyebrow · title · status · star · gear — task detail only ── -->
|
||||||
<Border DockPanel.Dock="Top" Classes="island-header">
|
<Border DockPanel.Dock="Top" Classes="island-header"
|
||||||
|
IsVisible="{Binding IsTaskDetailVisible}">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
<Button Grid.Column="0" Classes="flat"
|
<Button Grid.Column="0" Classes="flat"
|
||||||
Command="{Binding ToggleDoneCommand}"
|
Command="{Binding ToggleDoneCommand}"
|
||||||
@@ -86,24 +88,50 @@
|
|||||||
<TextBlock Text="{loc:Tr details.agentSettingsHeading}" FontWeight="SemiBold"/>
|
<TextBlock Text="{loc:Tr details.agentSettingsHeading}" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr details.modelLabel}"/>
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.modelLabel}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
|
||||||
|
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
|
Command="{Binding ResetTaskModelCommand}"/>
|
||||||
|
</Grid>
|
||||||
<ComboBox ItemsSource="{Binding TaskModelOptions}"
|
<ComboBox ItemsSource="{Binding TaskModelOptions}"
|
||||||
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
|
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{Binding ModelInheritedHint}"
|
||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
<TextBlock Classes="meta"
|
</StackPanel>
|
||||||
Text="{Binding EffectiveModelLabel}"
|
|
||||||
Opacity="0.6"/>
|
<StackPanel Spacing="2">
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.maxTurnsLabel}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
|
||||||
|
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
|
Command="{Binding ResetTaskTurnsCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
<NumericUpDown Value="{Binding TaskMaxTurns, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{Binding TurnsInheritedHint}"
|
||||||
|
Minimum="1" Maximum="200" Increment="1" FormatString="0"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
|
||||||
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
|
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
|
||||||
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"
|
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
|
||||||
PlaceholderText="{Binding EffectiveSystemPromptHint}"/>
|
<TextBlock Classes="meta" Opacity="0.6"
|
||||||
|
Text="{loc:Tr details.systemPromptPrepended}"
|
||||||
|
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="{Binding EffectiveSystemPromptHint}"
|
||||||
|
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="2">
|
<StackPanel Spacing="2">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr details.agentFileLabel}"/>
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.agentFileLabel}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentBadge}"/>
|
||||||
|
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
|
Command="{Binding ResetTaskAgentCommand}"/>
|
||||||
|
</Grid>
|
||||||
<ComboBox ItemsSource="{Binding TaskAgentOptions}"
|
<ComboBox ItemsSource="{Binding TaskAgentOptions}"
|
||||||
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
|
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
|
||||||
HorizontalAlignment="Stretch">
|
HorizontalAlignment="Stretch">
|
||||||
@@ -113,9 +141,6 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<TextBlock Classes="meta"
|
|
||||||
Text="{Binding EffectiveAgentLabel}"
|
|
||||||
Opacity="0.6"/>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Flyout>
|
</Flyout>
|
||||||
@@ -124,13 +149,14 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ── Agent status strip (sticky, above metadata footer) ── -->
|
<!-- ── Agent status strip (sticky, above metadata footer) — task detail only ── -->
|
||||||
<islands:AgentStripView DockPanel.Dock="Bottom"/>
|
<islands:AgentStripView DockPanel.Dock="Bottom"
|
||||||
|
IsVisible="{Binding IsTaskDetailVisible}"/>
|
||||||
|
|
||||||
<!-- ── Body: task details (normal) or notes editor (notes mode) ── -->
|
<!-- ── Body: task details (normal), notes editor (notes mode), or prep log (prep mode) ── -->
|
||||||
<Grid>
|
<Grid>
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
IsVisible="{Binding !IsNotesMode}">
|
IsVisible="{Binding IsTaskDetailVisible}">
|
||||||
<StackPanel Spacing="0">
|
<StackPanel Spacing="0">
|
||||||
|
|
||||||
<!-- Planning merge section — visible only for planning parent tasks -->
|
<!-- Planning merge section — visible only for planning parent tasks -->
|
||||||
@@ -159,6 +185,66 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Review section — visible when task is WaitingForReview -->
|
||||||
|
<Border Classes="section-divider"
|
||||||
|
IsVisible="{Binding IsWaitingForReview}">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Classes="section-label" Text="{loc:Tr tasks.reviewTitle}" Margin="0,0,0,2"/>
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr tasks.feedbackLabel}"/>
|
||||||
|
<TextBox Text="{Binding ReviewFeedback, Mode=TwoWay}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MinHeight="60"
|
||||||
|
MaxHeight="180"
|
||||||
|
PlaceholderText="{loc:Tr tasks.feedbackPlaceholder}"
|
||||||
|
Padding="8"
|
||||||
|
Background="{DynamicResource Surface2Brush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Classes="btn accent"
|
||||||
|
Content="{loc:Tr tasks.approve}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.approveTip}"
|
||||||
|
Command="{Binding ApproveReviewCommand}"/>
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr tasks.reject}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.rejectTip}"
|
||||||
|
Command="{Binding RejectReviewCommand}"/>
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr tasks.park}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.parkTip}"
|
||||||
|
Command="{Binding ParkReviewCommand}"/>
|
||||||
|
<Button Classes="btn"
|
||||||
|
Content="{loc:Tr tasks.cancel}"
|
||||||
|
ToolTip.Tip="{loc:Tr tasks.cancelTip}"
|
||||||
|
Command="{Binding CancelReviewCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Improvement-children outcomes — visible when this task has agent-suggested children -->
|
||||||
|
<Border Classes="section-divider"
|
||||||
|
IsVisible="{Binding HasChildOutcomes}">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Classes="section-label" Text="{loc:Tr details.childOutcomesLabel}" Margin="0,0,0,2"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding ChildOutcomes}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ChildOutcomeRowViewModel">
|
||||||
|
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,2">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding Title}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding RoadblockText}"
|
||||||
|
IsVisible="{Binding HasRoadblock}"
|
||||||
|
Foreground="#E0A030" Margin="8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding StatusLabel}"
|
||||||
|
Opacity="0.75" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Steps section -->
|
<!-- Steps section -->
|
||||||
<Border Classes="section-divider">
|
<Border Classes="section-divider">
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
@@ -292,13 +378,35 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Session terminal — auto-sizes to output, scrolls internally after MaxHeight -->
|
<!-- Session terminal — auto-sizes to output, scrolls internally after MaxHeight -->
|
||||||
<islands:SessionTerminalView MaxHeight="420"/>
|
<islands:SessionTerminalView MaxHeight="420"
|
||||||
|
Entries="{Binding Log}"
|
||||||
|
Label="{Binding BranchLine, StringFormat='claude-session · {0}'}"
|
||||||
|
IsRunning="{Binding IsRunning}" IsDone="{Binding IsDone}" IsFailed="{Binding IsFailed}"/>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
<Panel IsVisible="{Binding IsNotesMode}">
|
<Panel IsVisible="{Binding IsNotesMode}">
|
||||||
<islands:NotesEditorView DataContext="{Binding Notes}"/>
|
<islands:NotesEditorView DataContext="{Binding Notes}"/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<Panel IsVisible="{Binding IsPrepMode}">
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Top" Padding="12,8">
|
||||||
|
<Button Classes="btn primary"
|
||||||
|
Command="{Binding PlanDayCommand}"
|
||||||
|
IsEnabled="{Binding !IsPrepRunning}"
|
||||||
|
Content="{loc:Tr details.planDay}"/>
|
||||||
|
</Border>
|
||||||
|
<Panel>
|
||||||
|
<islands:SessionTerminalView
|
||||||
|
Entries="{Binding PrepLog}" Label="daily-prep"
|
||||||
|
IsRunning="{Binding IsPrepRunning}"/>
|
||||||
|
<TextBlock IsVisible="{Binding ShowPrepEmptyState}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
|
Text="{loc:Tr details.prepEmpty}"/>
|
||||||
|
</Panel>
|
||||||
|
</DockPanel>
|
||||||
|
</Panel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
x:Class="ClaudeDo.Ui.Views.Islands.SessionTerminalView"
|
x:Class="ClaudeDo.Ui.Views.Islands.SessionTerminalView"
|
||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:Name="Root">
|
||||||
<Border Classes="terminal" Margin="18,8,18,0">
|
<Border Classes="terminal" Margin="18,8,18,0">
|
||||||
<DockPanel LastChildFill="True">
|
<DockPanel LastChildFill="True">
|
||||||
|
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
<!-- Session label -->
|
<!-- Session label -->
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Classes="meta"
|
Classes="meta"
|
||||||
Text="{Binding BranchLine, StringFormat='claude-session · {0}'}"
|
Text="{Binding #Root.Label}"
|
||||||
LetterSpacing="0.8"
|
LetterSpacing="0.8"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<!-- LIVE chip -->
|
<!-- LIVE chip -->
|
||||||
<Border Grid.Column="2" Classes="live-chip pulsing"
|
<Border Grid.Column="2" Classes="live-chip pulsing"
|
||||||
IsVisible="{Binding IsRunning}"
|
IsVisible="{Binding #Root.IsRunning}"
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center">
|
Margin="0,0,8,0" VerticalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
||||||
<Ellipse VerticalAlignment="Center"/>
|
<Ellipse VerticalAlignment="Center"/>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<!-- DONE chip -->
|
<!-- DONE chip -->
|
||||||
<Border Grid.Column="2" Classes="live-chip done"
|
<Border Grid.Column="2" Classes="live-chip done"
|
||||||
IsVisible="{Binding IsDone}"
|
IsVisible="{Binding #Root.IsDone}"
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center">
|
Margin="0,0,8,0" VerticalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
||||||
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource MossBrush}"/>
|
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource MossBrush}"/>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<!-- FAILED chip -->
|
<!-- FAILED chip -->
|
||||||
<Border Grid.Column="2" Classes="live-chip failed"
|
<Border Grid.Column="2" Classes="live-chip failed"
|
||||||
IsVisible="{Binding IsFailed}"
|
IsVisible="{Binding #Root.IsFailed}"
|
||||||
Margin="0,0,8,0" VerticalAlignment="Center">
|
Margin="0,0,8,0" VerticalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
|
||||||
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource BloodBrush}"/>
|
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource BloodBrush}"/>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
VerticalScrollBarVisibility="Visible"
|
VerticalScrollBarVisibility="Visible"
|
||||||
AllowAutoHide="False"
|
AllowAutoHide="False"
|
||||||
Padding="10,8,10,12">
|
Padding="10,8,10,12">
|
||||||
<ItemsControl ItemsSource="{Binding Log}">
|
<ItemsControl ItemsSource="{Binding #Root.Entries}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="vm:LogLineViewModel">
|
<DataTemplate DataType="vm:LogLineViewModel">
|
||||||
<Grid ColumnDefinitions="60,*" Margin="0,1">
|
<Grid ColumnDefinitions="60,*" Margin="0,1">
|
||||||
|
|||||||
@@ -1,30 +1,50 @@
|
|||||||
|
using System.Collections;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.Islands;
|
namespace ClaudeDo.Ui.Views.Islands;
|
||||||
|
|
||||||
public partial class SessionTerminalView : UserControl
|
public partial class SessionTerminalView : UserControl
|
||||||
{
|
{
|
||||||
|
public static readonly StyledProperty<IEnumerable?> EntriesProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, IEnumerable?>(nameof(Entries));
|
||||||
|
public static readonly StyledProperty<string?> LabelProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(Label));
|
||||||
|
public static readonly StyledProperty<bool> IsRunningProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsRunning));
|
||||||
|
public static readonly StyledProperty<bool> IsDoneProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsDone));
|
||||||
|
public static readonly StyledProperty<bool> IsFailedProperty =
|
||||||
|
AvaloniaProperty.Register<SessionTerminalView, bool>(nameof(IsFailed));
|
||||||
|
|
||||||
|
public IEnumerable? Entries { get => GetValue(EntriesProperty); set => SetValue(EntriesProperty, value); }
|
||||||
|
public string? Label { get => GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
|
||||||
|
public bool IsRunning { get => GetValue(IsRunningProperty); set => SetValue(IsRunningProperty, value); }
|
||||||
|
public bool IsDone { get => GetValue(IsDoneProperty); set => SetValue(IsDoneProperty, value); }
|
||||||
|
public bool IsFailed { get => GetValue(IsFailedProperty); set => SetValue(IsFailedProperty, value); }
|
||||||
|
|
||||||
|
private INotifyCollectionChanged? _subscribedCollection;
|
||||||
|
|
||||||
public SessionTerminalView() { InitializeComponent(); }
|
public SessionTerminalView() { InitializeComponent(); }
|
||||||
|
|
||||||
private DetailsIslandViewModel? _boundVm;
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
|
||||||
{
|
{
|
||||||
base.OnDataContextChanged(e);
|
base.OnPropertyChanged(change);
|
||||||
if (_boundVm is not null)
|
if (change.Property != EntriesProperty) return;
|
||||||
_boundVm.Log.CollectionChanged -= OnLogChanged;
|
|
||||||
_boundVm = DataContext as DetailsIslandViewModel;
|
if (_subscribedCollection is not null)
|
||||||
if (_boundVm is not null)
|
_subscribedCollection.CollectionChanged -= OnEntriesChanged;
|
||||||
_boundVm.Log.CollectionChanged += OnLogChanged;
|
|
||||||
|
_subscribedCollection = change.NewValue as INotifyCollectionChanged;
|
||||||
|
|
||||||
|
if (_subscribedCollection is not null)
|
||||||
|
_subscribedCollection.CollectionChanged += OnEntriesChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void OnEntriesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Action != NotifyCollectionChangedAction.Add) return;
|
if (e.Action != NotifyCollectionChangedAction.Add) return;
|
||||||
// Scroll after the next layout pass so the freshly-added (wrapping) line
|
|
||||||
// is measured first — otherwise ScrollToEnd stops short and clips it.
|
|
||||||
EventHandler? handler = null;
|
EventHandler? handler = null;
|
||||||
handler = (_, _) =>
|
handler = (_, _) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -129,33 +129,31 @@
|
|||||||
<!-- Chip row -->
|
<!-- Chip row -->
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
|
||||||
|
<!-- Roadblock badge -->
|
||||||
|
<PathIcon Width="13" Height="13" VerticalAlignment="Center"
|
||||||
|
Data="{StaticResource Icon.Warning}"
|
||||||
|
Foreground="#E0A800"
|
||||||
|
IsVisible="{Binding HasRoadblock}"
|
||||||
|
ToolTip.Tip="{Binding RoadblockTooltip}"/>
|
||||||
|
|
||||||
|
<!-- Agent-suggested badge -->
|
||||||
|
<PathIcon Width="10" Height="10" VerticalAlignment="Center"
|
||||||
|
Data="{StaticResource Icon.AgentSuggested}"
|
||||||
|
Foreground="#5C8FA8"
|
||||||
|
IsVisible="{Binding IsAgentSuggested}"
|
||||||
|
ToolTip.Tip="Suggested by the agent"/>
|
||||||
|
|
||||||
<!-- Status chip -->
|
<!-- Status chip -->
|
||||||
<Border Classes="chip"
|
<Border Classes="chip"
|
||||||
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
|
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
|
||||||
Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForReview}"
|
Classes.review="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForReview}"
|
||||||
|
Classes.children="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=WaitingForChildren}"
|
||||||
Classes.done="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Done}"
|
Classes.done="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Done}"
|
||||||
Classes.error="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Failed}"
|
Classes.error="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Failed}"
|
||||||
Classes.queued="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Queued}">
|
Classes.queued="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Queued}">
|
||||||
<TextBlock Text="{Binding StatusLabel}"/>
|
<TextBlock Text="{Binding StatusLabel}"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Review actions (visible when WaitingForReview) -->
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4"
|
|
||||||
IsVisible="{Binding IsWaitingForReview}">
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.approve}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.approveTip}"
|
|
||||||
Click="OnApproveReviewClick"/>
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.reject}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.rejectTip}"
|
|
||||||
Click="OnRejectReviewClick"/>
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.park}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.parkTip}"
|
|
||||||
Click="OnParkReviewClick"/>
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.cancelTip}"
|
|
||||||
Click="OnCancelReviewClick"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
|
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
|
||||||
<Button Classes="icon-btn dequeue-btn"
|
<Button Classes="icon-btn dequeue-btn"
|
||||||
IsVisible="{Binding CanRemoveFromQueue}"
|
IsVisible="{Binding CanRemoveFromQueue}"
|
||||||
@@ -247,35 +245,5 @@
|
|||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Hidden reject-feedback anchor (its Flyout is shown from the Reject button) -->
|
|
||||||
<Button Grid.Row="1" x:Name="RejectAnchor"
|
|
||||||
Width="1" Height="1" Opacity="0"
|
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Top"
|
|
||||||
IsHitTestVisible="False" Focusable="False">
|
|
||||||
<Button.Flyout>
|
|
||||||
<Flyout Placement="Bottom" ShowMode="Standard">
|
|
||||||
<Border Background="{DynamicResource Surface2Brush}"
|
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="10"
|
|
||||||
Padding="16" Width="320">
|
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Classes="title" Text="{loc:Tr tasks.rejectRerunTitle}"/>
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Classes="eyebrow" Text="{loc:Tr tasks.feedbackLabel}"
|
|
||||||
Foreground="{DynamicResource TextDimBrush}" Opacity="0.6"/>
|
|
||||||
<TextBox x:Name="RejectFeedback"
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap"
|
|
||||||
MinHeight="80" PlaceholderText="{loc:Tr tasks.feedbackPlaceholder}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8"
|
|
||||||
HorizontalAlignment="Right" Margin="0,4,0,0">
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" Click="OnRejectCancelClick" MinWidth="76"/>
|
|
||||||
<Button Content="{loc:Tr tasks.rerun}" Classes="accent" Click="OnRejectConfirmClick" MinWidth="76"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Flyout>
|
|
||||||
</Button.Flyout>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -88,43 +88,6 @@ public partial class TaskRowView : UserControl
|
|||||||
await vm.SetStatusOnRowAsync(row, status);
|
await vm.SetStatusOnRowAsync(row, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnApproveReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.ApproveReviewCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnParkReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.RejectReviewToIdleCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnCancelReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.CancelReviewCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRejectReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not TaskRowViewModel) return;
|
|
||||||
RejectFeedback.Text = "";
|
|
||||||
RejectAnchor.Flyout?.ShowAt(RejectAnchor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnRejectConfirmClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
RejectAnchor.Flyout?.Hide();
|
|
||||||
if (DataContext is not TaskRowViewModel row || FindTasksVm() is not { } vm) return;
|
|
||||||
var feedback = RejectFeedback.Text ?? "";
|
|
||||||
if (string.IsNullOrWhiteSpace(feedback)) return;
|
|
||||||
await vm.RejectReviewToQueueAsync(row, feedback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRejectCancelClick(object? sender, RoutedEventArgs e)
|
|
||||||
=> RejectAnchor.Flyout?.Hide();
|
|
||||||
|
|
||||||
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not TaskRowViewModel row) return;
|
if (DataContext is not TaskRowViewModel row) return;
|
||||||
|
|||||||
@@ -28,14 +28,21 @@
|
|||||||
IsVisible="{Binding HasStatusPill}">
|
IsVisible="{Binding HasStatusPill}">
|
||||||
<TextBlock Text="{Binding StatusPill}"/>
|
<TextBlock Text="{Binding StatusPill}"/>
|
||||||
</Border>
|
</Border>
|
||||||
<Button Classes="icon-btn" Command="{Binding SortCommand}" ToolTip.Tip="{loc:Tr tasks.sortTip}">
|
|
||||||
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Sort}"/>
|
|
||||||
</Button>
|
|
||||||
<Button Classes="icon-btn" Classes.active="{Binding IsShowingCompleted}"
|
<Button Classes="icon-btn" Classes.active="{Binding IsShowingCompleted}"
|
||||||
Command="{Binding ToggleShowCompletedCommand}"
|
Command="{Binding ToggleShowCompletedCommand}"
|
||||||
ToolTip.Tip="{loc:Tr tasks.showCompletedTip}">
|
ToolTip.Tip="{loc:Tr tasks.showCompletedTip}">
|
||||||
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Eye}"/>
|
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Eye}"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
|
||||||
|
Command="{Binding ClearDayCommand}" ToolTip.Tip="{loc:Tr tasks.clearDayTip}">
|
||||||
|
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Broom}"/>
|
||||||
|
</Button>
|
||||||
|
<Button Classes="icon-btn" IsVisible="{Binding IsMyDayList}"
|
||||||
|
Command="{Binding ShowPrepLogCommand}" ToolTip.Tip="{loc:Tr tasks.planMyDayTip}">
|
||||||
|
<Viewbox Width="18" Height="18">
|
||||||
|
<Path Classes="plan-icon" Data="{StaticResource Icon.PlanDay}"/>
|
||||||
|
</Viewbox>
|
||||||
|
</Button>
|
||||||
<Button Classes="icon-btn" Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}">
|
<Button Classes="icon-btn" Command="{Binding OpenListSettingsCommand}" ToolTip.Tip="{loc:Tr tasks.listSettingsTip}">
|
||||||
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Settings}"/>
|
<PathIcon Width="15" Height="15" Data="{StaticResource Icon.Settings}"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,37 +18,6 @@
|
|||||||
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
|
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
|
||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
|
|
||||||
<Window.Styles>
|
|
||||||
<!-- diff line row tints via Tag selector (compiled-binding-friendly) -->
|
|
||||||
<Style Selector="Border.diff-line[Tag=add]">
|
|
||||||
<Setter Property="Background" Value="{StaticResource RunningTintBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=del]">
|
|
||||||
<Setter Property="Background" Value="{StaticResource ErrorTintBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=ctx]">
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=add] TextBlock.diff-sign">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=del] TextBlock.diff-sign">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=ctx] TextBlock.diff-sign">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=add] TextBlock.diff-text">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=del] TextBlock.diff-text">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="Border.diff-line[Tag=ctx] TextBlock.diff-text">
|
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}"/>
|
|
||||||
</Style>
|
|
||||||
</Window.Styles>
|
|
||||||
|
|
||||||
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}">
|
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}">
|
||||||
<ctl:ModalShell.Footer>
|
<ctl:ModalShell.Footer>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8"
|
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||||
@@ -99,43 +68,7 @@
|
|||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<ItemsControl ItemsSource="{Binding SelectedFile.Lines}">
|
<ctl:DiffLinesView Lines="{Binding SelectedFile.Lines}"/>
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:DiffLineViewModel">
|
|
||||||
<Border Classes="diff-line"
|
|
||||||
Tag="{Binding ClassName}"
|
|
||||||
Padding="4,1">
|
|
||||||
<Grid ColumnDefinitions="48,48,16,*">
|
|
||||||
<!-- Old line number -->
|
|
||||||
<TextBlock Grid.Column="0"
|
|
||||||
Text="{Binding OldNo}"
|
|
||||||
Classes="diff-lineno"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Margin="0,0,8,0"/>
|
|
||||||
<!-- New line number -->
|
|
||||||
<TextBlock Grid.Column="1"
|
|
||||||
Text="{Binding NewNo}"
|
|
||||||
Classes="diff-lineno"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Margin="0,0,8,0"/>
|
|
||||||
<!-- Sign -->
|
|
||||||
<TextBlock Grid.Column="2"
|
|
||||||
Classes="diff-sign"
|
|
||||||
Text="{Binding Sign}"
|
|
||||||
FontFamily="{DynamicResource MonoFont}"
|
|
||||||
FontSize="{StaticResource FontSizeMono}"/>
|
|
||||||
<!-- Line text -->
|
|
||||||
<TextBlock Grid.Column="3"
|
|
||||||
Classes="diff-text"
|
|
||||||
Text="{Binding Text}"
|
|
||||||
FontFamily="{DynamicResource MonoFont}"
|
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
|
||||||
TextWrapping="NoWrap"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -74,12 +74,31 @@
|
|||||||
<Border Classes="section">
|
<Border Classes="section">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.model}"/>
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.model}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
|
||||||
|
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
|
Command="{Binding ResetModelCommand}" Padding="6,1"/>
|
||||||
|
</Grid>
|
||||||
<ComboBox ItemsSource="{Binding ModelOptions}"
|
<ComboBox ItemsSource="{Binding ModelOptions}"
|
||||||
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
|
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{Binding ModelInheritedHint}"
|
||||||
HorizontalAlignment="Left" MinWidth="160" />
|
HorizontalAlignment="Left" MinWidth="160" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.maxTurns}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
|
||||||
|
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
|
Command="{Binding ResetTurnsCommand}" Padding="6,1"/>
|
||||||
|
</Grid>
|
||||||
|
<NumericUpDown Value="{Binding MaxTurns, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{Binding TurnsInheritedHint}"
|
||||||
|
Minimum="1" Maximum="200" Increment="1" FormatString="0"
|
||||||
|
HorizontalAlignment="Left" Width="160"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.systemPrompt}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.systemPrompt}"/>
|
||||||
<TextBox Text="{Binding SystemPrompt, Mode=TwoWay}"
|
<TextBox Text="{Binding SystemPrompt, Mode=TwoWay}"
|
||||||
@@ -88,7 +107,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.agentFile}"/>
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.agentFile}" VerticalAlignment="Center"/>
|
||||||
|
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentBadge}"/>
|
||||||
|
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
|
||||||
|
Command="{Binding ResetAgentCommand}" Padding="6,1"/>
|
||||||
|
</Grid>
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<ComboBox Grid.Column="0"
|
<ComboBox Grid.Column="0"
|
||||||
ItemsSource="{Binding Agents}"
|
ItemsSource="{Binding Agents}"
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.files.promptsSection}"/>
|
<TextBlock Classes="section-label" Text="{loc:Tr settings.files.promptsSection}"/>
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8">
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="120,*,Auto" RowSpacing="8">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.systemPrompt}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.systemPrompt}" VerticalAlignment="Center"/>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" Text="{Binding Files.SystemPromptPath}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" Text="{Binding Files.SystemPromptPath}" VerticalAlignment="Center"/>
|
||||||
<Button Classes="btn" Grid.Row="0" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
<Button Classes="btn" Grid.Row="0" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
||||||
@@ -190,10 +190,22 @@
|
|||||||
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningPromptPath}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningPromptPath}" VerticalAlignment="Center"/>
|
||||||
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
<Button Classes="btn" Grid.Row="1" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
||||||
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Planning"/>
|
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Planning"/>
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.agentPrompt}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.planningInitialPrompt}" VerticalAlignment="Center"/>
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono" Text="{Binding Files.AgentPromptPath}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningInitialPromptPath}" VerticalAlignment="Center"/>
|
||||||
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
<Button Classes="btn" Grid.Row="2" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
||||||
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Agent"/>
|
Command="{Binding Files.OpenPromptCommand}" CommandParameter="PlanningInitial"/>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.retryPrompt}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Classes="path-mono" Text="{Binding Files.RetryPromptPath}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="btn" Grid.Row="3" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
||||||
|
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Retry"/>
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.dailyPrepPrompt}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="1" Classes="path-mono" Text="{Binding Files.DailyPrepPromptPath}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="btn" Grid.Row="4" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
||||||
|
Command="{Binding Files.OpenPromptCommand}" CommandParameter="DailyPrep"/>
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.files.weeklyReportPrompt}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="1" Classes="path-mono" Text="{Binding Files.WeeklyReportPromptPath}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="btn" Grid.Row="5" Grid.Column="2" Content="{loc:Tr settings.files.openInEditor}"
|
||||||
|
Command="{Binding Files.OpenPromptCommand}" CommandParameter="WeeklyReport"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Classes="meta" Text="{Binding Files.StatusMessage}"
|
<TextBlock Classes="meta" Text="{Binding Files.StatusMessage}"
|
||||||
@@ -224,10 +236,10 @@
|
|||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TimePicker Grid.Column="2"
|
<TextBox Grid.Column="2"
|
||||||
SelectedTime="{Binding TimeOfDay, Mode=TwoWay}"
|
Text="{Binding TimeText, Mode=TwoWay}"
|
||||||
ClockIdentifier="24HourClock" MinuteIncrement="5"
|
PlaceholderText="HH:mm" MaxLength="5"
|
||||||
VerticalAlignment="Center"/>
|
Width="68" VerticalAlignment="Center"/>
|
||||||
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||||||
MinWidth="80"/>
|
MinWidth="80"/>
|
||||||
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
||||||
@@ -239,6 +251,11 @@
|
|||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<Button Classes="btn" Content="{loc:Tr settings.prime.addSchedule}" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
|
<Button Classes="btn" Content="{loc:Tr settings.prime.addSchedule}" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.prime.dailyPrepMaxTasks}" VerticalAlignment="Center"/>
|
||||||
|
<NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100" FormatString="0"
|
||||||
|
Value="{Binding Prime.DailyPrepMaxTasks, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|||||||
@@ -66,14 +66,7 @@
|
|||||||
<Grid Grid.Column="1" Background="{DynamicResource VoidBrush}">
|
<Grid Grid.Column="1" Background="{DynamicResource VoidBrush}">
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
<ScrollViewer HorizontalScrollBarVisibility="Auto"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<TextBox Text="{Binding DisplayedDiff, Mode=OneWay}"
|
<ctl:DiffLinesView Lines="{Binding DiffLines}"/>
|
||||||
IsReadOnly="True"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
FontFamily="{DynamicResource MonoFont}"
|
|
||||||
FontSize="{StaticResource FontSizeBody}"
|
|
||||||
Background="Transparent"
|
|
||||||
BorderThickness="0"
|
|
||||||
Padding="8"/>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Worker/
|
|||||||
External/ — ExternalMcpService
|
External/ — ExternalMcpService
|
||||||
Hub/ — WorkerHub, HubBroadcaster
|
Hub/ — WorkerHub, HubBroadcaster
|
||||||
Report/ — ClaudeHistoryReader, WeekReportPromptBuilder, WeekReportService; interfaces in Report/Interfaces/
|
Report/ — ClaudeHistoryReader, WeekReportPromptBuilder, WeekReportService; interfaces in Report/Interfaces/
|
||||||
|
Prime/ — daily-prep ("Prime Claude"): PrimeScheduler (BackgroundService), PrimeRunner (runs the daily prep), DailyPrepPrompt (fixed prompt + CLI args + LogPath() helper), NextDueCalculator, PrimeScheduleSignal; interfaces in Prime/Interfaces/ (IPrimeRunner, IPrimeClock, IPrimeScheduleSignal, IPrimeBroadcaster)
|
||||||
```
|
```
|
||||||
|
|
||||||
Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `Interfaces/` subfolder within their area; the namespace stays the area namespace.
|
Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `Interfaces/` subfolder within their area; the namespace stays the area namespace.
|
||||||
@@ -35,6 +36,21 @@ Interfaces (e.g. `IQueueWaker`, `IPrimeClock`, `ITaskStateService`) live in an `
|
|||||||
- `AgentMcpTools` — `ListAgents`
|
- `AgentMcpTools` — `ListAgents`
|
||||||
- `LifecycleMcpTools` — `ResetFailedTask`
|
- `LifecycleMcpTools` — `ResetFailedTask`
|
||||||
- `AppSettingsMcpTools` — `GetAppSettings` (read-only)
|
- `AppSettingsMcpTools` — `GetAppSettings` (read-only)
|
||||||
|
- `ExternalMcpService` also exposes two daily-prep tools:
|
||||||
|
- `GetDailyPrepCandidates` — returns Idle, non-blocked tasks in a git repo NOT excluded by `AppSettings.ReportExcludedPaths` and not already `IsMyDay`, plus the current Idle MyDay tasks and `maxTasks` (= `DailyPrepMaxTasks`). Repo-exclusion logic lives in the `DailyPrepFilter` helper (same file).
|
||||||
|
- `SetMyDay` — sets a task's `IsMyDay` (+ optional `SortOrder`); server-side cap-guard rejects turning on MyDay beyond `DailyPrepMaxTasks` open (Idle) MyDay tasks.
|
||||||
|
|
||||||
|
## Daily Prep (Prime Claude)
|
||||||
|
|
||||||
|
- **PrimeScheduler** (hosted `BackgroundService`) computes the next due time from the `prime_schedules` table and at that time calls `IPrimeRunner.FireAsync`. A manual run arrives via `WorkerHub.RunDailyPrepNow`. A `SemaphoreSlim` single-flight gate **in `PrimeRunner`** prevents overlapping runs (returns "already running"); both scheduled and manual runs go through it.
|
||||||
|
- **PrimeRunner** builds a fixed prompt via `DailyPrepPrompt.BuildPrompt`, parameterized by `AppSettings.DailyPrepMaxTasks` and today's date, then invokes:
|
||||||
|
```
|
||||||
|
claude -p --output-format stream-json --verbose --permission-mode acceptEdits --max-turns 30
|
||||||
|
--allowedTools mcp__claudedo__get_daily_prep_candidates mcp__claudedo__set_my_day
|
||||||
|
```
|
||||||
|
It relies on the globally-registered `claudedo` MCP (installer's `RegisterMcpStep`) — no separate `--mcp-config`. This replaced the old warm-up "ping".
|
||||||
|
- Each stdout line is streamed to the UI via `IPrimeBroadcaster.PrepLineAsync` AND written to `DailyPrepPrompt.LogPath()` = `<appdata>/logs/daily-prep.log` (truncated at the start of each run → last run only). `PrepStarted`/`PrepFinished` events bracket the run.
|
||||||
|
- Agentic behaviour: Claude calls `get_daily_prep_candidates`, picks an effort-aware subset capped at `DailyPrepMaxTasks`, and marks them via `set_my_day` (which broadcasts `TaskUpdated` so the UI updates live).
|
||||||
|
|
||||||
## Status Model
|
## Status Model
|
||||||
|
|
||||||
@@ -105,9 +121,9 @@ Each CLI invocation is recorded in the `task_runs` table via `TaskRunRepository`
|
|||||||
|
|
||||||
## SignalR Hub
|
## SignalR Hub
|
||||||
|
|
||||||
**WorkerHub** methods: `Ping`, `GetActive`, `RunNow`, `CancelTask`, `WakeQueue`, `ContinueTask`, `ResetTask`, `ApproveReview`, `RejectReviewToQueue`, `RejectReviewToIdle`, `CancelReview`, `GetAgents`, `RefreshAgents`, `GetAppSettings`, `UpdateAppSettings`, `CleanupFinishedWorktrees`, `ResetAllWorktrees`, `MergeTask`, `GetMergeTargets`, `UpdateList`, `UpdateListConfig`, `GetListConfig`, `UpdateTaskAgentSettings`, `GetWeekReport`, `GenerateWeekReport`, `GetDailyNotes`, `AddDailyNote`, `UpdateDailyNote`, `DeleteDailyNote`
|
**WorkerHub** methods: `Ping`, `GetActive`, `RunNow`, `CancelTask`, `WakeQueue`, `ContinueTask`, `ResetTask`, `ApproveReview`, `RejectReviewToQueue`, `RejectReviewToIdle`, `CancelReview`, `GetAgents`, `RefreshAgents`, `GetAppSettings`, `UpdateAppSettings`, `CleanupFinishedWorktrees`, `ResetAllWorktrees`, `MergeTask`, `GetMergeTargets`, `UpdateList`, `UpdateListConfig`, `GetListConfig`, `UpdateTaskAgentSettings`, `GetWeekReport`, `GenerateWeekReport`, `GetDailyNotes`, `AddDailyNote`, `UpdateDailyNote`, `DeleteDailyNote`, `RunDailyPrepNow`, `ClearMyDay`, `GetLastPrepLog`
|
||||||
|
|
||||||
**HubBroadcaster** events: `TaskStarted`, `TaskFinished`, `TaskMessage`, `WorktreeUpdated`, `TaskUpdated`, `RunCreated`, `ListUpdated`
|
**HubBroadcaster** events: `TaskStarted`, `TaskFinished`, `TaskMessage`, `WorktreeUpdated`, `TaskUpdated`, `RunCreated`, `ListUpdated`, `PrimeFired`, `PrepStarted`, `PrepLine`, `PrepFinished`
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
|
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
|
||||||
|
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
18
src/ClaudeDo.Worker/External/ConfigMcpTools.cs
vendored
18
src/ClaudeDo.Worker/External/ConfigMcpTools.cs
vendored
@@ -6,7 +6,7 @@ using ModelContextProtocol.Server;
|
|||||||
|
|
||||||
namespace ClaudeDo.Worker.External;
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath);
|
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class ConfigMcpTools
|
public sealed class ConfigMcpTools
|
||||||
@@ -26,12 +26,12 @@ public sealed class ConfigMcpTools
|
|||||||
public async Task<TaskConfigDto?> GetListConfig(string listId, CancellationToken cancellationToken)
|
public async Task<TaskConfigDto?> GetListConfig(string listId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
||||||
return cfg is null ? null : new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath);
|
return cfg is null ? null : new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Set a list's default model/system prompt/agent path. Passing all three as null clears the list config.")]
|
[McpServerTool, Description("Set a list's default model/system prompt/agent path/max turns. Passing all four as null clears the list config.")]
|
||||||
public async Task SetListConfig(
|
public async Task SetListConfig(
|
||||||
string listId, string? model, string? systemPrompt, string? agentPath, CancellationToken cancellationToken)
|
string listId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"List {listId} not found.");
|
?? throw new InvalidOperationException($"List {listId} not found.");
|
||||||
@@ -40,25 +40,25 @@ public sealed class ConfigMcpTools
|
|||||||
var sp = systemPrompt.NullIfBlank();
|
var sp = systemPrompt.NullIfBlank();
|
||||||
var ap = agentPath.NullIfBlank();
|
var ap = agentPath.NullIfBlank();
|
||||||
|
|
||||||
if (m is null && sp is null && ap is null)
|
if (m is null && sp is null && ap is null && maxTurns is null)
|
||||||
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
||||||
else
|
else
|
||||||
await _lists.SetConfigAsync(new ListConfigEntity
|
await _lists.SetConfigAsync(new ListConfigEntity
|
||||||
{
|
{
|
||||||
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap,
|
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = maxTurns,
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
await _broadcaster.ListUpdated(listId);
|
await _broadcaster.ListUpdated(listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path). Pass null for any field to clear that override.")]
|
[McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to clear that override.")]
|
||||||
public async Task SetTaskConfig(
|
public async Task SetTaskConfig(
|
||||||
string taskId, string? model, string? systemPrompt, string? agentPath, CancellationToken cancellationToken)
|
string taskId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), cancellationToken);
|
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, cancellationToken);
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/ClaudeDo.Worker/External/ExternalMcpService.cs
vendored
118
src/ClaudeDo.Worker/External/ExternalMcpService.cs
vendored
@@ -30,7 +30,9 @@ public sealed record TaskDto(
|
|||||||
string? CreatedBy,
|
string? CreatedBy,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
DateTime? StartedAt,
|
DateTime? StartedAt,
|
||||||
DateTime? FinishedAt);
|
DateTime? FinishedAt,
|
||||||
|
bool IsMyDay,
|
||||||
|
int SortOrder);
|
||||||
|
|
||||||
public sealed record WorktreeInfoDto(
|
public sealed record WorktreeInfoDto(
|
||||||
string Path, string Branch, string HeadCommit, string BaseCommit,
|
string Path, string Branch, string HeadCommit, string BaseCommit,
|
||||||
@@ -49,6 +51,15 @@ public sealed record WorktreeListItemDto(
|
|||||||
public sealed record CleanupWorktreeResult(
|
public sealed record CleanupWorktreeResult(
|
||||||
bool Removed, string WorktreePath, bool BranchDeleted);
|
bool Removed, string WorktreePath, bool BranchDeleted);
|
||||||
|
|
||||||
|
public sealed record DailyPrepCandidateDto(
|
||||||
|
string Id, string ListId, string ListName, string Title, string? Description,
|
||||||
|
bool IsStarred, DateTime? ScheduledFor, DateTime CreatedAt);
|
||||||
|
|
||||||
|
public sealed record DailyPrepDataDto(
|
||||||
|
int MaxTasks,
|
||||||
|
IReadOnlyList<DailyPrepCandidateDto> Candidates,
|
||||||
|
IReadOnlyList<DailyPrepCandidateDto> CurrentMyDay);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class ExternalMcpService
|
public sealed class ExternalMcpService
|
||||||
{
|
{
|
||||||
@@ -482,6 +493,83 @@ public sealed class ExternalMcpService
|
|||||||
return new CleanupWorktreeResult(result.Removed, path, result.Removed);
|
return new CleanupWorktreeResult(result.Removed, path, result.Removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Daily prep ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Daily prep: returns the open tasks eligible for today's MyDay selection. " +
|
||||||
|
"candidates = Idle, not blocked, in a git repo not excluded from the weekly report, and not already in MyDay. " +
|
||||||
|
"currentMyDay = Idle tasks already flagged IsMyDay (count them toward the cap). " +
|
||||||
|
"maxTasks = the hard cap on total open MyDay tasks. Use set_my_day to add tasks (never exceed maxTasks).")]
|
||||||
|
public async Task<DailyPrepDataDto> GetDailyPrepCandidates(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
|
var excludes = DailyPrepFilter.ParseExcludes(settings.ReportExcludedPaths);
|
||||||
|
var maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
|
||||||
|
var idle = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.List)
|
||||||
|
.Where(t => t.Status == TaskStatus.Idle)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var currentMyDay = idle
|
||||||
|
.Where(t => t.IsMyDay)
|
||||||
|
.OrderBy(t => t.SortOrder)
|
||||||
|
.Select(ToCandidate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var candidates = idle
|
||||||
|
.Where(t => !t.IsMyDay
|
||||||
|
&& t.BlockedByTaskId == null
|
||||||
|
&& DailyPrepFilter.IsIncludedRepo(t.List?.WorkingDir, excludes))
|
||||||
|
.OrderBy(t => t.CreatedAt)
|
||||||
|
.Select(ToCandidate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new DailyPrepDataDto(maxTasks, candidates, currentMyDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description(
|
||||||
|
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
||||||
|
"(use consecutive sortOrder values to keep related tasks together). " +
|
||||||
|
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
||||||
|
"clearing (isMyDay=false) is always allowed.")]
|
||||||
|
public async Task<TaskDto> SetMyDay(
|
||||||
|
string taskId,
|
||||||
|
bool isMyDay,
|
||||||
|
int? sortOrder,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var task = await ctx.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
if (isMyDay && !task.IsMyDay)
|
||||||
|
{
|
||||||
|
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
|
||||||
|
var max = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
var openMyDay = await ctx.Tasks.CountAsync(
|
||||||
|
t => t.IsMyDay && t.Status == TaskStatus.Idle, cancellationToken);
|
||||||
|
if (openMyDay >= max)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"MyDay limit {max} reached. Clear a task before adding another.");
|
||||||
|
}
|
||||||
|
|
||||||
|
task.IsMyDay = isMyDay;
|
||||||
|
if (sortOrder is not null) task.SortOrder = sortOrder.Value;
|
||||||
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return ToDto(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
||||||
|
t.Id, t.ListId, t.List?.Name ?? "", t.Title, t.Description,
|
||||||
|
t.IsStarred, t.ScheduledFor, t.CreatedAt);
|
||||||
|
|
||||||
// ── Private helpers ───────────────────────────────────────────────────────
|
// ── Private helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task<(TaskEntity Task, ListEntity List, WorktreeEntity Wt)> LoadWorktreeContextAsync(
|
private async Task<(TaskEntity Task, ListEntity List, WorktreeEntity Wt)> LoadWorktreeContextAsync(
|
||||||
@@ -567,5 +655,31 @@ public sealed class ExternalMcpService
|
|||||||
t.CreatedBy,
|
t.CreatedBy,
|
||||||
t.CreatedAt,
|
t.CreatedAt,
|
||||||
t.StartedAt,
|
t.StartedAt,
|
||||||
t.FinishedAt);
|
t.FinishedAt,
|
||||||
|
t.IsMyDay,
|
||||||
|
t.SortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class DailyPrepFilter
|
||||||
|
{
|
||||||
|
public static string[] ParseExcludes(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||||
|
return list is null ? [] : list.Select(Normalize).Where(p => p.Length > 0).ToArray();
|
||||||
|
}
|
||||||
|
catch (System.Text.Json.JsonException) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsIncludedRepo(string? workingDir, string[] excludes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(workingDir)) return false;
|
||||||
|
var norm = Normalize(workingDir);
|
||||||
|
return !excludes.Any(p => norm.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string path) =>
|
||||||
|
path.Trim().Replace('/', '\\').TrimEnd('\\');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,4 +54,12 @@ public sealed class HubBroadcaster : IPrimeBroadcaster
|
|||||||
|
|
||||||
Task IPrimeBroadcaster.PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt) =>
|
Task IPrimeBroadcaster.PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt) =>
|
||||||
PrimeFired(scheduleId, success, message, firedAt);
|
PrimeFired(scheduleId, success, message, firedAt);
|
||||||
|
|
||||||
|
public Task PrepStarted() => _hub.Clients.All.SendAsync("PrepStarted");
|
||||||
|
public Task PrepLine(string line) => _hub.Clients.All.SendAsync("PrepLine", line);
|
||||||
|
public Task PrepFinished(bool success) => _hub.Clients.All.SendAsync("PrepFinished", success);
|
||||||
|
|
||||||
|
Task IPrimeBroadcaster.PrepStartedAsync() => PrepStarted();
|
||||||
|
Task IPrimeBroadcaster.PrepLineAsync(string line) => PrepLine(line);
|
||||||
|
Task IPrimeBroadcaster.PrepFinishedAsync(bool success) => PrepFinished(success);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ public record AppSettingsDto(
|
|||||||
bool WorktreeAutoCleanupEnabled,
|
bool WorktreeAutoCleanupEnabled,
|
||||||
int WorktreeAutoCleanupDays,
|
int WorktreeAutoCleanupDays,
|
||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday);
|
int StandupWeekday,
|
||||||
|
int DailyPrepMaxTasks);
|
||||||
|
|
||||||
public record WorktreeCleanupDto(int Removed);
|
public record WorktreeCleanupDto(int Removed);
|
||||||
public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
@@ -54,9 +55,9 @@ public record ForceRemoveResultDto(bool Removed, string? Reason);
|
|||||||
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
|
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
|
||||||
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
||||||
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
||||||
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath);
|
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath);
|
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath);
|
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public record SeedResultDto(int Copied, int Skipped);
|
public record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||||
@@ -79,6 +80,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly PlanningMergeOrchestrator _planningMergeOrchestrator;
|
private readonly PlanningMergeOrchestrator _planningMergeOrchestrator;
|
||||||
private readonly PlanningChainCoordinator _planningChain;
|
private readonly PlanningChainCoordinator _planningChain;
|
||||||
private readonly IPrimeScheduleSignal _primeSignal;
|
private readonly IPrimeScheduleSignal _primeSignal;
|
||||||
|
private readonly IPrimeRunner _primeRunner;
|
||||||
private readonly ITaskStateService _state;
|
private readonly ITaskStateService _state;
|
||||||
private readonly IWeekReportService _report;
|
private readonly IWeekReportService _report;
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
PlanningMergeOrchestrator planningMergeOrchestrator,
|
PlanningMergeOrchestrator planningMergeOrchestrator,
|
||||||
PlanningChainCoordinator planningChain,
|
PlanningChainCoordinator planningChain,
|
||||||
IPrimeScheduleSignal primeSignal,
|
IPrimeScheduleSignal primeSignal,
|
||||||
|
IPrimeRunner primeRunner,
|
||||||
ITaskStateService state,
|
ITaskStateService state,
|
||||||
IWeekReportService report)
|
IWeekReportService report)
|
||||||
{
|
{
|
||||||
@@ -116,6 +119,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_planningMergeOrchestrator = planningMergeOrchestrator;
|
_planningMergeOrchestrator = planningMergeOrchestrator;
|
||||||
_planningChain = planningChain;
|
_planningChain = planningChain;
|
||||||
_primeSignal = primeSignal;
|
_primeSignal = primeSignal;
|
||||||
|
_primeRunner = primeRunner;
|
||||||
_state = state;
|
_state = state;
|
||||||
_report = report;
|
_report = report;
|
||||||
}
|
}
|
||||||
@@ -217,7 +221,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
row.WorktreeAutoCleanupEnabled,
|
row.WorktreeAutoCleanupEnabled,
|
||||||
row.WorktreeAutoCleanupDays,
|
row.WorktreeAutoCleanupDays,
|
||||||
row.ReportExcludedPaths,
|
row.ReportExcludedPaths,
|
||||||
row.StandupWeekday);
|
row.StandupWeekday,
|
||||||
|
row.DailyPrepMaxTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAppSettings(AppSettingsDto dto)
|
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||||
@@ -238,6 +243,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays,
|
WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays,
|
||||||
ReportExcludedPaths = dto.ReportExcludedPaths,
|
ReportExcludedPaths = dto.ReportExcludedPaths,
|
||||||
StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday,
|
StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday,
|
||||||
|
DailyPrepMaxTasks = dto.DailyPrepMaxTasks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +340,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
||||||
var agentPath = dto.AgentPath.NullIfBlank();
|
var agentPath = dto.AgentPath.NullIfBlank();
|
||||||
|
|
||||||
if (model is null && systemPrompt is null && agentPath is null)
|
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null)
|
||||||
{
|
{
|
||||||
await repo.DeleteConfigAsync(dto.ListId);
|
await repo.DeleteConfigAsync(dto.ListId);
|
||||||
}
|
}
|
||||||
@@ -346,6 +352,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
Model = model,
|
Model = model,
|
||||||
SystemPrompt = systemPrompt,
|
SystemPrompt = systemPrompt,
|
||||||
AgentPath = agentPath,
|
AgentPath = agentPath,
|
||||||
|
MaxTurns = dto.MaxTurns,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +365,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var repo = new ListRepository(ctx);
|
var repo = new ListRepository(ctx);
|
||||||
var config = await repo.GetConfigAsync(listId);
|
var config = await repo.GetConfigAsync(listId);
|
||||||
if (config is null) return null;
|
if (config is null) return null;
|
||||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath);
|
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTaskStatus(string taskId, string status)
|
public async Task SetTaskStatus(string taskId, string status)
|
||||||
@@ -405,7 +412,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
dto.TaskId,
|
dto.TaskId,
|
||||||
dto.Model.NullIfBlank(),
|
dto.Model.NullIfBlank(),
|
||||||
dto.SystemPrompt.NullIfBlank(),
|
dto.SystemPrompt.NullIfBlank(),
|
||||||
dto.AgentPath.NullIfBlank());
|
dto.AgentPath.NullIfBlank(),
|
||||||
|
dto.MaxTurns);
|
||||||
|
|
||||||
await _broadcaster.TaskUpdated(dto.TaskId);
|
await _broadcaster.TaskUpdated(dto.TaskId);
|
||||||
}
|
}
|
||||||
@@ -533,6 +541,15 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_primeSignal.Signal();
|
_primeSignal.Signal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RunDailyPrepNow()
|
||||||
|
{
|
||||||
|
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
|
||||||
|
var firedAt = DateTimeOffset.Now;
|
||||||
|
var outcome = await _primeRunner.FireAsync(schedule, Context.ConnectionAborted);
|
||||||
|
await _broadcaster.PrimeFired(Guid.Empty, outcome.Success, outcome.Message, firedAt);
|
||||||
|
return outcome.Success;
|
||||||
|
}
|
||||||
|
|
||||||
private static DateOnly Day(string iso) => DateOnly.ParseExact(iso, "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
private static DateOnly Day(string iso) => DateOnly.ParseExact(iso, "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
public Task<string?> GetWeekReport(string startIso, string endIso) =>
|
public Task<string?> GetWeekReport(string startIso, string endIso) =>
|
||||||
@@ -566,4 +583,32 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
using var ctx = _dbFactory.CreateDbContext();
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
await new DailyNoteRepository(ctx).DeleteAsync(id);
|
await new DailyNoteRepository(ctx).DeleteAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<string> GetLastPrepLog()
|
||||||
|
{
|
||||||
|
var path = DailyPrepPrompt.LogPath();
|
||||||
|
if (!File.Exists(path)) return Task.FromResult(string.Empty);
|
||||||
|
|
||||||
|
const int maxBytes = 256 * 1024;
|
||||||
|
var bytes = File.ReadAllBytes(path);
|
||||||
|
var text = bytes.Length <= maxBytes
|
||||||
|
? System.Text.Encoding.UTF8.GetString(bytes)
|
||||||
|
: System.Text.Encoding.UTF8.GetString(bytes, bytes.Length - maxBytes, maxBytes);
|
||||||
|
return Task.FromResult(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ClearMyDay()
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var ids = await ctx.Tasks.Where(t => t.IsMyDay).Select(t => t.Id).ToListAsync();
|
||||||
|
if (ids.Count == 0) return 0;
|
||||||
|
|
||||||
|
await ctx.Tasks.Where(t => t.IsMyDay)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.IsMyDay, false));
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
await _broadcaster.TaskUpdated(id);
|
||||||
|
|
||||||
|
return ids.Count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,17 +43,24 @@ public sealed class PlanningAggregator
|
|||||||
string planningTaskId, CancellationToken ct)
|
string planningTaskId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var ctx = _dbFactory.CreateDbContext();
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
var children = await ctx.Tasks
|
var parent = await ctx.Tasks
|
||||||
.Include(t => t.Worktree)
|
.Include(t => t.Worktree)
|
||||||
.Where(t => t.ParentTaskId == planningTaskId)
|
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
||||||
.OrderBy(t => t.SortOrder)
|
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct);
|
||||||
.ToListAsync(ct);
|
if (parent is null) return new List<SubtaskDiff>();
|
||||||
|
|
||||||
|
var nodes = new List<TaskEntity>();
|
||||||
|
// An improvement parent carries its own code branch — fold it in first so the
|
||||||
|
// combined diff matches what the tree-merge will produce.
|
||||||
|
if (parent.PlanningPhase == PlanningPhase.None && parent.Worktree is { State: WorktreeState.Active })
|
||||||
|
nodes.Add(parent);
|
||||||
|
nodes.AddRange(parent.Children.OrderBy(c => c.SortOrder));
|
||||||
|
|
||||||
var result = new List<SubtaskDiff>();
|
var result = new List<SubtaskDiff>();
|
||||||
foreach (var child in children)
|
foreach (var node in nodes)
|
||||||
{
|
{
|
||||||
if (child.Worktree is null) continue;
|
if (node.Worktree is null) continue;
|
||||||
var wt = child.Worktree;
|
var wt = node.Worktree;
|
||||||
var head = wt.HeadCommit ?? await _git.RevParseHeadAsync(wt.Path, ct);
|
var head = wt.HeadCommit ?? await _git.RevParseHeadAsync(wt.Path, ct);
|
||||||
string unified;
|
string unified;
|
||||||
try
|
try
|
||||||
@@ -62,11 +69,11 @@ public sealed class PlanningAggregator
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "diff failed for subtask {Id}", child.Id);
|
_logger.LogWarning(ex, "diff failed for node {Id}", node.Id);
|
||||||
unified = "";
|
unified = "";
|
||||||
}
|
}
|
||||||
result.Add(new SubtaskDiff(
|
result.Add(new SubtaskDiff(
|
||||||
child.Id, child.Title, wt.BranchName, wt.BaseCommit, head, wt.DiffStat, unified));
|
node.Id, node.Title, wt.BranchName, wt.BaseCommit, head, wt.DiffStat, unified));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -87,12 +94,18 @@ public sealed class PlanningAggregator
|
|||||||
|
|
||||||
await GitRawAsync(repoDir, ct, "checkout", "-b", integrationBranch);
|
await GitRawAsync(repoDir, ct, "checkout", "-b", integrationBranch);
|
||||||
|
|
||||||
foreach (var child in childSubtasks)
|
var nodes = new List<TaskEntity>();
|
||||||
|
// Fold the improvement parent's own branch in first (planning parents have none).
|
||||||
|
if (planning.PlanningPhase == PlanningPhase.None && planning.Worktree is { State: WorktreeState.Active })
|
||||||
|
nodes.Add(planning);
|
||||||
|
nodes.AddRange(childSubtasks);
|
||||||
|
|
||||||
|
foreach (var node in nodes)
|
||||||
{
|
{
|
||||||
if (child.Worktree is null) continue;
|
if (node.Worktree is null) continue;
|
||||||
var (code, _) = await _git.MergeNoFfAsync(
|
var (code, _) = await _git.MergeNoFfAsync(
|
||||||
repoDir, child.Worktree.BranchName,
|
repoDir, node.Worktree.BranchName,
|
||||||
$"Integrate subtask: {child.Title}", ct);
|
$"Integrate: {node.Title}", ct);
|
||||||
if (code != 0)
|
if (code != 0)
|
||||||
{
|
{
|
||||||
List<string> files;
|
List<string> files;
|
||||||
@@ -104,7 +117,7 @@ public sealed class PlanningAggregator
|
|||||||
try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); } catch { }
|
try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); } catch { }
|
||||||
|
|
||||||
return new CombinedDiffResult.Failed(
|
return new CombinedDiffResult.Failed(
|
||||||
new CombinedDiffFailure(child.Id, files));
|
new CombinedDiffFailure(node.Id, files));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +148,7 @@ public sealed class PlanningAggregator
|
|||||||
using var ctx = _dbFactory.CreateDbContext();
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
var planning = await ctx.Tasks
|
var planning = await ctx.Tasks
|
||||||
.Include(t => t.List)
|
.Include(t => t.List)
|
||||||
|
.Include(t => t.Worktree)
|
||||||
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
||||||
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct)
|
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct)
|
||||||
?? throw new KeyNotFoundException($"Planning task '{planningTaskId}' not found.");
|
?? throw new KeyNotFoundException($"Planning task '{planningTaskId}' not found.");
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public sealed class PlanningMcpService
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ctx = _contextAccessor.Current;
|
var ctx = _contextAccessor.Current;
|
||||||
var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, commitType, cancellationToken);
|
var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, commitType, createdBy: null, cancellationToken);
|
||||||
await BroadcastTaskUpdatedAsync(child.Id, cancellationToken);
|
await BroadcastTaskUpdatedAsync(child.Id, cancellationToken);
|
||||||
await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken);
|
await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken);
|
||||||
return new CreatedChildDto(child.Id, child.Status.ToString());
|
return new CreatedChildDto(child.Id, child.Status.ToString());
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public sealed class PlanningMergeOrchestrator
|
|||||||
{
|
{
|
||||||
public required string TargetBranch { get; init; }
|
public required string TargetBranch { get; init; }
|
||||||
public required Queue<string> RemainingSubtaskIds { get; init; }
|
public required Queue<string> RemainingSubtaskIds { get; init; }
|
||||||
|
public required bool IsPlanning { get; init; }
|
||||||
public string? CurrentSubtaskId { get; set; }
|
public string? CurrentSubtaskId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,32 +44,40 @@ public sealed class PlanningMergeOrchestrator
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(string planningTaskId, string targetBranch, CancellationToken ct)
|
public async Task StartAsync(string parentTaskId, string targetBranch, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string workingDir;
|
string workingDir;
|
||||||
List<TaskEntity> children;
|
List<TaskEntity> children;
|
||||||
|
bool isPlanning;
|
||||||
|
bool parentHasWorktree;
|
||||||
|
|
||||||
using (var ctx = _dbFactory.CreateDbContext())
|
using (var ctx = _dbFactory.CreateDbContext())
|
||||||
{
|
{
|
||||||
var planning = await ctx.Tasks
|
var parent = await ctx.Tasks
|
||||||
.Include(t => t.List)
|
.Include(t => t.List)
|
||||||
|
.Include(t => t.Worktree)
|
||||||
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
.Include(t => t.Children).ThenInclude(c => c.Worktree)
|
||||||
.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct)
|
.SingleOrDefaultAsync(t => t.Id == parentTaskId, ct)
|
||||||
?? throw new KeyNotFoundException($"Planning task '{planningTaskId}' not found.");
|
?? throw new KeyNotFoundException($"Planning task '{parentTaskId}' not found.");
|
||||||
workingDir = planning.List.WorkingDir
|
workingDir = parent.List.WorkingDir
|
||||||
?? throw new InvalidOperationException("List has no working directory.");
|
?? throw new InvalidOperationException("List has no working directory.");
|
||||||
children = planning.Children.OrderBy(c => c.SortOrder).ToList();
|
children = parent.Children.OrderBy(c => c.SortOrder).ToList();
|
||||||
|
isPlanning = parent.PlanningPhase != PlanningPhase.None;
|
||||||
|
parentHasWorktree = parent.Worktree is { State: WorktreeState.Active };
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var c in children)
|
if (isPlanning)
|
||||||
{
|
{
|
||||||
if (c.Status != TaskStatus.Done)
|
foreach (var c in children)
|
||||||
throw new InvalidOperationException($"subtask {c.Id} is not Done (status {c.Status})");
|
{
|
||||||
if (c.Worktree is null)
|
if (c.Status != TaskStatus.Done)
|
||||||
throw new InvalidOperationException($"subtask {c.Id} has no worktree");
|
throw new InvalidOperationException($"subtask {c.Id} is not Done (status {c.Status})");
|
||||||
if (c.Worktree.State != WorktreeState.Active && c.Worktree.State != WorktreeState.Merged)
|
if (c.Worktree is null)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException($"subtask {c.Id} has no worktree");
|
||||||
$"subtask {c.Id} worktree state is {c.Worktree.State}");
|
if (c.Worktree.State != WorktreeState.Active && c.Worktree.State != WorktreeState.Merged)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"subtask {c.Id} worktree state is {c.Worktree.State}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await _git.IsMidMergeAsync(workingDir, ct))
|
if (await _git.IsMidMergeAsync(workingDir, ct))
|
||||||
@@ -76,17 +85,22 @@ public sealed class PlanningMergeOrchestrator
|
|||||||
if (await _git.HasChangesAsync(workingDir, ct))
|
if (await _git.HasChangesAsync(workingDir, ct))
|
||||||
throw new InvalidOperationException("working tree has uncommitted changes");
|
throw new InvalidOperationException("working tree has uncommitted changes");
|
||||||
|
|
||||||
var queue = new Queue<string>(
|
var idsToMerge = new List<string>();
|
||||||
|
if (!isPlanning && parentHasWorktree)
|
||||||
|
idsToMerge.Add(parentTaskId);
|
||||||
|
idsToMerge.AddRange(
|
||||||
children
|
children
|
||||||
.Where(c => c.Worktree!.State == WorktreeState.Active)
|
.Where(c => c.Status == TaskStatus.Done && c.Worktree is { State: WorktreeState.Active })
|
||||||
.Select(c => c.Id));
|
.Select(c => c.Id));
|
||||||
|
|
||||||
var state = new State { TargetBranch = targetBranch, RemainingSubtaskIds = queue };
|
var queue = new Queue<string>(idsToMerge);
|
||||||
if (!_states.TryAdd(planningTaskId, state))
|
|
||||||
throw new InvalidOperationException($"Merge already in progress for {planningTaskId}.");
|
|
||||||
|
|
||||||
await _broadcaster.PlanningMergeStarted(planningTaskId, targetBranch);
|
var state = new State { TargetBranch = targetBranch, RemainingSubtaskIds = queue, IsPlanning = isPlanning };
|
||||||
await DrainAsync(planningTaskId, ct);
|
if (!_states.TryAdd(parentTaskId, state))
|
||||||
|
throw new InvalidOperationException($"Merge already in progress for {parentTaskId}.");
|
||||||
|
|
||||||
|
await _broadcaster.PlanningMergeStarted(parentTaskId, targetBranch);
|
||||||
|
await DrainAsync(parentTaskId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ContinueAsync(string planningTaskId, CancellationToken ct)
|
public async Task ContinueAsync(string planningTaskId, CancellationToken ct)
|
||||||
@@ -167,7 +181,7 @@ public sealed class PlanningMergeOrchestrator
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.CurrentSubtaskId = null;
|
state.CurrentSubtaskId = null;
|
||||||
await FinalizePlanningDoneAsync(planningTaskId, ct);
|
await FinalizeParentDoneAsync(planningTaskId, state.IsPlanning, ct);
|
||||||
await _broadcaster.PlanningCompleted(planningTaskId);
|
await _broadcaster.PlanningCompleted(planningTaskId);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -176,16 +190,20 @@ public sealed class PlanningMergeOrchestrator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FinalizePlanningDoneAsync(string planningTaskId, CancellationToken ct)
|
private async Task FinalizeParentDoneAsync(string parentTaskId, bool isPlanning, CancellationToken ct)
|
||||||
{
|
{
|
||||||
using var ctx = _dbFactory.CreateDbContext();
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
var planning = await ctx.Tasks.SingleOrDefaultAsync(t => t.Id == planningTaskId, ct);
|
var parent = await ctx.Tasks.SingleOrDefaultAsync(t => t.Id == parentTaskId, ct);
|
||||||
if (planning is null) return;
|
if (parent is null) return;
|
||||||
planning.Status = TaskStatus.Done;
|
parent.Status = TaskStatus.Done;
|
||||||
planning.FinishedAt = DateTime.UtcNow;
|
parent.FinishedAt = DateTime.UtcNow;
|
||||||
await ctx.SaveChangesAsync(ct);
|
await ctx.SaveChangesAsync(ct);
|
||||||
|
|
||||||
try { await _aggregator.CleanupIntegrationBranchAsync(planningTaskId, ct); }
|
// Only planning builds an integration branch via the aggregator; skip cleanup otherwise.
|
||||||
catch (Exception ex) { _logger.LogWarning(ex, "integration branch cleanup failed"); }
|
if (isPlanning)
|
||||||
|
{
|
||||||
|
try { await _aggregator.CleanupIntegrationBranchAsync(parentTaskId, ct); }
|
||||||
|
catch (Exception ex) { _logger.LogWarning(ex, "integration branch cleanup failed"); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,47 +363,14 @@ public sealed class PlanningSessionManager
|
|||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private static string BuildSystemPrompt()
|
private static string BuildSystemPrompt() => PromptFiles.ReadOrDefault(PromptKind.Planning);
|
||||||
{
|
|
||||||
var fromFile = PromptFiles.ReadOrNull(PromptKind.Planning);
|
|
||||||
if (fromFile is not null) return fromFile;
|
|
||||||
|
|
||||||
return
|
private static string BuildInitialPrompt(TaskEntity task) =>
|
||||||
"""
|
PromptFiles.Render(PromptKind.PlanningInitial, new Dictionary<string, string>
|
||||||
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.
|
|
||||||
""";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildInitialPrompt(TaskEntity task)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"# Task: {task.Title}");
|
|
||||||
if (!string.IsNullOrWhiteSpace(task.Description))
|
|
||||||
{
|
{
|
||||||
sb.AppendLine();
|
["title"] = task.Title,
|
||||||
sb.AppendLine(task.Description);
|
["description"] = task.Description ?? "",
|
||||||
}
|
});
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("---");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("Please analyze this task and break it down into concrete subtasks.");
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BranchNameFor(string taskId) =>
|
private static string BranchNameFor(string taskId) =>
|
||||||
$"claudedo/planning/{taskId.Replace("-", "")}";
|
$"claudedo/planning/{taskId.Replace("-", "")}";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Runner;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Planning;
|
namespace ClaudeDo.Worker.Planning;
|
||||||
@@ -9,7 +10,7 @@ public sealed class PlanningTokenAuthMiddleware
|
|||||||
|
|
||||||
public PlanningTokenAuthMiddleware(RequestDelegate next) => _next = next;
|
public PlanningTokenAuthMiddleware(RequestDelegate next) => _next = next;
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext ctx, TaskRepository tasks)
|
public async Task InvokeAsync(HttpContext ctx, TaskRepository tasks, TaskRunTokenRegistry runTokens)
|
||||||
{
|
{
|
||||||
if (!ctx.Request.Path.StartsWithSegments("/mcp"))
|
if (!ctx.Request.Path.StartsWithSegments("/mcp"))
|
||||||
{
|
{
|
||||||
@@ -26,15 +27,23 @@ public sealed class PlanningTokenAuthMiddleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
var token = auth.Substring("Bearer ".Length).Trim();
|
var token = auth.Substring("Bearer ".Length).Trim();
|
||||||
|
|
||||||
var parent = await tasks.FindByPlanningTokenAsync(token, ctx.RequestAborted);
|
var parent = await tasks.FindByPlanningTokenAsync(token, ctx.RequestAborted);
|
||||||
if (parent is null || parent.PlanningPhase != ClaudeDo.Data.Models.PlanningPhase.Active)
|
if (parent is not null && parent.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.Active)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 401;
|
ctx.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };
|
||||||
await ctx.Response.WriteAsync("Invalid or expired planning token");
|
await _next(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };
|
if (runTokens.TryResolve(token, out var callerTaskId))
|
||||||
await _next(ctx);
|
{
|
||||||
|
ctx.Items["TaskRunContext"] = new TaskRunMcpContext { CallerTaskId = callerTaskId };
|
||||||
|
await _next(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.StatusCode = 401;
|
||||||
|
await ctx.Response.WriteAsync("Invalid or expired token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs
Normal file
24
src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace ClaudeDo.Worker.Prime;
|
||||||
|
|
||||||
|
public static class DailyPrepPrompt
|
||||||
|
{
|
||||||
|
public const string CandidatesTool = "mcp__claudedo__get_daily_prep_candidates";
|
||||||
|
public const string SetMyDayTool = "mcp__claudedo__set_my_day";
|
||||||
|
|
||||||
|
public static string LogPath() =>
|
||||||
|
System.IO.Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "logs", "daily-prep.log");
|
||||||
|
|
||||||
|
public static string BuildArgs(int maxTurns) =>
|
||||||
|
"-p --output-format stream-json --verbose --permission-mode acceptEdits " +
|
||||||
|
$"--max-turns {maxTurns} " +
|
||||||
|
$"--allowedTools {CandidatesTool} {SetMyDayTool}";
|
||||||
|
|
||||||
|
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
||||||
|
ClaudeDo.Data.PromptFiles.Render(
|
||||||
|
ClaudeDo.Data.PromptKind.DailyPrep,
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["date"] = today.ToString("yyyy-MM-dd"),
|
||||||
|
["maxTasks"] = maxTasks.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,4 +3,7 @@ namespace ClaudeDo.Worker.Prime;
|
|||||||
public interface IPrimeBroadcaster
|
public interface IPrimeBroadcaster
|
||||||
{
|
{
|
||||||
Task PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
|
Task PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
|
||||||
|
Task PrepStartedAsync();
|
||||||
|
Task PrepLineAsync(string line);
|
||||||
|
Task PrepFinishedAsync(bool success);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,96 @@
|
|||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Worker.Runner;
|
using ClaudeDo.Worker.Runner;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Prime;
|
namespace ClaudeDo.Worker.Prime;
|
||||||
|
|
||||||
public sealed class PrimeRunner : IPrimeRunner
|
public sealed class PrimeRunner : IPrimeRunner
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan FireTimeout = TimeSpan.FromSeconds(60);
|
private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
|
||||||
private readonly IClaudeProcess _claude;
|
private const int MaxTurns = 30;
|
||||||
private readonly ILogger<PrimeRunner> _logger;
|
|
||||||
|
|
||||||
public PrimeRunner(IClaudeProcess claude, ILogger<PrimeRunner> logger)
|
private readonly IClaudeProcess _claude;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly IPrimeClock _clock;
|
||||||
|
private readonly ILogger<PrimeRunner> _logger;
|
||||||
|
private readonly IPrimeBroadcaster _broadcaster;
|
||||||
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
|
|
||||||
|
public PrimeRunner(
|
||||||
|
IClaudeProcess claude,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
IPrimeClock clock,
|
||||||
|
ILogger<PrimeRunner> logger,
|
||||||
|
IPrimeBroadcaster broadcaster)
|
||||||
{
|
{
|
||||||
_claude = claude;
|
_claude = claude;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_clock = clock;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_broadcaster = broadcaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var cwd = Paths.AppDataRoot();
|
if (!await _gate.WaitAsync(0, ct))
|
||||||
Directory.CreateDirectory(cwd);
|
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||||
|
|
||||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
||||||
timeoutCts.CancelAfter(FireTimeout);
|
|
||||||
|
|
||||||
|
var success = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var prompt = schedule.PromptOverride ?? "ping";
|
var logPath = DailyPrepPrompt.LogPath();
|
||||||
|
try { if (File.Exists(logPath)) File.Delete(logPath); } catch { /* best effort */ }
|
||||||
|
await using var logWriter = new LogWriter(logPath);
|
||||||
|
|
||||||
|
await _broadcaster.PrepStartedAsync();
|
||||||
|
|
||||||
|
var cwd = Paths.AppDataRoot();
|
||||||
|
Directory.CreateDirectory(cwd);
|
||||||
|
|
||||||
|
int maxTasks;
|
||||||
|
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||||
|
{
|
||||||
|
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
|
||||||
|
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
||||||
|
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||||
|
|
||||||
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
timeoutCts.CancelAfter(FireTimeout);
|
||||||
|
|
||||||
var result = await _claude.RunAsync(
|
var result = await _claude.RunAsync(
|
||||||
arguments: "-p --max-turns 1",
|
arguments: args,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
workingDirectory: cwd,
|
workingDirectory: cwd,
|
||||||
onStdoutLine: _ => Task.CompletedTask,
|
onStdoutLine: async line =>
|
||||||
|
{
|
||||||
|
await logWriter.WriteLineAsync(line);
|
||||||
|
await _broadcaster.PrepLineAsync(line);
|
||||||
|
},
|
||||||
ct: timeoutCts.Token);
|
ct: timeoutCts.Token);
|
||||||
|
|
||||||
if (IsSuccess(result))
|
success = result.IsSuccess;
|
||||||
return new PrimeRunOutcome(true, "Primed Claude");
|
return success
|
||||||
return new PrimeRunOutcome(false, FailureMessage(result));
|
? new PrimeRunOutcome(true, "Daily prep complete")
|
||||||
|
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !ct.IsCancellationRequested)
|
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalSeconds:0}s");
|
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Prime fire failed");
|
_logger.LogWarning(ex, "Daily prep run failed");
|
||||||
return new PrimeRunOutcome(false, ex.Message);
|
return new PrimeRunOutcome(false, ex.Message);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _broadcaster.PrepFinishedAsync(success);
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSuccess(RunResult result) => result.IsSuccess;
|
|
||||||
private static string FailureMessage(RunResult result) => $"exit code {result.ExitCode}";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,8 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
var logRoot = cfg.LogRoot;
|
var logRoot = cfg.LogRoot;
|
||||||
Directory.CreateDirectory(logRoot);
|
Directory.CreateDirectory(logRoot);
|
||||||
builder.Host.UseSerilog((ctx, lc) => lc
|
builder.Host.UseSerilog((ctx, lc) =>
|
||||||
.MinimumLevel.Information()
|
ClaudeDo.Logging.LoggingSetup.Configure(lc, "worker", logRoot));
|
||||||
.WriteTo.File(
|
|
||||||
System.IO.Path.Combine(logRoot, "worker-.log"),
|
|
||||||
rollingInterval: RollingInterval.Day,
|
|
||||||
retainedFileCountLimit: 7,
|
|
||||||
shared: true));
|
|
||||||
|
|
||||||
builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
|
builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
|
||||||
opt.UseSqlite($"Data Source={cfg.DbPath}"));
|
opt.UseSqlite($"Data Source={cfg.DbPath}"));
|
||||||
@@ -56,6 +51,7 @@ builder.Services.AddSingleton<HubBroadcaster>();
|
|||||||
builder.Services.AddSingleton<GitService>();
|
builder.Services.AddSingleton<GitService>();
|
||||||
builder.Services.AddSingleton<WorktreeManager>();
|
builder.Services.AddSingleton<WorktreeManager>();
|
||||||
builder.Services.AddSingleton<ClaudeArgsBuilder>();
|
builder.Services.AddSingleton<ClaudeArgsBuilder>();
|
||||||
|
builder.Services.AddSingleton<TaskRunTokenRegistry>();
|
||||||
builder.Services.AddSingleton<TaskRunner>();
|
builder.Services.AddSingleton<TaskRunner>();
|
||||||
builder.Services.AddSingleton<WorktreeMaintenanceService>();
|
builder.Services.AddSingleton<WorktreeMaintenanceService>();
|
||||||
builder.Services.AddSingleton<TaskResetService>();
|
builder.Services.AddSingleton<TaskResetService>();
|
||||||
@@ -131,6 +127,8 @@ builder.Services.AddSingleton<IPlanningTerminalLauncher>(sp =>
|
|||||||
new WindowsTerminalPlanningLauncher("wt.exe", cfg.ClaudeBin));
|
new WindowsTerminalPlanningLauncher("wt.exe", cfg.ClaudeBin));
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<PlanningMcpContextAccessor>();
|
builder.Services.AddScoped<PlanningMcpContextAccessor>();
|
||||||
|
builder.Services.AddScoped<TaskRunMcpContextAccessor>();
|
||||||
|
builder.Services.AddScoped<TaskRunMcpService>();
|
||||||
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
||||||
builder.Services.AddScoped<TaskRepository>();
|
builder.Services.AddScoped<TaskRepository>();
|
||||||
@@ -138,7 +136,8 @@ builder.Services.AddScoped<ListRepository>();
|
|||||||
builder.Services.AddScoped<PlanningMcpService>();
|
builder.Services.AddScoped<PlanningMcpService>();
|
||||||
builder.Services.AddMcpServer()
|
builder.Services.AddMcpServer()
|
||||||
.WithHttpTransport()
|
.WithHttpTransport()
|
||||||
.WithTools<PlanningMcpService>();
|
.WithTools<PlanningMcpService>()
|
||||||
|
.WithTools<TaskRunMcpService>();
|
||||||
|
|
||||||
// Loopback-only bind. Firewall is irrelevant for 127.0.0.1.
|
// Loopback-only bind. Firewall is irrelevant for 127.0.0.1.
|
||||||
builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}");
|
builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}");
|
||||||
|
|||||||
@@ -5,33 +5,19 @@ namespace ClaudeDo.Worker.Report;
|
|||||||
|
|
||||||
public static class WeekReportPromptBuilder
|
public static class WeekReportPromptBuilder
|
||||||
{
|
{
|
||||||
private const string Instructions = """
|
|
||||||
You are generating a concise weekly standup report for a software developer.
|
|
||||||
Summarize what they accomplished between {0} and {1}.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Write the ENTIRE report in German.
|
|
||||||
- Group by day. One "## {{Wochentag}}, {{dd.MM.yyyy}}" section per day that has
|
|
||||||
activity (German weekday names). Omit days with no activity entirely.
|
|
||||||
- Within each day: 3-5 first-person, past-tense bullets ("- Habe X umgesetzt",
|
|
||||||
"- Y behoben"). Merge related small work into one bullet.
|
|
||||||
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
|
|
||||||
- Blend the developer's own notes and the derived activity into ONE deduplicated
|
|
||||||
bullet list per day. The developer's notes are authoritative - never omit or
|
|
||||||
contradict their substance.
|
|
||||||
- Name the project/repo when it adds clarity.
|
|
||||||
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
|
|
||||||
""";
|
|
||||||
|
|
||||||
public static string Build(
|
public static string Build(
|
||||||
DateOnly start, DateOnly end,
|
DateOnly start, DateOnly end,
|
||||||
IReadOnlyList<RepoActivity> activity,
|
IReadOnlyList<RepoActivity> activity,
|
||||||
IReadOnlyDictionary<DateOnly, List<string>> notesByDay)
|
IReadOnlyDictionary<DateOnly, List<string>> notesByDay)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Instructions,
|
sb.AppendLine(ClaudeDo.Data.PromptFiles.Render(
|
||||||
start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
ClaudeDo.Data.PromptKind.WeeklyReport,
|
||||||
end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)));
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["start"] = start.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||||
|
["end"] = end.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture),
|
||||||
|
}));
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
|
|
||||||
var days = new SortedDictionary<DateOnly, List<(string Repo, DayActivity Day)>>();
|
var days = new SortedDictionary<DateOnly, List<(string Repo, DayActivity Day)>>();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user