Approve, Submit, Reject, and Park each get their own OperationStatus so the
button locks and shows an OperationIndicator for the duration of the call;
Approve additionally scopes a MergeProgressEvent subscription to its own
task id to sharpen the label from "merging" to "verifying" mid-flight.
MergeSectionViewModel's preview refresh gets the same treatment for display
only, since it has no command button to gate.
Foundation for long-running-operation feedback (P0-1). OperationStatus tracks
IsRunning/ShowIndicator(300ms grace)/Elapsed/IsStalled(60s since last Report)
via an injectable TimeProvider, no static timer. OperationIndicator binds a
status to the shared spinner style. Pre-provisions ops.* locale keys for
groups A and C; no ViewModel wired up yet.
The merge commit message was hand-rolled per caller ("Merge task: <title>",
"Merge <branch>", "Merge subtask") and ignored the task's commit type. Every
caller now passes a blank message and TaskMergeService fills in
CommitMessageBuilder.BuildMerge -> {commitType}(list-slug): merge <title> plus the
ClaudeDo-Task trailer; the merge modal prefills it from GetMergeTargets.
A merge whose list has a verify command holds the MergeTask call for minutes (5m46s
on this repo), during which the modal only disabled its button - no spinner, no
message, so a landed merge looked like a dead app. TaskMergeService now broadcasts
MergeProgress(taskId, phase, elapsedSeconds) for the merging and verifying phases
(re-reported every 30s) plus a WorkerLog line when verify starts; the modal shows a
spinner and the localized phase.
Padding on RowsListBox mirrored the ScrollViewer.Padding-excluded-from-
Extent trap fixed elsewhere today, so move the 10,4 inset onto the
VirtualizingStackPanel's Margin instead, matching the other scrollables.
Headless measurement showed Avalonia's Fluent ListBox template actually
forwards Padding onto PART_ItemsPresenter's margin already (content
side), so this particular control was never affected -- both variants
produce an identical Extent and a fully visible last row at max scroll.
Kept the change for consistency and corrected the CLAUDE.md gotcha to
reflect the verified behavior instead of assuming the same trap applied.
From the 2026-08-11 unpushed-commit review (Low). Both in `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`. **Visual verification by the user is required — do not claim either is fixed without a screenshot; list both as open visual checks in the result.**
## A) Chain step badge is drawn under the task card
Commit `eb66ae7` moved `Border.chain-step-badge` from column 0 into column 1 with `Margin
ClaudeDo-Task: 9ec7d4ea-272b-4645-8da4-41ff3621f2f0
The indent column was hardcoded to 24px, so every row reserved the
gutter even with no child/chain relation. Restore Auto sizing with the
width back on the Border, which collapses to 0 while it is invisible.
Move the step badge into column 1 with a negative margin so its desired
width can never widen the Auto column (same on-screen position).
Reuses the existing 24px indent track (now gated on ShowAsChild OR
ShowAsChainMember) instead of a second mechanism, adds a round step-number
badge centered on the rail line, and a dimmed "after X" chip for chain
members whose head isn't in view or that render as a planning child.
Slice 4/5 of task-numbers: TaskRowViewModel.Number renders as a dimmed
"#123" before the row title; DetailsIslandViewModel.TaskIdBadge now
shows "#123" instead of the unusable "#T<guid-prefix>" handle; and the
curated WorkerLog business events in TaskRunner, TaskMergeService, and
TaskResetService prefix their quoted title with "#<Number>".
Avalonia 12 leaves ScrollViewer.Padding out of the scroll Extent, so at
maximum offset the content still overhangs the viewport by the padding
height — the last lines were cut off and unreachable even after
ScrollToEnd(). Move the inset onto the content (Margin on the inner
ItemsControl/StackPanel) and leave the ScrollViewer padding-free.
Affects the work console Output/Git/Session tabs, the reusable session
terminal (task + prep log) and the log visualizer.
Shorten the review-prompt placeholder and localize it (session.reviewFeedbackPlaceholder,
en+de) instead of a hardcoded string that overran the narrow column. Switch the growing
TextBox's VerticalContentAlignment from Center to Top so multi-line input grows downward
instead of being clipped top/bottom, add an auto scrollbar past MaxHeight, and top-align
the prompt glyph and Resume button to match.
Cards touched with Margin=0. Add margin on the inner task-row border
instead of the ListBoxItem style, so the drag/drop hit-test container
still covers the row and the drop-hint doesn't flicker between items.
Replace the three ItemsControl sections (Overdue/Open/Completed) with a single
flat Rows collection (HeaderRow | TaskRowViewModel) bound to a ListBox +
VirtualizingStackPanel, so the realized container count stays bounded instead
of growing with the list (measured: ~7-8 realized containers at any scroll
position with 1000 source rows). Group headers become regular row entries,
each section is simply omitted from Rows when empty, and the Completed
section's Clear-completed action moves onto the header row itself.
SectionFor/ReorderAsync/FindNextInSameSection now read a row's section off
the nearest preceding HeaderRow in Rows instead of three separate bound
collections, and ScrollSelectedIntoView goes through ListBox.ScrollIntoView so
it still works for rows outside the realized window. Drag/reorder behavior,
grouping/counts, and the Clear-completed button are unchanged.
Auto-scroll-while-dragging and the cross-section reorder bug are explicitly
out of scope (follow-up tasks).
Two follow-ups to the Phase 3 reconcile tick:
- LogVisualizer and WorktreesOverview only stopped their timer in the Close
command, but ShowLogVisualizerAsync/ShowWorktreesOverviewAsync had no
dlg.Closed fallback (unlike the MergeHelper picker). An Alt+F4 or owner
close left a 4s timer polling the worker / re-running LoadAsync on an
orphaned VM, accumulating one per open. Extracted StopReconcileTick on both
VMs and wired it from Closed.
- The tick scanned Items linearly per id; at the 500-row cap that is ~250k
string comparisons every few seconds. Index the rows once instead.
TasksIslandViewModel now runs a periodic tick that diffs the flat Items
collection against SQLite and patches properties in place (capped at 500
rows, sharing the Phase 1 delta sequence guard so it never overtakes a
fresher broadcast). Never rebuilds rows or triggers Regroup/LoadForList, so
it stays decoupled from the parallel Phase 2b virtualization work. The same
cadence now refreshes the long-lived Worktrees Overview, Log Visualizer, and
Merge Helper selection overlays; short-lived modals are untouched.
get_task/batch_get_tasks now return failureReason (max_turns|timeout|error|
cancelled|unknown) plus failureTurnsUsed/failureMaxTurns on a Failed task, so
max_turns (worktree usually fine, continue_task) is distinguishable from a
real error (reset_failed_task) without pulling get_task_log's raw NDJSON.
Classified and stamped onto TaskEntity by TaskRunner.MarkFailed via
TaskStateService.FailAsync; TaskRunEntity also keeps the CLI's raw
terminal_reason/result_subtype/errors for deeper diagnosis. reset_failed_task's
description now warns explicitly that it discards the worktree and points at
continue_task for max_turns. Surfaced on the task card's status-chip tooltip.
Collapse the grabbed row out of layout instead of overlapping neighbors
with scale/opacity/shadow (the ghost window already shows the moving
snapshot). Replace the thin moss drop-hint lines with a dashed
placeholder gap sized like a collapsed row. Suppress the ordinary hover
highlight and transitions on whatever row is currently the drop target
so it doesn't read as "clickable" while dragging.
MergeHelperSelectionModalViewModel now loads ParentTaskId and orders
rows hierarchy-aware (parent immediately followed by its children,
mirroring TasksIslandViewModel.Regroup). Children render indented with
the same indent-track visual as the main task list; ticking a parent
cascades selection to its children, while a child can still be
deselected on its own. A child whose parent got filtered out (e.g.
already Done) renders top-level with an explicit hint instead of
disappearing. Confirm keeps returning parents ahead of their children.
The diff viewer used ShowDialog at both call sites (task details, worktrees
overview), blocking navigation of the rest of the app while open. Switch both
to Window.Show(owner) and reuse the one open window (swap DataContext,
Activate()) instead of stacking a second one on re-open.
Add an opt-in maximize/restore button + double-click-titlebar toggle to
ModalShell (ShowMaximizeButton, default false so other modals are untouched),
wired on for DiffViewerView only. Reuses the existing Icon.WinMax/WinRestore
geometry and OffScreenMargin inset from MainWindow's own maximize handling.
The review gate (Approve & Merge locked until the diff is opened) already
fires DiffViewed before the dialog/window is shown, so it is unaffected by
the modal-to-non-modal change.
The prime_schedules.prompt_override column existed end-to-end but was
never populated or read. DailyPrepPrompt.BuildPrompt now takes an
optional override and appends it as an extra paragraph after the fixed
prompt (additive, never a replacement, so a user can't disable the
required get_daily_prep_candidates/set_my_day flow). PrimeRunner passes
schedule.PromptOverride through. The Prime tab in Settings now has a
multiline field per schedule wired to a new PromptOverride property on
PrimeScheduleRowViewModel (blank persists as null).
ModalShell moved its window by assigning Window.Position on every pointer-move,
which bypasses the OS move loop entirely - so Windows never saw a drag gesture
and no snap was ever recognised. Dragging the diff viewer to the top edge did
nothing. The titlebar now calls Window.BeginMoveDrag, which hands the drag to the
window manager and brings back maximise-on-top-edge, half-screen side snap, snap
layouts and the preview overlay. No opt-in flag is needed: the OS only snaps
resizable windows, so the non-resizable modals stay as they are.
The stale comment claiming BeginMoveDrag does not work under Avalonia 12 was
wrong - the method is there in 12.0.4. The VisualRoot cast was the actual problem,
and the window still has to come from TopLevel.GetTopLevel.
Also inset the shell by the window's OffScreenMargin, mirroring what MainWindow
already does for itself. The modals share MainWindow's extended-client-area flags,
so without it a maximised modal overhangs the screen by the invisible resize
border and gets its edges and close button clipped.
Three things were wrong with the fresh side-by-side viewer:
- The panes scrolled independently. The sync hunted the editors' templated
ScrollViewers at load time, but the viewer starts collapsed (no file selected),
so nothing was ever measured, no template existed, and the hook silently
no-opped for the lifetime of the window. Reading now goes through
TextView.ScrollOffsetChanged, which exists from construction; writing goes
through a lazily resolved ScrollViewer.Offset. TextEditor.ScrollToVerticalOffset
is NOT usable here - it is a silent no-op in AvaloniaEdit 12.0.0 even with the
editor templated (verified headlessly).
- Neither pane said what it showed. Added a BASE | WORKTREE header row inside
DiffTextView, sharing the pane grid's columns so each label sits over its editor.
- The layout picker was two lookalike ToggleButtons. It is now a segmented switch
(Border.segmented + Button.segment.active), with wrap demoted to an icon toggle
since it is orthogonal to the layout mode.
The scroll sync, TextMate setup, grammar switching, brush fallback and segment
struct were duplicated between this control and the 3-pane conflict resolver,
which the design doc had used as a copy-paste template. They now live in
DiffEditorSetup; both surfaces migrated. They stay separate controls on purpose -
a read-only two-way renderer over aligned rows with filler lines is not a variant
of a three-way editor over a writable document.
The resolver thereby also picks up the fixed scroll sync and the TryFindResource
brush lookup (its TryGetResource never resolved anything). All four merge tokens
happen to equal their hardcoded fallbacks, so nothing changes on screen.
Submit for Review is now disabled while a ConPTY pane is starting, has
failed to launch, or has already exited, and MissionControlViewModel
guards against a rapid double-click racing two SubmitTaskForReviewAsync
calls. A failed launch no longer permanently occupies its TaskId dedupe
slot -- a Retry button re-fetches the launch spec and restarts the pane
in place. CloseConPtySession/Dispose now also unsubscribe
SubmitForReviewRequested, matching the other pane event handlers. Also
fixes the pre-existing nullable-dereference warning in
IslandsShellViewModel.SyncInteractiveSessionChips.
TerminalView kills its child process on any logical-tree detach unless
BeginReparent() suppressed it, and RebuildOverviewGrid() detaches every
existing pane whenever one is added - so opening a second interactive
session killed the first. Focus-mode tab switches had the same defect.
PtyTerminalSession now puts the control in permanent reparent mode after
launch (teardown stays explicit via ConPtyPaneViewModel.Dispose -> Kill),
and the new ConPtyPaneHost reparents each pane's single long-lived view
across grid rebuilds and tab switches instead of letting the DataTemplate
instantiate a dead replacement.
review_task/continue_merge on a planning parent always leaves conflicts in
the tree, and the UI auto-opened the resolver on every PlanningMergeConflict
broadcast regardless of who started the merge -- so a running Claude session
resolving a unit-merge conflict could race a human editing the same shared
checkout in a resolver window neither of them asked for.
PlanningMergeOrchestrator.StartAsync now takes an externallyDriven flag (set
by ExternalMcpService's MCP-driven review_task path, left false for the UI's
ApproveReview) that rides along on the PlanningMergeConflict broadcast. The
UI only auto-opens the resolver when it's false; otherwise it shows a
persistent banner with a manual "Open resolver" button, cleared on
PlanningMergeAborted/PlanningCompleted. A new GetActiveExternalConflictsAsync
query (checked against GitService.IsMidMergeAsync rather than the in-memory
flag alone) lets the UI resync the banner on reconnect instead of trusting a
one-shot broadcast that isn't replayed after a restart.
The childless single-task conflict path was checked and needed no change --
it only broadcasts the generic TaskUpdated, never PlanningMergeConflict.
Stamps ClaudeDo.Worker's build with its exact git SHA (SourceRevisionId ->
InformationalVersion) and exposes it via a new GetWorkerBuildInfo hub call.
For the currently selected list, the shell compares that SHA against the
list's git HEAD (GitService.IsAncestorAsync) and shows a persistent footer
banner -- never auto-clearing, never shown on an unrelated repo or when the
ancestry can't be determined -- so "verified against a merge" claims aren't
silently made against a stale process. No auto-restart; the banner just
offers the existing RestartWorkerCommand.
Remove the redundant TaskUpdated broadcast in TaskRunner.ContinueAsync's
queue-claim path, consolidate InteractiveLaunchSpecService's seven
MCP_TOOL_TIMEOUT literals into one constant (fixing the merge-helper handoff
spec's stale 200000ms value), and surface OpenQuickClaudeSession's two
failure cases via ErrorReported/footer instead of a silent no-op, with a
less ambiguous icon.