Commit Graph
215 Commits
Author SHA1 Message Date
Mika Kuns a614feb96d feat(ui): add operation-timing NDJSON sink for hub invokes and bulk DB paths
Two chokepoints per P0-2: WorkerClient.InvokeTimedAsync(<T>) wraps all 63
_hub.InvokeAsync call sites (plus TryInvokeAsync), and Stopwatch blocks
around the Island VMs' bulk DB load/write paths (list load, task list load,
reorders, batch delete, reconcile tick, count refresh). Single-row reads
stay untouched per the plan's rule of thumb. No display, no behavior change.
2026-08-12 08:31:35 +02:00
mika kuns fc9df7f9ac Merge task: UI polish: ListBox Padding scroll clip, 4s log rebuild, ClassifyItems side effects 2026-08-11 18:34:36 +02:00
mika kuns e55c84cda4 fix(ui): stop ClearCompletedAsync from mutating chain-display fields
ClassifyItems() layers ApplyChainGrouping on top of partitioning, which
writes ShowAsChainMember/ChainStep/ChainAfterLabel on every row.
ClearCompletedAsync only needed the completed-section count/rows, so it
got that mutation as an unwanted side effect of counting.

Split the pure overdue/open/completed partition into PartitionItems();
ClassifyItems() now composes it with the chain-grouping mutation for
Regroup's actual render path. ClearCompletedAsync uses PartitionItems
directly. Regroup's behavior is unchanged.
2026-08-11 18:32:59 +02:00
mika kuns 6c4fa7b1a5 chore(claude-do): Task-row visual fixes: chain step badge z-order + hide "#0"
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
2026-08-11 16:46:21 +02:00
mika kuns 79b35801ae fix(ui): stop the reconcile ticks from destroying user state
The 4s reconcile tick was added to three long-lived surfaces. On two of
them it reloads by rebuilding every row instance, which silently threw
away whatever the user had built up since the overlay opened; on the
third it healed a row's data but left it in the wrong section.

- WorktreesOverview: LoadAsync clears Rows, SelectedCount, ConflictRows
  and BatchProgress, so batch-merge ticks, the highlighted row and a
  finished batch's outcome badges were wiped every four seconds --
  assembling a multi-worktree selection was impossible. Carry that state
  across the reload, keyed by task id, and re-point SelectedRow at the
  fresh instance (or clear it when the worktree is gone).
- MergeHelperSelection: the remember/restore had no re-entrancy guard, so
  a second tick entering between the first one's reload and its restore
  snapshotted rows already back at IsTickedByDefault and wrote that
  default back, re-ticking what the user had unticked. One tick at a time,
  and hold the row instances instead of a value snapshot so a tick landed
  during the await survives.
- TasksIsland: the tick deliberately never called Regroup because Phase 2b
  owned Rows in parallel. 2b has landed, so a healed task that went Done
  stayed in the Open section under a stale count, and a healed depends-on
  link never pulled its dependent under the chain head. Regroup when a
  patch moved a grouping input, gated on a cheap key so an idle tick stays
  free.
2026-08-11 16:39:20 +02:00
mika kuns 18babe3dee feat(ui): compute dependency-chain step/grouping in TasksIslandViewModel
Slice 1 of dependency-chain display: TaskRowViewModel gains DependsOnTaskId
plus the extrinsic ShowAsChainMember/ChainStep/ChainAfterLabel contract for
Slice 2's rail/badge rendering. Regroup's ClassifyItems walks each row's
DependsOnTaskId chain per section, pulls dependents directly under their
head regardless of SortOrder, and falls back to a flat row + label when the
head isn't in the same section (mirrors the ParentInView precedent).
Planning children never join a chain group - parent indent wins - and only
ever carry the label.

No AXAML changes; that's Slice 2.
2026-08-11 14:38:16 +02:00
mika kuns 38af549a80 feat(ui,worker): surface task numbers in row/detail UI and worker log
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>".
2026-08-11 13:51:20 +02:00
mika kuns d451edd367 fix(data): normalize a list working dir's trailing separator on write
A list working_dir stored as "C:\Dev\Tests\StaplerTracking\" broke every consumer
that puts it on a Windows command line: argv rules read \" as an escaped quote, so
the token never closes. "Open in terminal" passed wt.exe a starting directory of
C:\Dev\Tests\StaplerTracking" and it failed with 0x8007010b; the same data had
already corrupted the ConPTY list handler's arg list in August.

Paths.TrimTrailingSeparator is now the single helper (replacing the copies in
InteractiveLaunchSpecService and ClaudeHelpLauncher) and ListRepository applies it
on Add/Update, which covers every writer: UI create, repo import, hub UpdateList,
and MCP CreateList/UpdateList. OpenInTerminal also switches to ArgumentList so its
quoting is correct regardless of what's stored.
2026-08-10 17:12:46 +02:00
mika kuns bbfe40242c fix(ui): re-emit the flat list's section headers on a language change
Phase 2b-1 moved the group headers from {loc:Tr} bindings in TasksIslandView
into HeaderRow.Label, resolved once in Regroup(). RefreshLocalizedText only
updated CompletedHeader and the task rows, so after switching the language the
three section headers kept the old one. Re-run Regroup() there.

Not unit-tested: the only seam is the global static Loc.Current, and mutating
it would bleed into test classes running in parallel.
2026-08-10 16:26:16 +02:00
mika kuns 2fe40f3d65 Merge claudedo/923d3b07183c4ca8bcd15aed12f9df39 2026-08-10 16:21:01 +02:00
mika kuns ec6c230822 refactor(ui): flatten TasksIsland's task list into one virtualized ListBox
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).
2026-08-10 16:18:55 +02:00
mika kuns 82881acc70 fix(ui): stop the reconcile tick on native close, index rows in the tick
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.
2026-08-10 16:05:37 +02:00
mika kuns 1cad264967 feat(ui): add reconcile tick to self-heal lost worker broadcasts
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.
2026-08-10 15:56:05 +02:00
mika kuns 66f7a8ae99 Merge branch 'claudedo/9f963a215bb34ca4a28050d4f39178d3' 2026-08-10 15:19:57 +02:00
mika kuns 333ce89eef Merge branch 'claudedo/16172942c1424a67a0499e01cefb7d60' 2026-08-10 15:08:51 +02:00
mika kuns f41a3e9083 Merge claudedo/1891227787ae4085a52c112bd1dcd75a 2026-08-10 15:05:53 +02:00
mika kuns d3155e6868 feat(queue): warn when the base branch has uncommitted changes
Queuing (update_task_status, batch_update_task_status) and run_task_now
now surface a non-blocking baseDirty warning (separate modified/untracked
counts) when the list's working dir has uncommitted changes at enqueue
time, since a new worktree forks from the commit tip and silently misses
them. BaseDirtyChecker caches per working dir for a few seconds so a
batch queue over many tasks in one list only shells out to git once. The
UI surfaces the same warning via the footer error strip on queue actions.
2026-08-10 14:30:33 +02:00
mika kuns a067a7bfff fix(mcp): report a Failed task's failureReason instead of a bare status
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.
2026-08-10 14:20:00 +02:00
mika kuns 359bb3a439 refactor(ui): clean up task-row drag visuals
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.
2026-08-10 13:58:40 +02:00
mika kuns 7f10d45898 perf(ui): push task-list filtering into SQL
LoadForListAsync loaded the entire tasks table (all lists, two Include
joins) then filtered to the selected list in C#, never using
idx_tasks_list_id/idx_tasks_status. ITaskListFilter now exposes
MatchExpression, an Expression<Func<TaskEntity,bool>> that EF Core can
translate to SQL; a new TaskListFilterBase compiles it on demand for the
existing in-memory Matches(). The primary match query now runs as
db.Tasks.Where(filter.MatchExpression), with two small follow-up queries
(scoped by parent-id IN-lists) for contextual planning-parent rows and
pulled-in children, followed by an explicit re-sort to restore the global
SortOrder/CreatedAt ordering Regroup() depends on.
2026-08-10 13:49:09 +02:00
mika kuns afe1b68f46 fix(merge): refuse a merge that would overwrite an untracked file in the target
Before merging (or staging a conflict resolution's git add -A), compare the
branch's newly-added paths against what's currently untracked in the target
working directory. A collision aborts with a new untracked_collision status
naming the path and size, surfaced through merge_task/review_task,
preview_merge/preview_merge_set (which merge-tree alone can't see), and the
UI merge paths via FlashFooterError/ShowErrorAsync instead of a silent
catch{}. git's own preflight already refuses this while the path stays
untracked at merge time; this closes the gap once a path becomes trackable
in between (e.g. an unrelated conflict resolution's git add -A) or across
the continue_merge TOCTOU window.
2026-08-10 12:15:41 +02:00
mika kuns 0568f003d9 feat(ui): open a list's findings folder 2026-08-10 10:45:51 +02:00
mika kuns 696abd0a7a feat(ui): toggle whether the findings folder is committed 2026-08-10 10:43:12 +02:00
mika kuns 549aee097b feat(ui): default to the Git tab for manual and interactive tasks
A hand-driven task produces no streamed agent output, so the Output tab
opened empty. Bind now selects git for manual/interactive rows and resets
to output for autonomous ones.
2026-08-07 14:01:26 +02:00
mika kuns ac099dd1a8 fix(ui): drop stale delta refreshes so the newest task state wins 2026-08-07 09:18:52 +02:00
mika kuns cba7d01c31 fix(ui): retry the task delta refresh instead of swallowing the error 2026-08-07 09:16:21 +02:00
mika kuns 30efbcbf2f Merge branch 'claudedo/a935c68eed6341bfb96f0bb82f13354e' 2026-08-06 14:43:57 +02:00
mika kuns 96723f4582 Merge claudedo/28d494e791e842fc966eb8691181e6da 2026-08-06 14:41:37 +02:00
mika kuns 27d50b81ff fix(ui): surface merge-drain cancel rejection in task-row quick cancel
TasksIslandViewModel.CancelReviewAsync swallowed the HubException the
worker raises when a task's unit merge is draining (79f90a9), unlike
the details-pane version fixed in the same commit. Mirror that fix:
report the rejection via ErrorReported instead of a bare catch.
2026-08-06 14:31:13 +02:00
mika kuns 166021049a fix(ui): close interactive-session gate bypasses in reset-and-retry and plan queueing
Reset & Retry discarded the branch and queued an autonomous run even while the
user had an interactive ConPTY pane open on the task, and finalizing a plan
queued every child unconditionally (the hub has no notion of a UI-hosted
session) — both bypassed the HasInteractiveSession gate added for
CanSendToQueue. CanResetAndRetry now checks it too, with a subscription on the
bound task so the command re-evaluates when the flag flips without Task
itself changing; SendToQueueAsync now blocks queuing the whole plan and
surfaces the affected child titles when any child has an open session.
2026-08-06 14:30:24 +02:00
mika kuns a78cc526a6 fix(ui): surface worker-offline failures when deleting a task
DeleteTaskAsync had no IsConnected guard, unlike every other worker-dependent
command in DetailsIslandViewModel, so an offline delete was a silent no-op.
WorkerClient.DeleteTaskAsync also only caught HubException, letting the
InvalidOperationException thrown by an inactive hub connection escape into
the unobserved command task and vanish.

Gate DeleteTaskCommand behind CanDeleteTask (Task != null && IsConnected),
re-evaluate it on connection-state changes, widen WorkerClient to catch the
connection-inactive case too, and wrap the ViewModel's call in try/catch as
a second line of defense against a race between the guard and the call.
2026-08-06 14:29:15 +02:00
mika kuns 8dbdfb3b89 Merge branch 'claudedo/a92b87e7748840ababcfc87423a8636c' 2026-08-06 13:49:30 +02:00
mika kuns bd83cac57d Merge claudedo/973ea49ec0a642ea9b1ddcff9b71b6fe 2026-08-06 13:47:35 +02:00
mika kuns 79f90a9a8e fix(worker,ui): block cancelling a task while its unit merge is draining
TaskStateService.CancelAsync allowed cancelling a WaitingForReview task
even while PlanningMergeOrchestrator was mid-drain on it: ApproveReview
awaits the whole multi-subtask merge synchronously, so a concurrent
CancelReview (UI or MCP) could flip the parent to Cancelled while the
orchestrator kept merging children onto the target branch, then
FinalizeParentDoneAsync would find the parent no longer WaitingForReview
and give up - leaving the merged diffs stranded with no rollback.

CancelAsync now rejects with a clear reason when HasActiveMerge(taskId)
is true. TaskStateService can't take PlanningMergeOrchestrator as a
direct constructor dependency (circular back to ITaskStateService), so
it takes a lazily-resolved Func<IActiveMergeState> instead, mirroring
the existing Func<ITaskStateService> cycle-break already used for
PlanningChainCoordinator.

UI polish: DetailsIslandViewModel.IsMergeDraining gates
CancelReviewCommand's CanExecute (same shape as
WorktreesOverviewModalViewModel.IsMerging), and the command's catch now
raises ErrorReported instead of swallowing the rejection silently.
2026-08-06 13:38:02 +02:00
mika kuns d84607f796 fix(ui): gate queueing on an open interactive ConPTY session
A task-based ConPTY session leaves the row Idle in the DB (sessions never
touch status), so nothing stopped the queue picker from claiming it too:
CanSendToQueue ignored HasInteractiveSession, and both TasksIslandViewModel.
SendToQueueAsync and MissionControlViewModel.EnqueueTaskAsync (drag-to-queue)
wrote Status=Queued straight via EF, bypassing TaskStateService entirely and
its manual/draft-child guards. That let an autonomous claude process spawn in
the same worktree a user was hand-editing in the ConPTY pane.

Add !HasInteractiveSession to CanSendToQueue, and route both UI enqueue paths
through IWorkerClient.SetTaskStatusAsync (worker hub -> TaskStateService.
EnqueueAsync) instead of raw EF writes. The interactive-session gate itself
stays in the UI: the worker has no notion of a UI-hosted ConPTY pane.
2026-08-06 13:29:44 +02:00
mika kuns 9e2a15e421 fix(worker,ui): route details-pane task delete through worker to advance blocked parents
Deleting a child from the details pane hard-deleted straight from the UI
process via TaskRepository, bypassing TaskStateService.TryAdvanceParentAsync
entirely. Deleting the last child of a WaitingForChildren parent that way
left it wedged forever. WorkerHub.DeleteTask now mirrors the MCP delete_task
tool (running-task guard, FK-friendly error, advance-parent call), and the
UI goes through it.

TryAdvanceParentAsync also short-circuited when zero children remained,
treating "no children left" as "nothing to evaluate" instead of "all done" -
removed the early return so an empty child list (vacuously) counts as all
terminal.
2026-08-06 13:25:49 +02:00
mika kuns abac5e5150 Merge claudedo/c97dcaafbb3542bda9c670e502ffffb6 2026-08-06 11:43:07 +02:00
mika kuns b54a133c16 fix(worker,ui): clean up three review leftovers from the list-handler run
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.
2026-08-06 11:31:03 +02:00
mika kuns 6980aae77e Merge claudedo/64fbe15dae7e4d81b8c1045baa7e3c87 2026-08-06 11:22:35 +02:00
mika kuns 75561930d7 chore(claude-do): RepoImportFolders auf den WorkerHub-Pfad umziehen (Ui schrei
## Befund (am Code verifiziert 2026-08-06)

`AppSettingsEntity.RepoImportFolders` (`src/ClaudeDo.Data/Models/AppSettingsEntity.cs:26`, JSON-Array als TEXT) ist **nicht** Teil des `AppSettingsDto` in `src/ClaudeDo.Worker/Hub/WorkerHub.cs:32-49` und wird weder von `GetAppSettings` (~:375-393) noch von `UpdateAppSettings` (~:396-424) transportiert.

Stattdessen liest und schreibt die Ui das Feld dire

ClaudeDo-Task: 64fbe15d-ae7e-4d81-b8c1-045baa7e3c87
2026-08-06 11:18:00 +02:00
mika kuns 46c664a03f fix(ui): restore ForegroundHelper.AllowAny call before wt.exe launch
OpenInTerminal lost its foreground-grant call, leaving ForegroundHelper
without a caller. Restore it ahead of the Process.Start (covers both the
wt.exe and cmd.exe fallback paths) instead of deleting the helper.
2026-08-06 11:09:25 +02:00
mika kuns f106c890b3 fix(ui): refresh the run session id live, close the stale handoff pane
A task selected before its run started kept LatestRunSessionId null, so the
roadblock reply box and Continue stayed dead until it was re-selected.

The handoff left the Phase 1-2 tile open so its last message could be read, but
its process is gone by then and the terminal renders empty -- a dead
placeholder. It is closed on handoff now.

Also drops the WARN flood from worktree cleanup (already-unregistered worktree
and already-deleted branch are normal outcomes, not failures) and replaces the
fixed sleep in UsageGate_TransitionLogging_FiresOncePerChange with the polling
helper that already sits three lines below it in the same file.
2026-08-06 10:20:54 +02:00
mika kuns 95eba1d5e7 feat(claude-do): Quick-Claude-Button in der Kopfleiste der Tasks-Insel: oeffn
## Ziel

Ein kleines Icon in der Kopfleiste der Tasks-Insel, das fuer die gerade ausgewaehlte Liste **sofort** eine interaktive ConPTY-Claude-Session im Arbeitsverzeichnis dieser Liste oeffnet — ohne Task, ohne Dialog, ohne Ordnerauswahl.

## Ausgangslage (verifiziert 2026-08-05)

Der komplette Unterbau existiert schon; das ist im Kern Verdrahtung:

- `MissionControlViewModel.OpenAdHocConPtySessio

ClaudeDo-Task: 5d8e6246af9444afa500fbb4546873e7
2026-08-05 20:18:22 +02:00
mika kuns 7d3d6d7b54 fix(worker): record real raw token usage per run, not the uncached remainder
task_runs.tokens_in only ever held the API's uncached "input_tokens" field
(off by a factor of ~400,000 on a resumed session), and tokens_out summed
only the last result event instead of the whole session. TaskRunner now
reads each run's cache-read/cache-write/input/output totals from the
session transcript via a new ITranscriptUsageReader.ReadSessionTotalsAsync,
storing the delta against prior runs on the same session so a --resume
doesn't double-count. New task_runs.cache_read_tokens/cache_write_tokens
columns; the Session tab now shows the raw total (what actually counts
against the 5h/7d usage limit) with a breakdown tooltip.
2026-08-05 15:44:20 +02:00
mika kuns 519ea5a8e0 Merge claudedo/8c1c213004574c4fad6beb75b84b70d7 2026-08-05 12:15:21 +02:00
mika kuns d2ca7fb500 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.
2026-08-05 11:49:33 +02:00
mika kuns c9ba1e2645 feat(worker): add post-merge verification gate for list merges
Per-list optional VerifyCommand (list_config.verify_command) runs via
VerifyCommandRunner in the list's working dir right after a successful
merge/continue-merge, before the task is allowed to reach Done. A
non-zero exit or timeout leaves the merge in place but keeps the task
out of Done and reports StatusVerifyFailed with an output excerpt
through MergeResultDto/review_task; no command configured behaves
exactly as before. Merges against the same repo are now serialized
per working dir so a running verify can't be interrupted by a second
merge landing mid-build. Adds the field to the List Settings modal
(en/de localized) and covers success/failure/timeout in
TaskMergeServiceTests + VerifyCommandRunnerTests.
2026-08-05 11:19:22 +02:00
mika kuns eee87f086c fix(ui): drop notification for the removed CanPickUpInTerminal property
Semantic merge collision between two tasks merged in the same run: the
"remove pick up in terminal" refactor deleted DetailsIslandViewModel's
CanPickUpInTerminal, while the status-refresh fix (written against the
pre-deletion base) still raised a change notification for it. Git merged
both cleanly; the build did not.

CanAcceptDrop is still notified - that binding remains.
2026-08-05 09:29:38 +02:00
mika kuns 9804ecefff Merge claudedo/181d4abf368046b99bd57d5e2a7ab97a 2026-08-05 09:27:33 +02:00
mika kuns 7d29ec0725 Merge branch 'claudedo/df0e925e1ff4419d8b7a45f8a184f6d0' 2026-08-05 09:26:46 +02:00