Files
ClaudeDo/src/ClaudeDo.Ui/CLAUDE.md
T
mika kuns af0f318bf0 docs(ui): record the shared diff plumbing and the modal chrome rules
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.
2026-08-07 13:30:48 +02:00

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 (extends ObservableObject)
  • 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; FlashFooterError surfaces UI-action failures + the worker's Warn/Error there), responsive-layout flags (ShowLists/ShowDetails by window width), PrimeStatus flash, the modal openers, and RestartWorkerAsync/CheckForUpdatesAsync. Hosts UpdateCheckService.
  • 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. Raises NotesRequested/PrepRequested for the shell.
  • DetailsIslandViewModel — the detail pane for a bound TaskRowViewModel. Owns live-log streaming (Log via StreamLineFormatter), debounced title/description editing, subtasks, the session-outcome/roadblock split, the three-tab work console (output/git/session), child surfacing (ChildOutcomes + ChildrenNeedingAttention drive an attention band on the Session tab), attachments, and the modes IsNotesMode/IsPrepMode/computed IsTaskDetailVisible. Failures raise ErrorReported, wired by the shell into FlashFooterError.
    • 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 by CanReplyToRoadblock on LatestRunSessionId) resumes the session over the same ContinueTaskAsync transport as ContinueCommand, but with the user's own text.
    • Attachments write directly via new AttachmentStore() + new TaskAttachmentRepository(ctx); ComposedPreview includes attachment paths.
  • TaskRowViewModel / ListNavItemViewModel — lightweight display VMs. Task row: status, planning phase, parent/blocked links, roadblock count, computed IsDraft/IsPlanned/IsChild/IsPlanningParent/CanRefine, plus IsManual (→ MANUAL badge; suppresses CanSendToQueue/CanRefine/CanOpenPlanningSession) and HasInteractiveSession (→ 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 UsagePill in 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 tracks WorkerHub — treat src/ClaudeDo.Worker/Hub/WorkerHub.cs as the canonical method list rather than duplicating it here. Events mirror HubBroadcaster. Lifecycle (StartAsync/StopAsync) and a few admin methods live only on the concrete WorkerClient.
  • INotesApi / WorkerNotesApi — daily-note CRUD; UI DTO DailyNoteDto(Id, Date, Text, SortOrder).
  • IPrimeScheduleApi — prime-schedule CRUD.
  • UpdateCheckService — polls releases; LastCheckStatus/LatestVersion/CheckNowAsync feed 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

  • PathIcon fills its geometry. Line-art/stroke icons must be authored as filled geometry or rendered with a stroked Path (e.g. Icon.PlanDay via the Path.plan-icon style). A pure stroke path in a PathIcon is invisible.
  • NumericUpDown.Value is decimal? 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-nullable int/decimal, that null throws InvalidCastException. Either bind a decimal? property (as AgentConfigEditorViewModel.MaxTurns does) or add Converter={StaticResource KeepLastNumber}, which drops the null via BindingOperations.DoNothing.
  • Never bind bare punctuation gestures. Window key bindings live on MainWindow (Ctrl+K search, Ctrl+N add-task). OemQuestion once held search focus and silently swallowed # app-wide on a German layout.
  • FocusClearing's Escape handler is scoped to MainWindow (AddClassHandler<MainWindow>, not <TopLevel>) — it clears focus from a TextBox on Escape, mirroring click-outside. Modals are separate Window instances that bind their own Escape → close, so it never runs there. Mission Control's ConPTY tiles are in MissionControlWindow, 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).
  • SessionTerminalView is the reusable log terminal (StyledProperties Entries, Label, IsRunning, IsDone, IsFailed) — used for both the task Log and the prep PrepLog.
  • DetailsIslandView is a pane-wide drag-and-drop file target (DragDrop.AllowDrop, Avalonia 12 DataFormat.File) with a "Drop to attach" overlay; DescriptionStepsCard shows the attachments list, an "Add file…" picker, and an explicit DropStatus line. Keys use the details.attachments.* locale namespace (en + de).