docs(specs): Design für Feedback bei langlaufenden Operationen
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# Feedback für langlaufende Operationen
|
||||
|
||||
**Datum:** 2026-08-11
|
||||
**Status:** Design freigegeben, Implementierung offen
|
||||
**Auslöser:** Ein Merge über das Detail-Pane blockierte minutenlang ohne jede Anzeige.
|
||||
|
||||
## Problem
|
||||
|
||||
Langlaufende Operationen geben kein Feedback. Der Nutzer klickt, nichts passiert sichtbar,
|
||||
die App liest sich als abgestürzt. Der gemeldete Fall war ein Merge mit Verify-Gate, aber das
|
||||
ist ein Symptom eines Musters: Feedback wird **pro Fall ad-hoc** gebaut
|
||||
(`PrepStarted/Line/Finished`, `RefineStarted/Finished`, `PlanningMerge*`, `MergeProgress`),
|
||||
und dabei geht regelmäßig eine Hälfte verloren oder eine Fläche wird vergessen.
|
||||
|
||||
Belegend: `HubBroadcaster.MergeProgress` existierte bereits, während `WorkerClient` noch
|
||||
keinen Listener hatte — Sender ohne Empfänger. Das wurde parallel zu diesem Design in einer
|
||||
anderen Session nachgezogen (siehe *Abhängigkeit zur Parallelarbeit*), deckt aber nur das
|
||||
MergeModal ab, nicht den Approve-Pfad im Detail-Pane.
|
||||
|
||||
## Vier Fehlerbilder
|
||||
|
||||
Die Analyse trennt vier Klassen, die **verschiedene** Lösungen brauchen. Sie zusammen als
|
||||
„fehlendes Feedback" zu behandeln war der Fehler der bisherigen Einzelfall-Fixes.
|
||||
|
||||
### Bild 1 — Stiller Await im UI
|
||||
|
||||
Ein `[RelayCommand]` awaitet einen Call, ohne Busy-State, ohne Anzeige. Der Button bleibt
|
||||
klickbar (Doppelklick-Risiko), es gibt keinen Hinweis, dass etwas läuft.
|
||||
|
||||
| Stelle | Befund |
|
||||
|---|---|
|
||||
| `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:1206` `ApproveReviewAsync` | **Der gemeldete Fall.** Kein Busy-Flag, kein Indikator; das Verify-Gate kann den Call Minuten halten |
|
||||
| `DetailsIslandViewModel.cs:1244` `SubmitForReviewAsync` | kein Busy-Flag (`MissionControlViewModel` hat für denselben Call ein `IsSubmitPending`, das Detail-Pane nicht) |
|
||||
| `DetailsIslandViewModel.cs` `RejectReviewAsync`, `ParkReviewAsync` | kein Busy-Flag; Fehler werden verschluckt (`catch { return; }`) |
|
||||
| `src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs:121` `PreviewMergeAsync` | kein Indikator |
|
||||
| `WorktreesOverviewModalViewModel` (`IsBusy` vorhanden) | `IsBusy` schaltet in `WorktreesOverviewModalView.axaml:107-108` **nur** Buttons aus — kein Spinner, kein Text. Betrifft Cleanup, Reset, ForceRemove, Refresh, Batch-Merge |
|
||||
| Settings-Tabs: `SessionSkillsSettingsTabViewModel`, `FilesSettingsTabViewModel`, `OnlineInboxSettingsViewModel` | dito — `IsEnabled="{Binding !IsBusy}"`, keine Anzeige. Skill-Install ist ein `git clone` |
|
||||
| `WeeklyReportModalViewModel`, `RepoImportModalViewModel` | `IsBusy` nur als CanExecute-Gate |
|
||||
| Islands ↔ SQLite direkt | Die Islands sprechen direkt mit der DB (`new TaskRepository(ctx)` u.ä., ~15 Stellen allein in `DetailsIslandViewModel`). Meist schnell; `ClearCompleted`, Drag-Reorder über viele Zeilen und das Laden großer Listen nicht zwingend |
|
||||
|
||||
Bereits sauber gelöst und **nicht** anzufassen: `MergeModalViewModel` (Spinner + Phase +
|
||||
Elapsed), `ConflictResolverViewModel` (`IsBusy` + Text), `DiffViewerViewModel.IsLoadingCombined`,
|
||||
`UsageMonitorModalViewModel` (Spinner).
|
||||
|
||||
### Bild 2 — UI-Thread-Freeze
|
||||
|
||||
Eine andere Fehlerklasse: hier hilft kein Indikator, weil der Dispatcher blockiert ist und
|
||||
der Spinner nicht gezeichnet werden könnte. Erst auslagern, dann anzeigen.
|
||||
|
||||
| Stelle | Befund |
|
||||
|---|---|
|
||||
| `src/ClaudeDo.Ui/ViewModels/Modals/DiffViewerViewModel.cs:182` und `:260` | `UnifiedDiffParser.Parse(raw)` läuft **synchron auf dem UI-Thread** |
|
||||
| `src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs:102` | `DiffAlignment.Build(File?.Lines)` ebenfalls |
|
||||
|
||||
Im gesamten `ClaudeDo.Ui`-Projekt gibt es **zwei** `Task.Run`-Offloads (`RepoScanner`,
|
||||
`RestartWorkerService`). CPU-Arbeit auf dem UI-Thread ist damit die Norm, nicht die Ausnahme.
|
||||
|
||||
### Bild 3 — Worker-Stille
|
||||
|
||||
Operationen ohne UI-Auslöser. Kein Client erfährt, dass sie laufen; die UI zeigt im
|
||||
Startfall nur „reconnecting".
|
||||
|
||||
| Stelle | Warum langsam |
|
||||
|---|---|
|
||||
| 6× `src/ClaudeDo.Worker/Lifecycle/*Recovery.cs` beim Worker-Start (`OrphanRecovery`, `StaleTaskRecovery`, `PromptFileRecovery`, `AttachmentOrphanRecovery`, `PlanningLineageRecovery`, `LegacyWorktreeFolderRecovery`) | git + DB über alle Tasks/Worktrees |
|
||||
| `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` — Worktree-Anlage beim Task-Start | `git worktree add` + Branch; stille Lücke zwischen `Queued` und erster Ausgabe |
|
||||
| `src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs` | Hintergrund-git über alle Worktrees |
|
||||
| `TaskMergeService.RebaseOthersAfterMergeAsync` | läuft **innerhalb** des Merge-Calls, **nach** dem Phasen-Broadcast — vollständig unsichtbar |
|
||||
| `PlanningAggregator`, `PlanningMergeOrchestrator` | ein Merge pro Subtask |
|
||||
| `OnlineSyncService`, `UsageMonitorService`, `PrimeScheduler`, `QueueService` | periodisch, Netz/git |
|
||||
|
||||
Querverweis: `docs/superpowers/specs/2026-08-07-ui-reaktivitaet-und-listen-performance-design.md`
|
||||
führt `WorktreeManager.cs:103` als DB-Write-ohne-Broadcast (Loch 3). C3 muss prüfen, ob das
|
||||
inzwischen geschlossen ist, statt es doppelt zu beheben.
|
||||
|
||||
### Bild 4 — MCP-Stille
|
||||
|
||||
Trifft nicht die UI, sondern Agenten-Runs. Nur drei Dateien reporten Progress:
|
||||
`ExternalMcpService` (5 Tools), `TaskWaitMcpTools`, `TaskMergeService`. Ohne Progress laufen
|
||||
u.a. die 8 `batch_*`-Tools, `cleanup_task_worktree`, `get_task_diff`, `preview_merge_set`.
|
||||
|
||||
Das ist die dokumentierte Ursache des Traps „MCP 300s idle abort leaves merges stuck": der
|
||||
Client bricht bei Stille ab, während der Worker weiterarbeitet, und der Task bleibt hängen.
|
||||
|
||||
### Bewusst ausgeschlossen: der Installer
|
||||
|
||||
`ClaudeDo.Installer` hat eine vollständige `IProgress<string>`-Step-Pipeline mit Live-Anzeige
|
||||
(`Core/InstallerService.cs`, `Steps/*`, `Pages/InstallPage`). Das ist die beste Progress-Fläche
|
||||
im Repo und braucht nichts.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. UI-Primitive: `OperationStatus`
|
||||
|
||||
Eine `ObservableObject`-Klasse, die ein ViewModel als Property exponiert. Mehrere pro VM sind
|
||||
erlaubt und erwünscht — `WorktreesOverviewModalViewModel` braucht getrennte für Refresh,
|
||||
Cleanup und Merge, sonst blockiert ein laufender Refresh die Cleanup-Anzeige.
|
||||
|
||||
| Property | Verhalten |
|
||||
|---|---|
|
||||
| `IsRunning` | sofort `true` → treibt `CanExecute`, verhindert Doppelklick |
|
||||
| `ShowIndicator` | erst nach **300 ms** `true` → kein Flackern bei schnellen Calls |
|
||||
| `Label` | lokalisierter Text, **mid-flight überschreibbar** (`Report(label)`) |
|
||||
| `Elapsed` | `mm:ss`, **lokal getickt** |
|
||||
| `IsStalled` | `true`, wenn **60 s ohne Aktualisierung** vergangen sind → Hinweis „läuft weiter, Worker antwortet noch" |
|
||||
|
||||
`IsStalled` bemisst sich an der Zeit seit dem letzten `Report`, **nicht** an der Gesamtdauer.
|
||||
Sonst würde ein regulär mehrminütiges Verify-Gate fälschlich als hängend gemeldet. Eine
|
||||
Operation ohne jeden `Report` gilt nach 60 s als stalled — das ist der Fall, der heute wie ein
|
||||
Absturz aussieht.
|
||||
|
||||
Benutzung:
|
||||
|
||||
```csharp
|
||||
using var op = Approve.Begin(Loc.T("ops.merge.merging"));
|
||||
var result = await _worker.ApproveReviewAsync(...);
|
||||
```
|
||||
|
||||
`Dispose` beendet die Operation auch im Exception-Fall.
|
||||
|
||||
**Zeitquelle ist injizierbar** (`TimeProvider`, in .NET 8 vorhanden), kein statischer
|
||||
`DispatcherTimer`. Ein geteilter statischer Timer wäre genau die Sorte Shared State, die die
|
||||
`Ui.Tests` reihenfolgen-abhängig flaky macht. Timer-Callbacks kommen vom Threadpool und müssen
|
||||
auf den Dispatcher gepostet werden.
|
||||
|
||||
**Fehlerbehandlung bleibt unverändert.** Weiter über `ShowErrorAsync` / `ErrorReported` /
|
||||
`FlashFooterError`. `OperationStatus` transportiert keine Fehler.
|
||||
|
||||
### 2. UI-Control: `OperationIndicator`
|
||||
|
||||
`src/ClaudeDo.Ui/Views/Controls/OperationIndicator.axaml` — Spinner (`Ellipse.spinner` aus
|
||||
`IslandStyles`) + Label + Elapsed, gebunden an eine `OperationStatus`. Ohne das Control wird
|
||||
die Spinner-StackPanel aus `MergeModalView.axaml:28-30` acht Mal von Hand nachgebaut.
|
||||
|
||||
Alle Texte über Locale-Keys im Namespace `ops.*`; en/de-Parität erzwingen die
|
||||
`Localization.Tests`.
|
||||
|
||||
### 3. Worker-Kanal: ein generisches Progress-Event
|
||||
|
||||
Der Worker sendet nur, was das UI **nicht erraten kann** — interne Phasen und Zählstände. Beim
|
||||
Klick weiß das UI selbst, was es angestoßen hat.
|
||||
|
||||
```
|
||||
OperationProgress(string opKey, string phase, int current, int total)
|
||||
```
|
||||
|
||||
- `opKey` = TaskId bei task-gebundenen Operationen, sonst ein stabiler String
|
||||
(`"worktree-cleanup"`, `"startup-recovery"`, `"planning-integration:<taskId>"`)
|
||||
- **Kein Elapsed auf der Leitung** — das tickt das UI
|
||||
- `current`/`total` erlaubt echten Fortschritt („Worktree 3/12"), was das heutige
|
||||
`MergeProgress` nicht kann
|
||||
|
||||
Dieses Event **ersetzt** `MergeProgress`. Begründung, dass sich die Verallgemeinerung lohnt:
|
||||
es gibt drei Produzenten (Merge-Phasen, Worktree-Cleanup, Planning-Integration), nicht einen.
|
||||
|
||||
**Aber `IWorkerClient.MergeProgressEvent` bleibt als dünner Forwarder bestehen.** Sonst
|
||||
zerstört C1 die Gruppen-Isolation: A1 abonniert dieses Event, und ein Rename würde C zwingen,
|
||||
`DetailsIslandViewModel` zu ändern — eine Datei, die Gruppe A besitzt. Der Forwarder kostet
|
||||
vier Zeilen und hält die vier Sessions unabhängig. Er darf später entfernt werden, wenn A und
|
||||
C beide gemergt sind.
|
||||
|
||||
### 4. MCP: `ProgressReporter` als eigene Klasse
|
||||
|
||||
`TaskMergeService.RunReportingProgressAsync` (Zeile ~175) ist die einzige existierende
|
||||
Progress-Schleife. Sie wird als eigenständige Klasse extrahiert, sodass jedes MCP-Tool sie
|
||||
nutzen kann, statt die Schleife zu kopieren. Zusätzlich ein Overload für Element-Fortschritt
|
||||
(`i/n`) für die `batch_*`-Tools.
|
||||
|
||||
## Festgelegte Grenzen
|
||||
|
||||
Bewusst **nicht** Teil dieses Designs:
|
||||
|
||||
- **Kein Cancel.** Nur Sichtbarkeit. Ein Abbruch wäre bei einem Merge ohnehin irreführend: das
|
||||
Verify-Gate läuft, wenn der Merge-Commit längst geschrieben ist — „abbrechen" könnte nur
|
||||
*aufhören zu warten* bedeuten, nicht zurückrollen. Ohne Cancel entfällt der
|
||||
`opId`-Handshake; die Korrelation läuft über `taskId` bzw. das offene Modal.
|
||||
- **Keine Footer-Anzeige für laufende Operationen.** Anzeige am auslösenden Control und an der
|
||||
Task-Zeile / im Detail-Pane. Der Footer bleibt für Fehler (`FlashFooterError`) und den
|
||||
Worker-Log.
|
||||
- **Keine Prozent-Balken.** Die meisten Operationen haben kein sinnvolles Total.
|
||||
`current/total` wird als Text gezeigt, nicht als Balken.
|
||||
- **Keine EF-Migration.** Damit entfällt der Trap paralleler Migrationen.
|
||||
|
||||
## Messung
|
||||
|
||||
„Erst messen, dann instrumentieren" gilt für Bild 1 und 3 — aber an **einer** Stelle, nicht an
|
||||
zwanzig:
|
||||
|
||||
1. Ein Timing-Hook in `WorkerClient` loggt jeden Hub-Invoke mit Dauer.
|
||||
2. Ein zweiter im DB-Pfad der Islands.
|
||||
|
||||
Ein Tag Nutzung liefert eine sortierte Liste echter Ausreißer. A5 und C entscheiden danach,
|
||||
statt zu raten. Bild 2 und 4 brauchen keine Messung — der Befund ist aus dem Code eindeutig.
|
||||
|
||||
Faustregel für den Zweifelsfall: instrumentiert wird, was git aufruft, Netz nutzt, einen
|
||||
Prozess startet, oder O(n) über unbegrenzt viele DB-Zeilen läuft. Einzelzeilen-Reads nicht.
|
||||
|
||||
## Parallelisierung: vier Merge-Helper-Gruppen
|
||||
|
||||
Der Zuschnitt folgt dem **Datei-Eigentum**, nicht der Fachlichkeit — nur so können vier
|
||||
Sessions gleichzeitig laufen, ohne sich zu überschreiben.
|
||||
|
||||
```
|
||||
P0 (Fundament, muss allein zuerst landen)
|
||||
├── Gruppe A Bild 1: UI-Stille (5 Pakete)
|
||||
└── Gruppe B Bild 2: UI-Freeze (3 Pakete)
|
||||
|
||||
Gruppe D Bild 4: MCP (4 Pakete) ← braucht P0 NICHT, kann sofort starten
|
||||
Gruppe C Bild 3: Worker (5 Pakete) ← wartet auf den Peer-Commit
|
||||
```
|
||||
|
||||
| Gruppe | Exklusiv besessene Dateien |
|
||||
|---|---|
|
||||
| **P0** | `OperationStatus.cs`, `OperationIndicator.axaml`, **beide `locales/*.json`** |
|
||||
| **A** | Island-VMs, Modal-VMs und deren AXAML |
|
||||
| **B** | `DiffViewerViewModel.cs`, `DiffTextView.axaml.cs` |
|
||||
| **C** | `HubBroadcaster`, `WorkerHub`, `WorkerClient`, `IWorkerClient`, `StubWorkerClient`, `Lifecycle/*Recovery`, `WorktreeManager` |
|
||||
| **D** | `Worker/External/*McpTools`, `ExternalMcpService` |
|
||||
|
||||
**Der einzige echte Konfliktkandidat sind die Locale-Dateien.** A und C brauchen Keys — D
|
||||
nicht: MCP-Progress-Meldungen gehen an Agenten, nicht an den Nutzer, und bleiben englische
|
||||
Klartext-Strings ohne Locale-Eintrag.
|
||||
|
||||
Regel: **P0 legt die Keys für A und C vorab an**, danach fasst keine Gruppe die JSONs mehr an.
|
||||
Braucht eine Gruppe doch einen nicht vorgesehenen Key, hängt sie ihn als **letzte** Änderung
|
||||
des Pakets an und behandelt ihn im Merge-Helper als bekannte Konfliktstelle.
|
||||
|
||||
Drei erzwungene Serialisierungen:
|
||||
- `TaskMergeService` — die Parallelsession arbeitet dort; C1 danach.
|
||||
- `WorkerClient.cs` — P0-2 setzt dort den Timing-Hook, C ändert dieselbe Datei. P0 liegt
|
||||
ohnehin vor C, damit unkritisch — aber C darf nicht vor P0 starten.
|
||||
- Innerhalb jeder Gruppe `serializeOnFileOverlap` auf der Liste setzen.
|
||||
|
||||
Ergänzung zum Datei-Eigentum: `Services/UpdateCheckService.cs` gehört zu **A** (Paket A3),
|
||||
`Services/WorkerClient.cs` zu **C**. Beide liegen im selben Ordner, sind aber verschiedene
|
||||
Dateien.
|
||||
|
||||
### Abhängigkeit zur Parallelarbeit
|
||||
|
||||
Während dieses Designs hat eine andere Session im gemeinsamen `main`-Checkout das
|
||||
Merge-Feedback für das **MergeModal** implementiert: Phasen-Broadcast in `TaskMergeService`,
|
||||
`HubBroadcaster.MergeProgress`, `IWorkerClient.MergeProgressEvent`,
|
||||
`MergeModalViewModel.ProgressMessage`, Spinner in `MergeModalView.axaml`, plus Tests.
|
||||
|
||||
Konsequenzen:
|
||||
- Der Merge-Fall im MergeModal ist **Vorarbeit**, nicht neu zu bauen.
|
||||
- Zwei Nachbesserungen bleiben: A1 schließt den Approve-Pfad im Detail-Pane an, C1 stellt
|
||||
Elapsed auf UI-lokalen Tick um (die Implementierung lässt den Worker alle 30 s ticken —
|
||||
die ersten 30 Sekunden zeigt sie nur „merging").
|
||||
- C darf erst starten, wenn dieser Commit liegt.
|
||||
|
||||
## Pakete
|
||||
|
||||
### P0 — Fundament
|
||||
|
||||
| # | Inhalt |
|
||||
|---|---|
|
||||
| P0-1 | `OperationStatus` + `OperationIndicator` + `ops.*`-Keys **für alle vier Gruppen vorab** + `Ui.Tests` mit Fake-`TimeProvider` |
|
||||
| P0-2 | Timing-Hook in `WorkerClient` + im DB-Pfad der Islands |
|
||||
| P0-3 | Regel in `src/ClaudeDo.Ui/CLAUDE.md` und `src/ClaudeDo.Worker/CLAUDE.md`: jeder `IWorkerClient`-Call in einem `[RelayCommand]` läuft durch eine `OperationStatus`; MCP-Tools über ~5 s reporten Progress |
|
||||
|
||||
### Gruppe A — Bild 1: UI-Stille
|
||||
|
||||
| # | Inhalt |
|
||||
|---|---|
|
||||
| A1 | Detail-Pane: Approve & Merge, Submit for review, Reject, Park, Preview-Merge. Abonniert das Merge-Phasen-Event zum Nachschärfen des Labels |
|
||||
| A2 | WorktreesOverview: Refresh, Cleanup, Reset, ForceRemove, Batch-Merge (inkl. Zeilen-Status) |
|
||||
| A3 | Settings-Tabs: SessionSkill install/update, Restore-Defaults, OnlineInbox-SignIn, RepoImport-Scan, Update-Check |
|
||||
| A4 | Reports und Planning-UI: GenerateWeekReport, RunDailyPrepNow, BuildPlanningIntegrationBranch, GetPlanningAggregate, Finalize/QueuePlanningSubtasks |
|
||||
| A5 | Island-DB-Pfade — **nur die, die P0-2 als Ausreißer zeigt** |
|
||||
|
||||
### Gruppe B — Bild 2: UI-Freeze
|
||||
|
||||
| # | Inhalt |
|
||||
|---|---|
|
||||
| B1 | `UnifiedDiffParser.Parse` nach `Task.Run` + Indikator im DiffViewer |
|
||||
| B2 | `DiffAlignment.Build`: Grenze definieren (ab N Zeilen auslagern oder inkrementell aufbauen) |
|
||||
| B3 | Guard-Test: Parse/Build über ein großes Fixture blockiert den Dispatcher nicht |
|
||||
|
||||
### Gruppe C — Bild 3: Worker-Stille
|
||||
|
||||
| # | Inhalt |
|
||||
|---|---|
|
||||
| C1 | `OperationProgress(opKey, phase, current, total)` ersetzt `MergeProgress`; Elapsed auf UI-lokalen Tick |
|
||||
| C2 | Startup-Recovery sichtbar (6 Services) — Anzeige im Shell statt nur „reconnecting" |
|
||||
| C3 | Worktree-Anlage beim Task-Start sichtbar an der Task-Zeile. Vorher prüfen, ob der Broadcast-Gap aus dem 08-07-Spec schon geschlossen ist |
|
||||
| C4 | `RebaseOthersAfterMergeAsync` und `WorktreeMaintenanceService` in den Kanal |
|
||||
| C5 | Periodische Dienste (Usage, OnlineSync, Prime, Queue): nur Aktivität und Fehler, kein Tick-Spam |
|
||||
|
||||
### Gruppe D — Bild 4: MCP
|
||||
|
||||
| # | Inhalt |
|
||||
|---|---|
|
||||
| D1 | `ProgressReporter` aus `TaskMergeService.RunReportingProgressAsync` extrahieren, inkl. `i/n`-Overload |
|
||||
| D2 | Die 8 `batch_*`-Tools mit Element-Fortschritt |
|
||||
| D3 | Worktree- und Diff-Tools: `cleanup_task_worktree`, `batch_cleanup_task_worktrees`, `get_task_diff`, `preview_merge_set` |
|
||||
| D4 | Restliche Long-Runner + Doku-Regel im Worker-`CLAUDE.md` |
|
||||
|
||||
## Test-Strategie
|
||||
|
||||
- **`OperationStatus`**: reine Unit-Tests mit Fake-`TimeProvider` — 300-ms-Grace, 60-s-Stall,
|
||||
Elapsed-Formatierung, `Dispose` im Exception-Fall, `Report` überschreibt Label.
|
||||
- **VM-Tests** (`Ui.Tests`, headless): Command setzt `IsRunning`, `CanExecute` sperrt während
|
||||
des Laufs, Zustand ist nach einer Exception zurückgesetzt. Fakes: `StubWorkerClient` muss
|
||||
bei jedem neuen Event mitwachsen.
|
||||
- **B3** ist der einzige Test, der Blockierung prüft: großes Diff-Fixture, Messung, dass der
|
||||
Dispatcher weiter Nachrichten verarbeitet.
|
||||
- **Worker/MCP** (`Worker.Tests`): Progress-Callbacks werden mit einem Fake-`IProgress`
|
||||
gezählt; keine echte Claude-CLI, keine echten Timeouts.
|
||||
- **Visuell**: jedes Paket in A und B hat eine offene visuelle Prüfung. Spinner, Grace-Periode
|
||||
und Layout kann kein Test bestätigen — das läuft über den Nutzer.
|
||||
Reference in New Issue
Block a user