10 KiB
ClaudeDo.Ui
Avalonia UI layer: views, viewmodels, converters, and the SignalR client.
Deeper detail: review-merge (diff stack + conflict resolver) · conpty-sessions (Mission Control tiles) · usage-monitoring (usage pill + modal).
Pattern
MVVM with CommunityToolkit.Mvvm source generators:
[ObservableProperty]for bindable properties,[RelayCommand]for commands- 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
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)
Core ViewModels
- IslandsShellViewModel — root coordinator. Owns the three island VMs and the
WorkerClient, wires cross-island events (selection, notes/prep mode, conflict resolution), connection state, the update banner, the inline worker-log strip (clickable → Log Visualizer overlay;FlashFooterErrorsurfaces UI-action failures + the worker's Warn/Error there), responsive-layout flags (ShowLists/ShowDetailsby window width),PrimeStatusflash, the modal openers, andRestartWorkerAsync/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; planning session lifecycle;
RefineTask,OpenConPtySessionRequested,ToggleManual,SyncInteractiveSessions; MyDay extras (IsMyDayList,ClearDayCommand,ShowPrepLogCommand) and the pinned Notes pseudo-row. RaisesNotesRequested/PrepRequestedfor the shell. - DetailsIslandViewModel — the detail pane for a bound
TaskRowViewModel. Owns live-log streaming (LogviaStreamLineFormatter), debounced title/description editing, subtasks, the session-outcome/roadblock split, the three-tab work console (output/git/session), child surfacing (ChildOutcomes+ChildrenNeedingAttentiondrive an attention band on the Session tab), attachments, and the modesIsNotesMode/IsPrepMode/computedIsTaskDetailVisible. Failures raiseErrorReported, wired by the shell intoFlashFooterError.- Three concerns are extracted into section VMs exposed as properties:
AgentSettings(AgentConfigEditorViewModel, scope=Task),MergeSectionViewModel,PrepPanelViewModel. Helper rows live in the same file. - The ROADBLOCK card's reply field (
RoadblockReplyDraft/SendRoadblockReplyCommand, gated byCanReplyToRoadblockonLatestRunSessionId) resumes the session over the sameContinueTaskAsynctransport asContinueCommand, but with the user's own text. - Attachments write directly via
new AttachmentStore()+new TaskAttachmentRepository(ctx);ComposedPreviewincludes attachment paths.
- Three concerns are extracted into section VMs exposed as properties:
- 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 jumps to that Mission Control pane). List row: kind Smart/Virtual/User, count, icon/dot keys, drop hints,IsManual. - NotesEditorViewModel — day navigator + bullet CRUD via
INotesApi. - UsagePillViewModel — one shared instance backs the
UsagePillin both the footer and the Mission Control header → usage-monitoring.
Modal VMs
| VM | Notes |
|---|---|
SettingsModalViewModel |
Four tabs: General, Worktrees, Files (prompt paths), Prime Claude. General hosts the per-model preset table (ModelPresetRowViewModel: effort + max turns per alias) which replaced the single global "Max turns" field. |
ListSettingsModalViewModel |
Name, working dir, commit type, "manual list" flag, VerifyCommand, delete. Hosts the shared AgentConfigEditorViewModel as Agent (scope=List) — ⚠️ save delegates to Agent.SaveAsync(verifyCommand) because both land in the same list_config row via one UpdateListConfig call and would otherwise clobber each other. |
WeeklyReportModalViewModel |
Range pickers default "since last standup weekday → today", cached per range. |
MergeHelperSelectionModalViewModel |
"Let Claude handle it" picker → conpty-sessions. |
UsageMonitorModalViewModel |
Opened from the usage pill; gauges are dynamic per UsageSnapshotDto.Limits row. |
Self-explanatory: RepoImportModalViewModel (bulk-create lists from git repos; already-wired
repos disabled), MergeModalViewModel, WorktreesOverviewModalViewModel,
UnfinishedPlanningModalViewModel, LogVisualizerViewModel (last 30 min, all levels + a
warn/error filter), WorkerConnectionModalViewModel, AboutModalViewModel.
Diff & Conflicts
UnifiedDiffParser (static) + DiffModels.cs shared types + DiffViewerViewModel (one unified
read-only viewer, Files and Planning modes) + DiffLinesView.
ConflictResolverViewModel is an in-app Rider-style 3-pane AvaloniaEdit merge editor for both
single-task and planning unit-merge conflicts. Full detail →
review-merge.
Services
- WorkerClient / IWorkerClient — SignalR client on
http://127.0.0.1:47821/hub, auto-reconnect with exponential backoff. The surface tracksWorkerHub— treatsrc/ClaudeDo.Worker/Hub/WorkerHub.csas the canonical method list rather than duplicating it here. Events mirrorHubBroadcaster. Lifecycle (StartAsync/StopAsync) and a few admin methods live only on the concreteWorkerClient. - INotesApi / WorkerNotesApi — daily-note CRUD; UI DTO
DailyNoteDto(Id, Date, Text, SortOrder). - IPrimeScheduleApi — prime-schedule CRUD.
- UpdateCheckService — polls releases;
LastCheckStatus/LatestVersion/CheckNowAsyncfeed 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
In Converters/ — grep rather than list: log-level brush, dot brush,
status equality, icon key, strike/italic/opacity toggles, null→bool,
uppercase.
Dialog Pattern
Modals use TaskCompletionSource results behind the reusable ModalShell control — the dialog
sets the result on save/cancel, the caller awaits the TCS.
Gotchas
PathIconfills its geometry. Line-art/stroke icons must be authored as filled geometry or rendered with a strokedPath(e.g.Icon.PlanDayvia thePath.plan-iconstyle). A pure stroke path in aPathIconis invisible.NumericUpDown.Valueisdecimal?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-nullableint/decimal, that null throwsInvalidCastException. Either bind adecimal?property (asAgentConfigEditorViewModel.MaxTurnsdoes) or addConverter={StaticResource KeepLastNumber}, which drops the null viaBindingOperations.DoNothing.- Never bind bare punctuation gestures. Window key bindings live on
MainWindow(Ctrl+Ksearch,Ctrl+Nadd-task).OemQuestiononce held search focus and silently swallowed#app-wide on a German layout. FocusClearing's Escape handler is scoped toMainWindow(AddClassHandler<MainWindow>, not<TopLevel>) — it clears focus from a TextBox on Escape, mirroring click-outside. Modals are separateWindowinstances that bind their own Escape → close, so it never runs there. Mission Control's ConPTY tiles are inMissionControlWindow, 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.
- Context menus exist on both list and task rows; right-click selects before opening the menu.
- "Run Now" CanExecute re-evaluates when worker connection state changes.
Ellipse.spinner(IslandStyles) is the shared indeterminate spinner (starting ConPTY pane, refining task row).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" overlay;DescriptionStepsCardshows the attachments list, an "Add file…" picker, and an explicitDropStatusline. Keys use thedetails.attachments.*locale namespace (en + de).