diff --git a/docs/explore-notes/review-merge.md b/docs/explore-notes/review-merge.md index dfd06551..e4e0674e 100644 --- a/docs/explore-notes/review-merge.md +++ b/docs/explore-notes/review-merge.md @@ -65,7 +65,15 @@ allowed to reach `Done`. On failure `MergeResult.Status` comes back `TaskMergeService.StatusVerifyFailed` (`"verify_failed"`) with an output excerpt in `ErrorMessage`. This flows through `MergeResultDto` (hub) and `ReviewTaskResult` (`review_task`) unchanged, because both already -treat any non-`blocked`/`conflict` status generically. +treat any non-`blocked`/`conflict` status generically. All three UI merge entry points handle it +explicitly (detail-pane Approve, merge modal, worktrees batch) — a generic fallback there showed +the raw status string instead of the failure. + +**Worktree-less approvals are gated too.** A task with no active `WorktreeEntity` — a sandbox run, +or a list-handler task that commits straight into `list.WorkingDir` — skips the merge entirely, +but `ApproveAndMergeAsync` still runs the verify command (same per-repo gate, working dir = +`list.WorkingDir`) before the task may reach `Done`. Without that, the run that lands the most on +the target branch at once would be the one run nothing checks. **Serialization:** a process-wide `ConcurrentDictionary` keyed by `list.WorkingDir` serializes `MergeAsync` / `ContinueMergeAsync` (git ops + verify) per repo, diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md index 22e55bb7..8213da06 100644 --- a/src/ClaudeDo.Ui/CLAUDE.md +++ b/src/ClaudeDo.Ui/CLAUDE.md @@ -96,6 +96,7 @@ sets the result on save/cancel, the caller awaits the TCS. ## Gotchas - **`PathIcon` *fills* its geometry.** Line-art/stroke icons must be authored as filled geometry or rendered with a stroked `Path` (e.g. `Icon.PlanDay` via the `Path.plan-icon` style). A pure stroke path in a `PathIcon` is **invisible**. +- **`NumericUpDown.Value` is `decimal?` and goes null while the box is empty** — i.e. every time the user clears a value to type a new one. Bound TwoWay to a non-nullable `int`/`decimal`, that null throws `InvalidCastException`. Either bind a `decimal?` property (as `AgentConfigEditorViewModel.MaxTurns` does) or add `Converter={StaticResource KeepLastNumber}`, which drops the null via `BindingOperations.DoNothing`. - **Never bind bare punctuation gestures.** Window key bindings live on `MainWindow` (`Ctrl+K` search, `Ctrl+N` add-task). `OemQuestion` once held search focus and silently swallowed `#` app-wide on a German layout. - **`FocusClearing`'s Escape handler is scoped to `MainWindow`** (`AddClassHandler`, not ``) — it clears focus from a TextBox on Escape, mirroring click-outside. Modals are separate `Window` instances that bind their own Escape → close, so it never runs there. Mission Control's ConPTY tiles are in `MissionControlWindow`, also unaffected, so **Escape always reaches the PTY**. - **Review gate:** Approve & Merge stays disabled until the diff has been opened once, and re-locks per run → [review-merge](../../docs/explore-notes/review-merge.md).