Add a dedicated Usage Monitor & Gate section to Worker CLAUDE.md, wire UsagePillViewModel/UsageMonitorModalViewModel and the new IWorkerClient usage surface into Ui CLAUDE.md, and record open verification points for the pill/modal visual pass and the real gate E2E in docs/open.md. Data CLAUDE.md already covered the new entity columns and migration.
18 KiB
ClaudeDo.Ui
Avalonia UI layer: views, viewmodels, converters, and the SignalR client.
Pattern
MVVM with CommunityToolkit.Mvvm source generators:
[ObservableProperty]for bindable properties[RelayCommand]for commands (supports async and CanExecute)- All ViewModels inherit
ViewModelBase(extendsObservableObject) - All views use compiled bindings (
x:DataType)
Layout: Islands
MainWindow hosts three "islands" (lists | tasks | details). There is no MainWindowViewModel, StatusBarView, or task/list editor modal — the root coordinator is IslandsShellViewModel, and task/list editing happens inline in the islands.
ViewModels/
IslandsShellViewModel.cs — root coordinator
Islands/ — ListsIsland, TasksIsland, DetailsIsland, TaskRow, ListNavItem,
NotesEditor, MergePreviewPresenter
Agent/ — AgentConfigEditorViewModel (scope-parameterized: List | Task)
Modals/ — About, DiffViewer (+ DiffModels), ListSettings, Merge, MergeHelperSelection,
RepoImport, Settings (+ Settings/ tab VMs), UnfinishedPlanning, WeeklyReport,
WorkerConnection, WorktreesOverview, UnifiedDiffParser
Conflicts/ — ConflictResolverViewModel + ConflictModels (MergeFile/MergeFileSegment/MergeConflictBlock)
Views/ — mirrors the VM layout; Islands/Detail/ holds TaskHeaderBar,
DescriptionStepsCard, WorkConsole; plus AgentStripView, SessionTerminalView
Views/Controls/ — MarkdownView, ModalShell, ThemedDatePicker, DiffLinesView, InheritedBadge, AgentConfigEditor
Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyles.axaml
(component styles + the filled icon geometry library)
ViewModels
- 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 viaOpenLogVisualizerCommand;FlashFooterErrorsurfaces UI-action failures + the worker's Serilog Warn/Error there), responsive-layout flags (ShowLists/ShowDetailsby window width),PrimeStatusflash, and the modal openers (About, RepoImport, WeeklyReport, WorktreesOverview, WorkerConnection help, LogVisualizer) plusRestartWorkerAsync/CheckForUpdatesAsync. HostsUpdateCheckService. - 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) andSyncInteractiveSessions(mirrors Mission Control's open ConPTY panes onto the rows); MyDay extras (IsMyDayList,ClearDayCommand,ShowPrepLogCommand) and the pinned Notes pseudo-row (ShowNotesRow,OpenNotesCommand). RaisesNotesRequested/PrepRequestedevents consumed by the shell. - DetailsIslandViewModel — the detail pane for a bound
TaskRowViewModel. Owns live-log streaming (LogviaStreamLineFormatter), debounced title/description editing, subtasks, session-outcome/roadblock split (splitsResultat the roadblock marker into two cards) — the ROADBLOCK card also has a reply field (RoadblockReplyDraft/SendRoadblockReplyCommand, gated byCanReplyToRoadblockonLatestRunSessionId) that resumes the session via the sameContinueTaskAsynctransport asContinueCommandbut with the user's own text instead of the fixed re-run prompt; failures raiseErrorReported, wired by the shell intoFlashFooterError, the three-tab work console (output/git/session), child surfacing (ChildOutcomesrows plusChildrenNeedingAttention/HasChildrenNeedingAttention— children that failed, were cancelled, await review, or reported roadblocks — drive an attention band on the Session tab, which is only visible whenHasChildOutcomes), and the modes:IsNotesMode(hostsNotesEditorViewModel),IsPrepMode, computedIsTaskDetailVisible = !IsNotesMode && !IsPrepMode. Three concerns are extracted into section VMs exposed as properties: AgentConfigEditorViewModel (scope=Task; per-task Model/MaxTurns/AgentPath overrides withInheritedBadge+InheritanceResolver, additive SystemPrompt, debounced auto-save; exposed asAgentSettings), MergeSectionViewModel (merge-target selection, mergeability indicator viaMergePreviewPresenteroverPreviewMergeAsync,OpenDiffAsyncandReviewCombinedDiffCommand— both build aDiffViewerViewModel, callShowDiffViewer, and fire theDiffViewedcallback;HasReviewableDiffreports whether anything is inspectable, feeding the review gate), PrepPanelViewModel (daily-prep panel:PrepLog,PlanDayCommand→RunDailyPrepNowAsync, persisted last run viaGetLastPrepLogAsync). Attachments:Attachments(ObservableCollection<AttachmentRowViewModel>),IsDragOver,DropStatus,CanAcceptDrop,AddFilesAsync,RemoveAttachmentCommand; loads on task change;ComposedPreviewincludes attachment paths. Writes directly vianew 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; plusIsManual(→ MANUAL badge; suppressesCanSendToQueue/CanRefine/CanOpenPlanningSession) andHasInteractiveSession(→ 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. - UsagePillViewModel — one shared instance backs the
UsagePillcontrol hosted in both the footer and the Mission Control header; loads viaGetUsageSnapshotAsyncand updates live offIWorkerClient.UsageUpdatedEvent; derives display text, tooltip, and dot state (normal/warn/stale/blocked, mutually exclusive priority blocked > stale > warn > normal) from the sharedUsageSnapshotDto. - 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,VerifyCommand(optional post-merge verify gate, own field/section — not part ofAgentConfigEditorViewModel), delete list; hosts sharedAgentConfigEditorViewModelasAgentproperty (scope=List) — save delegates toAgent.SaveAsync(verifyCommand), since both fields land in the samelist_configrow via oneUpdateListConfigcall and would otherwise clobber each other),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 confirmListsIslandViewModelraisesLetClaudeHandleRequested→ shell →MissionControlViewModel.OpenMergeHelperConPtySessionAsync, which first callsIWorkerClient.CreateMergeHelperTaskAsyncto create one new ClaudeDo task per run in that list —Idle/IsManual=true(never queued), title/description localized (missionControl.mergeHelperTaskTitle/mergeHelperTaskDescriptionHeader),TaskEntity.HandlerBaseCommitstamped to the list repo's current HEAD — then opens a task-based ConPTY tile for it (deduped byTaskIdlikeOpenConPtySessionAsync, notCreateAdHoc) 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" stampsHandlerHeadCommitinstead of committing a worktree, and the detail pane'sMergeSectionViewModelfalls back toHandlerBaseCommit/HandlerHeadCommitover the list's working dir for its diff — seeTaskEntityinClaudeDo.Data/CLAUDE.md),WorkerConnectionModalViewModel(offline help),AboutModalViewModel,LogVisualizerViewModel(worker logs, last 30 min, all levels + a warn/error-only filter; loads viaGetRecentLogsAsync),UsageMonitorModalViewModel(opened from the usage pill; renders one gauge per row inUsageSnapshotDto.Limits— dynamic, since the fixedseven_day_opus/seven_day_sonnet-style buckets the raw Anthropic API can return are plan-dependent and come backnullon plans that don't have them, so a fixed gauge layout would break; also shows model usage (GetModelUsageAsync, ClaudeDo-vs-Other split per model) and top-task usage (GetTaskUsageAsync) tables over a 7d/30d preset or custom date range). - Diff stack —
UnifiedDiffParser(static; parsesgit diffoutput intoDiffFileViewModels, detecting added/deleted/renamed/binary files and per-line numbers;Flatteninjects file-header rows for a combined single-pane view).DiffModels.csholds shared types:DiffLineViewModel,DiffFileViewModel,DiffLineKind,DiffFileStatus,SubtaskDiffRow,DiffTreeNodeViewModel,DiffTree.DiffViewerViewModelis a single unified read-only diff viewer with two modes: Files (dirty worktree / branch-vs-base / commit-range — loads via GitService, shows a folder file-tree on the left + per-file diff pane on the right, Merge button for live branch source) and Planning (per-subtask diffs viaGetPlanningAggregateAsync, subtask list left + flat diff right, combined integration-branch toggle). The Merge button opens the merge form, which routes toConflictResolverViewModelon conflict.DiffLinesViewrenders per-file diff content with binary/empty placeholders. - Conflicts —
ConflictResolverViewModel(in-app Rider-style 3-pane merge editor for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflictMergeFileSegments via the worker'sGetMergeConflictDocuments; exposes the active file's three reconstructed documents —ActiveOursText/ActiveResultText/ActiveTheirsText(fromMergeFile.OursText/ResultText/TheirsText; Result seeds unresolved conflicts with Ours) — plusActiveFile/SelectFileCommand(multi-file switcher),Current/Next/Previous(focused-conflict nav), a per-active-filePositionTextreadout, per-blockAcceptOurs/Theirs/Both/Base+MergeFile.Compose, andCanContinuegated on every file resolved + no binary; writes each file viaWriteConflictResolution, continue/abort; planning mode viaOpenForPlanningAsync(parentId, subtaskId)loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort toContinuePlanningMerge/AbortPlanningMerge, so a unit-merge conflict re-opens the editor per subtask via thePlanningMergeConflictbroadcast). The view (Views/Conflicts/ConflictResolverView) shows the whole file in three AvaloniaEdit panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (themeStyleIncludeinApp.axaml); a code-behindIBackgroundRenderertints each conflict block (unresolved/resolved) across panes, anIReadOnlySectionProvider+TextAnchorregions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls toggle each side in/out of the result —›/‹add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; aFilesSummaryreadout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. A conflict overview ruler right of the Result pane (ConflictMap) maps every conflict in the file proportionally (click a tick to jump) — handy for long files. Conflict block tints live inTokens.axaml(Merge*TintBrush). The editor is reached from review Approve on conflict and from the Merge button in the Diff window (a conflictingMergeTaskhands off to the resolver viaRequestConflictResolution).
Services
- WorkerClient / IWorkerClient — SignalR client connecting to
http://127.0.0.1:47821/hub, auto-reconnect with exponential backoff. The surface tracksWorkerHub(seesrc/ClaudeDo.Worker/CLAUDE.mdfor the canonical method/event list); groups: task execution (RunNow/Cancel/Continue/Reset/SetTaskStatus), review (ApproveReviewAsync(taskId, targetBranch) -> MergeResultDto, reject-to-queue/idle, cancel review,PreviewMergeAsync -> MergePreviewDto), planning sessions (start/resume/discard/finalize, queue subtasks, pending draft count, refine), embedded ConPTY launch specs (GetInteractiveLaunchSpecAsync/GetAdHocLaunchSpecAsync), planning aggregate/integration-branch diffs, unit-merge continue/abort, single-task conflict resolving (start/get-conflict-documents/write-resolution/continue/abort), worktrees (overview, set state, force remove, cleanup, reset all), agents, app settings, lists/config, weekly report, daily notes, daily prep (RunDailyPrepNowAsync,ClearMyDayAsync,GetLastPrepLogAsync), prime schedules, recent worker logs (GetRecentLogsAsync), usage monitoring (GetUsageSnapshotAsync -> UsageSnapshotDto?,GetModelUsageAsync(from, to),GetTaskUsageAsync(from, to)). Events mirrorHubBroadcaster(task/worktree/list/run updates, prep events, planning-merge events, refine events, worker log,UsageUpdatedEvent). Lifecycle (StartAsync/StopAsync) and a few admin methods live only on the concreteWorkerClient. - INotesApi / WorkerNotesApi — daily-note CRUD (
ListAsync(day),AddAsync,UpdateAsync,DeleteAsync); UI DTODailyNoteDto(Id, Date, Text, SortOrder). - IPrimeScheduleApi — prime-schedule CRUD (
ListAsync,UpsertAsync,DeleteAsync). - UpdateCheckService — polls releases, exposes
LastCheckStatus/LatestVersion/CheckNowAsync(feeds the shell's update banner). - InheritanceResolver — resolves the task → list → global override chain to
(value, source)for the inherited badges. - RepoScanner, InstallArtifactLocator/InstallerLocator/WorkerLocator, ForegroundHelper (Win32 foreground before launching a terminal), FocusClearing.
Converters
StatusColorConverter (+ ConnectionColorConverter in the same file), WorkerLogLevelToBrushConverter, DotBrushConverter, EqStatusConverter, IconKeyConverter, CheckboxBorderConverter, StrikeIfTrueConverter, BoolToItalicConverter, BoolToDraftOpacityConverter, NotNullToBoolConverter, UpperCaseConverter, DateOnlyToDateTimeConverter.
Dialog Pattern
Modals use TaskCompletionSource results behind the reusable ModalShell control — the dialog sets the result on save/cancel, and the caller awaits the TCS.
Notes
- Context menus exist on both list rows and task rows; right-click selects before opening the menu
- "Run Now" CanExecute re-evaluates when worker connection state changes
- Icon gotcha:
PathIconfills geometry. Line-art/stroke icons must be defined as filled geometry or rendered as a strokedPath(e.g.Icon.PlanDayvia thePath.plan-iconstyle); a pure stroke path used withPathIconis invisible. - Window key bindings live on
MainWindow:Ctrl+Kfocuses search,Ctrl+Nthe add-task box. Do not bind bare punctuation gestures —OemQuestionused to hold search focus and silently swallowed#app-wide on a German layout. FocusClearingalso clears focus from a TextBox on Escape, mirroring its click-outside behavior — but the KeyDown handler is scoped toMainWindowspecifically (AddClassHandler<MainWindow>, not<TopLevel>). Modal windows (AboutModalViewetc.) each bind their ownWindow.KeyBindingsEscape → close; since modals are separateWindowinstances, this handler never runs there, so Escape still closes them unchanged. Mission Control's ConPTY tiles (InteractiveTerminalView) live inMissionControlWindow, also unaffected — Escape always reaches the PTY there.Ellipse.spinner(IslandStyles) is the shared indeterminate spinner: used for a starting ConPTY pane (InteractiveTerminalViewModel.IsStarting) and in place of the refine button whileTaskRowViewModel.IsRefining.ConPtyPaneViewModelresolves its own launch spec (ctor takes a descriptor factory; the host wires handlers and then callsStart()), so the Mission Control tile appears immediately with its spinner while the worker is still preparing the worktree. A failed launch keeps the tile with its inline error banner instead of never appearing.SessionTerminalViewis the reusable log terminal (StyledPropertiesEntries,Label,IsRunning,IsDone,IsFailed) used for both the taskLogand the prepPrepLog.DetailsIslandViewis a pane-wide drag-and-drop file target (DragDrop.AllowDrop, Avalonia 12DataFormat.File) with a "Drop to attach" hover overlay.DescriptionStepsCardshows an Attachments list (file name, size, remove button), an "Add file…" picker, and an explicitDropStatusconfirmation line. Keys use thedetails.attachments.*localization namespace (en + de).