DiffEditorSetup is the place for AvaloniaEdit host boilerplate now, so note why the diff viewer and the merge editor share it but stay separate controls, and that ModalShell owns the titlebar drag and the OffScreenMargin inset for every modal.
12 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 SessionTerminalView
Views/Controls/ — MarkdownView, ModalShell, ThemedDatePicker, DiffTextView, DiffEditorSetup,
InheritedBadge, AgentConfigEditor, UsagePill, UsageGaugeBar
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 (shown before the data loads via BeginLoad); gauges are dynamic per UsageSnapshotDto.Limits row, and the 5h/7d ones carry three draggable stage markers (soft/hard/gate) via UsageGaugeBar + the pure UsageThresholdDrag, plus a colour-matched legend with a NumericUpDown per stage → usage-monitoring. |
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) render through DiffAlignment (pure — pairs diff
lines into side-by-side rows and computes word-diff spans) and DiffTextView (AvaloniaEdit +
TextMate highlighting, unified/split layout, optional line wrap, synced scrolling). The
split/wrap toggles persist to ui.config.json via AppSettings.
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.
The two surfaces are not variants of one control (read-only 2-way renderer over aligned rows
vs. 3-way editor over a writable document), but they share their AvaloniaEdit host plumbing via
Views/Controls/DiffEditorSetup.cs: the process-wide TextMate Registry, InstallHighlighting,
ApplyGrammar, the Brush resource fallback, the Seg segment, and VerticalScrollSync. Put
new editor boilerplate there rather than copying it a third time.
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.
ModalShell also owns the window chrome for every modal: the titlebar drag goes through
Window.BeginMoveDrag (so Windows snap works — drag to the top edge to maximise; the OS only
snaps CanResize="True" windows, which is the opt-in), and it insets itself by the window's
OffScreenMargin so a maximised extended-client-area window isn't clipped at the edges.
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).