From d2ca7fb500956be5ac0cae874ac6e0a0e6d28660 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 5 Aug 2026 11:49:33 +0200 Subject: [PATCH] feat(ui): add roadblock reply field to the ROADBLOCK card A task that reports a roadblock but finishes successfully (Done/WaitingForReview/ Failed/Cancelled) had no way to answer it short of a full reset-and-rerun, losing the run's context. Adds a reply textbox + Send button to the existing ROADBLOCK card, modeled on the AskUser question card, that resumes the session via ContinueTaskAsync with the user's own text. Gated on LatestRunSessionId (disabled with a hint when there's nothing to resume); failures surface through the footer error strip instead of a modal. --- docs/open.md | 6 + src/ClaudeDo.Localization/locales/de.json | 6 + src/ClaudeDo.Localization/locales/en.json | 6 + src/ClaudeDo.Ui/CLAUDE.md | 2 +- .../Islands/DetailsIslandViewModel.cs | 38 +++ .../ViewModels/IslandsShellViewModel.cs | 1 + .../Views/Islands/DetailsIslandView.axaml | 19 ++ .../DetailsIslandRoadblockReplyTests.cs | 244 ++++++++++++++++++ 8 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandRoadblockReplyTests.cs diff --git a/docs/open.md b/docs/open.md index 4dd410e2..cb3241fc 100644 --- a/docs/open.md +++ b/docs/open.md @@ -74,6 +74,12 @@ Offene Entscheidungen dazu: `WaitingForReview`, and the detail pane's diff/merge card shows the full range of everything the run merged to main (via the new `HandlerBaseCommit`/`HandlerHeadCommit` fallback — no `WorktreeEntity` is created for this task, so the diff comes from `list.WorkingDir` directly). +- **Roadblock reply field (2026-08-05)** — build + unit tests all green, but **not visually + verified**: on a `Done` (or `WaitingForReview`/`Failed`/`Cancelled`) task with a reported + roadblock, the ROADBLOCK card shows a reply textbox + Send button under the roadblock text. + Check layout/spacing against the AskUser question card it's modeled on, Enter-to-send, the + disabled state + hint text when there's no session ID to resume, and that an override-slot-busy + error shows up in the footer log strip (not a modal) with the typed text still in the field. Approve should go straight to `Done` with no merge attempt. Design choice: the review range lives as two new nullable columns directly on `TaskEntity` (not a phantom `WorktreeEntity` row), specifically so `list_worktrees`/the Worktrees overview never see it. diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 4690448c..bd53c050 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -215,6 +215,12 @@ "prepTitle": "Tagesvorbereitung", "planDay": "Tag planen", "prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen", + "roadblockReply": { + "placeholder": "Auf den Roadblock antworten…", + "send": "Senden", + "noSession": "Keine Sitzung zum Fortsetzen — dieser Lauf hat keine Session-ID zum Antworten.", + "failed": "Antwort konnte nicht gesendet werden: {0}" + }, "attachments": { "sectionLabel": "ANHÄNGE", "dropToAttach": "Zum Anhängen ablegen", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 7c1fb6bc..3077f6c5 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -215,6 +215,12 @@ "prepTitle": "Daily prep", "planDay": "Plan day", "prepEmpty": "No prep run today yet — click Plan day", + "roadblockReply": { + "placeholder": "Reply to the roadblock…", + "send": "Send", + "noSession": "No session to resume — this run has no session ID to reply into.", + "failed": "Couldn't send the reply: {0}" + }, "attachments": { "sectionLabel": "ATTACHMENTS", "dropToAttach": "Drop to attach", diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md index 77e3f794..a5325f81 100644 --- a/src/ClaudeDo.Ui/CLAUDE.md +++ b/src/ClaudeDo.Ui/CLAUDE.md @@ -36,7 +36,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle - **IslandsShellViewModel** — root coordinator; owns the three island VMs and the `WorkerClient`, wires cross-island events (selection, notes/prep mode, conflict resolution), owns connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay via `OpenLogVisualizerCommand`; `FlashFooterError` surfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (`ShowLists`/`ShowDetails` by window width), `PrimeStatus` flash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plus `RestartWorkerAsync`/`CheckForUpdatesAsync`. Hosts `UpdateCheckService`. - **ListsIslandViewModel** — smart lists (My Day, Important, Planned, virtual queued/running/review), user lists, selection, list CRUD, drag-reorder, badge counts, opens list settings / repo import / worktrees overview, `OpenInExplorer`/`OpenInTerminal`. - **TasksIslandViewModel** — open/overdue/completed groups for the selected list with hierarchy-aware regrouping; task CRUD, drag-reorder, toggle done/star, schedule, enqueue/dequeue, cancel; review actions (approve, reject-rerun, reject-park, cancel); planning session lifecycle (open/resume/discard/finalize, `QueuePlanningSubtasksAsync`); `RefineTask`, `OpenConPtySessionRequested` (embedded ConPTY terminal), `ToggleManual` (per-task manual flag) and `SyncInteractiveSessions` (mirrors Mission Control's open ConPTY panes onto the rows); MyDay extras (`IsMyDayList`, `ClearDayCommand`, `ShowPrepLogCommand`) and the pinned Notes pseudo-row (`ShowNotesRow`, `OpenNotesCommand`). Raises `NotesRequested`/`PrepRequested` events consumed by the shell. -- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards), the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel`, call `ShowDiffViewer`, and fire the `DiffViewed` callback; `HasReviewableDiff` reports whether anything is inspectable, feeding the review gate), **PrepPanelViewModel** (daily-prep panel: `PrepLog`, `PlanDayCommand` → `RunDailyPrepNowAsync`, persisted last run via `GetLastPrepLogAsync`). Attachments: `Attachments` (`ObservableCollection`), `IsDragOver`, `DropStatus`, `CanAcceptDrop`, `AddFilesAsync`, `RemoveAttachmentCommand`; loads on task change; `ComposedPreview` includes attachment paths. Writes directly via `new AttachmentStore()` + `new TaskAttachmentRepository(ctx)`. Helper rows (`ChildOutcomeRowViewModel`, `SubtaskRowViewModel`, `LogLineViewModel`, `AttachmentRowViewModel`) live in the same file. +- **DetailsIslandViewModel** — the detail pane for a bound `TaskRowViewModel`. Owns live-log streaming (`Log` via `StreamLineFormatter`), debounced title/description editing, subtasks, session-outcome/roadblock split (splits `Result` at the roadblock marker into two cards) — the ROADBLOCK card also has a reply field (`RoadblockReplyDraft`/`SendRoadblockReplyCommand`, gated by `CanReplyToRoadblock` on `LatestRunSessionId`) that resumes the session via the same `ContinueTaskAsync` transport as `ContinueCommand` but with the user's own text instead of the fixed re-run prompt; failures raise `ErrorReported`, wired by the shell into `FlashFooterError`, the three-tab work console (`output`/`git`/`session`), child surfacing (`ChildOutcomes` rows plus `ChildrenNeedingAttention`/`HasChildrenNeedingAttention` — children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible when `HasChildOutcomes`), and the modes: `IsNotesMode` (hosts `NotesEditorViewModel`), `IsPrepMode`, computed `IsTaskDetailVisible = !IsNotesMode && !IsPrepMode`. Three concerns are extracted into section VMs exposed as properties: **AgentConfigEditorViewModel** (scope=Task; per-task Model/MaxTurns/AgentPath overrides with `InheritedBadge` + `InheritanceResolver`, additive SystemPrompt, debounced auto-save; exposed as `AgentSettings`), **MergeSectionViewModel** (merge-target selection, mergeability indicator via `MergePreviewPresenter` over `PreviewMergeAsync`, `OpenDiffAsync` and `ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel`, call `ShowDiffViewer`, and fire the `DiffViewed` callback; `HasReviewableDiff` reports whether anything is inspectable, feeding the review gate), **PrepPanelViewModel** (daily-prep panel: `PrepLog`, `PlanDayCommand` → `RunDailyPrepNowAsync`, persisted last run via `GetLastPrepLogAsync`). Attachments: `Attachments` (`ObservableCollection`), `IsDragOver`, `DropStatus`, `CanAcceptDrop`, `AddFilesAsync`, `RemoveAttachmentCommand`; loads on task change; `ComposedPreview` includes attachment paths. Writes directly via `new AttachmentStore()` + `new TaskAttachmentRepository(ctx)`. Helper rows (`ChildOutcomeRowViewModel`, `SubtaskRowViewModel`, `LogLineViewModel`, `AttachmentRowViewModel`) live in the same file. - **TaskRowViewModel** / **ListNavItemViewModel** — lightweight display VMs (task row: status, planning phase, parent/blocked links, roadblock count, computed `IsDraft`/`IsPlanned`/`IsChild`/`IsPlanningParent`/`CanRefine`; plus `IsManual` (→ MANUAL badge; suppresses `CanSendToQueue`/`CanRefine`/`CanOpenPlanningSession`) and `HasInteractiveSession` (→ accent "Interactive" chip instead of "Parked"; tapping it jumps to that Mission Control pane); list row: kind Smart/Virtual/User, count, icon/dot keys, drop hints, `IsManual`). - **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`. - **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows). General hosts the per-model preset table (`ModelPresets` → `ModelPresetRowViewModel`: effort + max turns per alias) which **replaced** the single global "Max turns" field, `ListSettingsModalViewModel` (name, working dir, commit type, "manual list" flag, delete list; hosts shared `AgentConfigEditorViewModel` as `Agent` property (scope=List) — save delegates to `Agent.SaveAsync()`), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `MergeHelperSelectionModalViewModel` ("Let Claude handle it": checkbox picker over one list's non-terminal, non-manual tasks, pre-ticks the actionable ones; list-scoped only — `Configure(listId, listName)`, no global scope. Opened from the list row's context menu, which is hidden when the list has no working dir; on confirm `ListsIslandViewModel` raises `LetClaudeHandleRequested` → shell → `MissionControlViewModel.OpenMergeHelperConPtySessionAsync`, which first calls `IWorkerClient.CreateMergeHelperTaskAsync` to create one new ClaudeDo task per run in that list — `Idle`/`IsManual=true` (never queued), title/description localized (`missionControl.mergeHelperTaskTitle`/`mergeHelperTaskDescriptionHeader`), `TaskEntity.HandlerBaseCommit` stamped to the list repo's current HEAD — then opens a **task-based** ConPTY tile for it (deduped by `TaskId` like `OpenConPtySessionAsync`, not `CreateAdHoc`) running the five-phase handler prompt. The handler still merges the tasks it handles itself; the host task never gets a worktree of its own, so "Submit for review" stamps `HandlerHeadCommit` instead of committing a worktree, and the detail pane's `MergeSectionViewModel` falls back to `HandlerBaseCommit`/`HandlerHeadCommit` over the list's working dir for its diff — see `TaskEntity` in `ClaudeDo.Data/CLAUDE.md`), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`, `LogVisualizerViewModel` (worker logs, last 30 min, all levels + a warn/error-only filter; loads via `GetRecentLogsAsync`). diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index c4d5ade3..0662135d 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -191,8 +191,24 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] + [NotifyPropertyChangedFor(nameof(CanReplyToRoadblock))] + [NotifyCanExecuteChangedFor(nameof(SendRoadblockReplyCommand))] private string? _latestRunSessionId; + // A resumable session to reply to a reported roadblock with (via ContinueAsync's + // transport). Distinct from ContinueCommand: that one is Failed/Cancelled-only and + // reruns with a fixed prompt, this one answers a roadblock reported on any terminal + // state (see ShowRoadblockCard) with the user's own text. + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SendRoadblockReplyCommand))] + private string _roadblockReplyDraft = string.Empty; + + public bool CanReplyToRoadblock => !string.IsNullOrEmpty(LatestRunSessionId); + + // Surfaces a UI-originated failure (e.g. override slot busy) to the shell's footer + // error strip — mirrors TasksIslandViewModel.ErrorReported. + public event Action? ErrorReported; + [ObservableProperty] private string? _model; [ObservableProperty] private string? _worktreePath; @@ -333,6 +349,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable DequeueCommand.NotifyCanExecuteChanged(); ResetAndRetryCommand.NotifyCanExecuteChanged(); ContinueCommand.NotifyCanExecuteChanged(); + SendRoadblockReplyCommand.NotifyCanExecuteChanged(); } }; _worker.PropertyChanged += _workerPropertyChangedHandler; @@ -524,6 +541,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable Task = row; OnPropertyChanged(nameof(TaskIdBadge)); Monitor.Reset(); + RoadblockReplyDraft = string.Empty; Subtasks.Clear(); ChildOutcomes.Clear(); Attachments.Clear(); @@ -1025,6 +1043,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable private bool CanContinue() => Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId); + [RelayCommand(CanExecute = nameof(CanSendRoadblockReply))] + private async System.Threading.Tasks.Task SendRoadblockReplyAsync() + { + if (Task == null) return; + var text = RoadblockReplyDraft; + if (string.IsNullOrWhiteSpace(text)) return; + try + { + await _worker.ContinueTaskAsync(Task.Id, text); + RoadblockReplyDraft = string.Empty; + } + catch (Exception ex) + { + ErrorReported?.Invoke(Loc.T("details.roadblockReply.failed", ex.Message)); + } + } + + private bool CanSendRoadblockReply() => + Task != null && _worker.IsConnected && CanReplyToRoadblock && !string.IsNullOrWhiteSpace(RoadblockReplyDraft); + [RelayCommand(CanExecute = nameof(CanResetAndRetry))] private async System.Threading.Tasks.Task ResetAndRetryAsync() { diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs index 204d74f2..7f689ffa 100644 --- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs @@ -256,6 +256,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable if (Lists.SelectedList is { } row) Lists.LetClaudeHandleListCommand.Execute(row); }; + Details.ErrorReported += FlashFooterError; Details.CloseDetail = () => Tasks.SelectedTask = null; Details.DeleteFromList = row => { diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml index 255a3408..7528c50a 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml @@ -78,6 +78,25 @@ + + + + + + +