170 lines
8.3 KiB
Markdown
170 lines
8.3 KiB
Markdown
# Handler-Run: Verknüpfung zu den behandelten Tasks
|
|
|
|
**Date:** 2026-08-07
|
|
**Status:** Design approved (Mika), implementation pending
|
|
**Verified against:** commit `c792765`
|
|
|
|
## Problem
|
|
|
|
Ein "Let Claude handle it"-Run besitzt seit 2026-08-05 einen echten Task (`IsManual=true`,
|
|
`HandlerBaseCommit`/`HandlerHeadCommit`, Diff über Commit-Range). Was fehlt: **welche Tasks der Run
|
|
behandelt hat, ist nirgends persistiert.** Die Auswahl lebt nur in der ConPTY-Session und im
|
|
Transcript; `HandoffMcpTools.HandoffListHandler` (`src/ClaudeDo.Worker/External/HandoffMcpTools.cs:28-45`)
|
|
bekommt `survivingTaskIds` als flüchtige Liste.
|
|
|
|
Folge: Nachdem ein Run durch ist und der Diff sichtbar wird, lässt sich nicht mehr nachvollziehen,
|
|
*was alles gemacht werden sollte* und *welcher Task was produziert hat*. Duplikate, die der Handler
|
|
in Phase 1 gecancelt hat, verschwinden vollständig aus dem Blickfeld.
|
|
|
|
Zweitens zeigt der Handler-Task in der Liste das Badge **MANUAL**, weil er `IsManual=true` setzt —
|
|
irreführend, denn es ist kein manueller Reminder.
|
|
|
|
## Ist-Zustand
|
|
|
|
### Es gibt kein Task-Kind
|
|
|
|
`TaskEntity` hat **kein `Kind`/`Type`-Enum**. Task-"Arten" sind heute Feld-Kombinationen:
|
|
|
|
| Feld | Bedeutung |
|
|
|---|---|
|
|
| `IsManual` | manueller Reminder — Queue/Daily-Prep/Refine überspringen ihn |
|
|
| `ParentTaskId` | Kind einer Planning-/Improvement-Session |
|
|
| `PlanningPhase` | Planning-Parent |
|
|
| `BlockedByTaskId` | Kettenglied, Queue-Picker überspringt es |
|
|
| `HandlerBaseCommit` | worktree-loser List-Handler-Host (`src/ClaudeDo.Data/Models/TaskEntity.cs:60-61`) |
|
|
|
|
Ein Handler-Task ist also allein durch `HandlerBaseCommit != null` identifiziert.
|
|
|
|
### `ParentTaskId` ist belegt
|
|
|
|
`TaskRepository.CreateChildAsync` (`src/ClaudeDo.Data/Repositories/TaskRepository.cs:306`) setzt es
|
|
für Planning-Kinder; `TaskRowViewModel.IsChild`/`ShowAsChild`
|
|
(`src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs:59,66`) hängen daran und rücken die Zeile
|
|
im Baum ein. Ein Recycling für Handler→behandelte Tasks würde die Auswahl optisch unter den Handler
|
|
schieben und mit echten Planning-Kindern kollidieren.
|
|
|
|
### Badge-Infrastruktur existiert
|
|
|
|
`TaskRowView.axaml:129-144` rendert DRAFT / PLANNED / PLANNING / MANUAL über
|
|
`Border Classes="badge <variant>"`. Basis-Style und Varianten liegen in
|
|
`src/ClaudeDo.Ui/Design/IslandStyles.axaml:963-990`, die Brushes als theme-fähige Tokens in
|
|
`Tokens.axaml`. Loc-Keys: `tasks.badgeManual`, `tasks.manualTip` (en.json:163-164).
|
|
|
|
### Kinder-Panel existiert
|
|
|
|
`DetailsIslandViewModel.LoadChildOutcomesAsync`
|
|
(`src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:704-748`) lädt
|
|
`Where(t => t.ParentTaskId == parentTaskId)` in `ChildOutcomes` (`:248`) und rendert pro Zeile
|
|
Id/Titel/Status/RoadblockCount/WorktreeState via `ChildOutcomeRowViewModel`; Refresh läuft über
|
|
`TaskUpdated`/`WorktreeUpdated` (`:814-833`).
|
|
|
|
## Entscheidungen
|
|
|
|
| Frage | Entscheidung | Begründung |
|
|
|---|---|---|
|
|
| Neues `TaskKind`-Enum? | **Nein** | Es gäbe kein Enum zu erweitern — es wäre das erste überhaupt, inkl. Migration und Rückwirkung auf Queue/Filter/UI. Der Bedarf ist eine Beziehung, kein Typ. |
|
|
| `ParentTaskId` wiederverwenden? | **Nein** | belegt durch Planning-Kinder, kollidiert mit Einrückungs-Logik |
|
|
| 1:n oder n:m? | **1:n**, eine nullable Spalte | Historie "welcher Run hat den Task mal berührt" bringt nichts, wenn ohnehin der letzte Run derjenige ist, dessen Diff man ansieht. Join-Tabelle = doppelter Code für einen Randfall. |
|
|
| Wann stempeln? | **Beim Anlegen des Handler-Tasks** | Die UI kennt die Auswahl bereits. Erfasst auch die Tasks, die der Handler in Phase 1 als Duplikat cancelt — genau das "was sollte alles gemacht werden". Ein Stempeln erst in `handoff_list_handler` würde Dedupe-Verlierer verlieren und bei Abbruch vor Phase 2 gar nichts verknüpfen. |
|
|
| Umfang der Anzeige | **Nur Liste + Endstatus** | Kein Phasen-Protokoll, kein Per-Task-Diff im Panel — der Diff hängt ohnehin am jeweiligen Task. |
|
|
|
|
## Design
|
|
|
|
### 1. Daten
|
|
|
|
Neue nullable Spalte auf `TaskEntity`:
|
|
|
|
```csharp
|
|
/// <summary>Id des Handler-Task-Runs, der diesen Task behandelt hat (null = keiner).</summary>
|
|
public string? HandlerTaskId { get; set; }
|
|
```
|
|
|
|
Konfiguration in `TaskEntityConfiguration`: `HasIndex(t => t.HandlerTaskId)`, kein FK-Constraint
|
|
(konsistent mit `BlockedByTaskId`-Handhabung; ein gelöschter Handler-Task soll die behandelten Tasks
|
|
nicht kaskadierend anfassen). EF-Core-Migration `AddHandlerTaskId`.
|
|
|
|
Ein zweiter Run über dieselben Tasks überschreibt die Zuordnung — gewollt (1:n).
|
|
|
|
### 2. Schreiben
|
|
|
|
Die Auswahl wird durchgereicht: UI → `IWorkerClient.CreateMergeHelperTaskAsync` →
|
|
`WorkerHub.CreateMergeHelperTask` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs:827-838`) →
|
|
`InteractiveLaunchSpecService.CreateMergeHelperTaskAsync`. Nach dem Anlegen des Handler-Tasks setzt
|
|
eine neue Repository-Methode die Zuordnung in einem Batch-Update:
|
|
|
|
```csharp
|
|
Task<int> SetHandlerTaskIdAsync(IReadOnlyList<string> taskIds, string handlerTaskId, CancellationToken ct);
|
|
```
|
|
|
|
Der Handler-Task selbst bekommt **kein** `HandlerTaskId` (kein Selbstbezug). Unbekannte Ids werden
|
|
still übersprungen.
|
|
|
|
### 3. Badge
|
|
|
|
`TaskRowViewModel`:
|
|
|
|
```csharp
|
|
public bool IsHandlerRun => !string.IsNullOrEmpty(HandlerBaseCommit);
|
|
public string? HandlerBadge => IsHandlerRun ? Loc.T("tasks.badgeHandler") : null;
|
|
public string? ManualBadge => IsManual && !IsHandlerRun ? Loc.T("tasks.badgeManual") : null;
|
|
```
|
|
|
|
`HandlerBaseCommit` muss dafür auf das Row-ViewModel und in dessen Mapping aufgenommen werden.
|
|
HANDLER hat Vorrang vor MANUAL — beide Badges nie gleichzeitig.
|
|
|
|
In `TaskRowView.axaml` analog zu `:141-144` ein `Border Classes="badge handler"` mit
|
|
`ToolTip.Tip="{loc:Tr tasks.handlerTip}"`. In `IslandStyles.axaml` eine `.badge.handler`-Variante
|
|
mit `{DynamicResource HandlerBadgeBrush}`, Token in `Tokens.axaml` für Light und Dark.
|
|
|
|
Neue Loc-Keys in en.json **und** de.json (Parität ist testgeprüft):
|
|
|
|
- `tasks.badgeHandler` — "HANDLER" / "HANDLER"
|
|
- `tasks.handlerTip` — "Handler run — lists the tasks it processed" / "Handler-Run — listet die
|
|
Tasks, die er bearbeitet hat"
|
|
|
|
### 4. Anzeige
|
|
|
|
Im Detail-Bereich eines Handler-Tasks eine Liste der behandelten Tasks, parallel zum bestehenden
|
|
Kinder-Panel:
|
|
|
|
- Neue Collection `HandledTasks` auf `DetailsIslandViewModel`, befüllt von `LoadHandledTasksAsync`
|
|
mit `Where(t => t.HandlerTaskId == taskId)`, sortiert wie die Kinder-Liste.
|
|
- Zeilen wiederverwenden `ChildOutcomeRowViewModel` (Id, Titel, Status, RoadblockCount,
|
|
WorktreeState) — keine neue Row-Klasse.
|
|
- Refresh über dieselben `TaskUpdated`-Events wie `ChildOutcomes`; der bestehende
|
|
`RefreshChildOutcomeAsync`-Pfad (`:814-833`) wird um die zweite Collection erweitert.
|
|
- Sichtbar nur wenn `HandledTasks.Count > 0`.
|
|
- **Keine Klick-Interaktion** — das bestehende `ChildOutcomes`-Template ist eine reine Anzeige
|
|
(Titel / Roadblock / Status, kein Tapped-Handler). Die neue Liste bleibt identisch; "zum Task
|
|
springen" wäre neues Verhalten und ist hier nicht enthalten.
|
|
|
|
### 5. Fehlerfälle
|
|
|
|
- Handler-Task gelöscht → `HandlerTaskId` der behandelten Tasks zeigt ins Leere; die Tasks bleiben
|
|
normal nutzbar, das Panel existiert schlicht nicht mehr. Kein Cleanup nötig.
|
|
- Behandelter Task gelöscht → verschwindet aus der Liste (Query läuft live gegen die Tasks).
|
|
- Leere Auswahl → kein Stempeln, Panel bleibt unsichtbar.
|
|
|
|
## Tests
|
|
|
|
| Ebene | Test |
|
|
|---|---|
|
|
| Data | `SetHandlerTaskIdAsync` stempelt alle übergebenen Ids, ignoriert unbekannte, überschreibt eine vorhandene Zuordnung |
|
|
| Worker | `CreateMergeHelperTaskAsync` stempelt die übergebene Auswahl und **nicht** den Handler-Task selbst |
|
|
| Ui | `TaskRowViewModel`: HANDLER schlägt MANUAL (`IsManual=true` + `HandlerBaseCommit` gesetzt → nur HANDLER) |
|
|
| Ui | `DetailsIslandViewModel`: `HandledTasks` lädt nach `HandlerTaskId`, aktualisiert sich auf `TaskUpdated` |
|
|
| Localization | Parität en/de — deckt der bestehende Test automatisch ab |
|
|
|
|
## Bewusst nicht enthalten
|
|
|
|
- Kein `TaskKind`-Enum.
|
|
- Keine n:m-Historie über mehrere Runs.
|
|
- Kein Phasen-Protokoll (Dedupe-Begründungen, Umformulierungen) — nur das Ergebnis.
|
|
- Kein Per-Task-Diff im Panel; der Diff bleibt am jeweiligen Task.
|
|
- Kein Badge auf den *behandelten* Tasks.
|
|
|
|
## Offen
|
|
|
|
- **Sichtprüfung durch Mika:** Badge-Farbe im Light- und Dark-Theme, Position des Panels im
|
|
Detail-Bereich, Verhalten bei vielen behandelten Tasks (Scroll).
|