8.3 KiB
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:
/// <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:
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:
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
HandledTasksaufDetailsIslandViewModel, befüllt vonLoadHandledTasksAsyncmitWhere(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 wieChildOutcomes; der bestehendeRefreshChildOutcomeAsync-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 →
HandlerTaskIdder 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).