From ca4377e641fea26d505e2cd1750a28ef8e8d7958 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Fri, 19 Jun 2026 11:12:02 +0200 Subject: [PATCH] feat(merge): toggle add/remove per side, MAIN/INCOMING labels, files readout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Conflict accept is now a per-side toggle: > adds MAIN (ours), < adds INCOMING (theirs) in click order (first on top); clicking again removes that side, so each side is included at most once. Region content is rebuilt from the included set. - Drop the separate reset (x) control — toggling both off clears the region. - Relabel the panes/tooltips Ours/Theirs -> MAIN/INCOMING (merge target vs task). - Add a cross-file 'N of M files unresolved' readout (FilesSummary) so you can see how many more files still have conflicts. en/de updated; Ui 128 + Loc 16 green. --- src/ClaudeDo.Localization/locales/de.json | 11 +-- src/ClaudeDo.Localization/locales/en.json | 11 +-- src/ClaudeDo.Ui/CLAUDE.md | 2 +- .../Conflicts/ConflictResolverViewModel.cs | 18 ++++ .../Conflicts/ConflictResolverView.axaml | 8 +- .../Conflicts/ConflictResolverView.axaml.cs | 87 +++++++++---------- 6 files changed, 80 insertions(+), 57 deletions(-) diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index cfd818e..62a5176 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -400,15 +400,16 @@ "windowTitle": "Merge-Konflikte lösen", "modalTitle": "KONFLIKTE LÖSEN", "loading": "Konflikte werden geladen…", - "ours": "OURS · aktuell (Ziel-Branch)", + "ours": "MAIN · Ziel-Branch", "result": "ERGEBNIS", - "theirs": "THEIRS · eingehend (Task)", + "theirs": "INCOMING · Task-Branch", "binaryHint": "Binärdateien können hier nicht zusammengeführt werden — brich ab und löse sie in deinem Editor:", "prevConflict": "Vorheriger Konflikt (Umschalt+F8)", "nextConflict": "Nächster Konflikt (F8)", - "acceptOurs": "Ours hinzufügen (Reihenfolge = Stapel)", - "acceptTheirs": "Theirs hinzufügen (Reihenfolge = Stapel)", - "clearConflict": "Konflikt zurücksetzen (neu beginnen)", + "acceptOurs": "Main hinzufügen", + "acceptTheirs": "Incoming hinzufügen", + "removeOurs": "Main entfernen", + "removeTheirs": "Incoming entfernen", "continue": "Lösen & fortfahren", "abort": "Merge abbrechen" }, diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 6a86a0c..0edc90d 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -400,15 +400,16 @@ "windowTitle": "Resolve merge conflicts", "modalTitle": "RESOLVE CONFLICTS", "loading": "Loading conflicts…", - "ours": "OURS · current (merge target)", + "ours": "MAIN · merge target", "result": "RESULT", - "theirs": "THEIRS · incoming (task)", + "theirs": "INCOMING · task branch", "binaryHint": "Binary files can't be merged here — abort and resolve them in your editor:", "prevConflict": "Previous conflict (Shift+F8)", "nextConflict": "Next conflict (F8)", - "acceptOurs": "Add ours (stacks in click order)", - "acceptTheirs": "Add theirs (stacks in click order)", - "clearConflict": "Clear this conflict (start over)", + "acceptOurs": "Add main", + "acceptTheirs": "Add incoming", + "removeOurs": "Remove main", + "removeTheirs": "Remove incoming", "continue": "Resolve & continue", "abort": "Abort merge" }, diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md index ad45181..7058a90 100644 --- a/src/ClaudeDo.Ui/CLAUDE.md +++ b/src/ClaudeDo.Ui/CLAUDE.md @@ -41,7 +41,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle - **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`. - **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows), `ListSettingsModalViewModel` (name, working dir, commit type, per-list Model/SystemPrompt/AgentPath/MaxTurns with inherited-badge + reset, delete list), `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), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`. - **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModalViewModel` has two modes: live worktree (branch diff vs base, with a Merge action) and commit-range `base..head` (`FromCommitRange = true` — shows a merged task's diff after its worktree is gone; no merge action). The view renders a file list with binary/empty placeholders via `DiffLinesView`. -- **Planning/Conflicts** — `PlanningDiffViewModel` (per-subtask diffs via `GetPlanningAggregateAsync`, toggle to combined integration-branch diff, conflict warnings), `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/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — Ours (read-only) | editable Result | Theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar) and the between-pane gutter controls **append** sides in click order — `›` adds ours, `‹` adds theirs (first pick on top, the next below), `✕` clears — so a conflict can take ours, theirs, both, or neither; the three panes share a proportional synced vertical scroll. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). +- **Planning/Conflicts** — `PlanningDiffViewModel` (per-subtask diffs via `GetPlanningAggregateAsync`, toggle to combined integration-branch diff, conflict warnings), `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/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). 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 (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions 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; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). ## Services diff --git a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs index 732fcd4..fe6b3e3 100644 --- a/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Conflicts/ConflictResolverViewModel.cs @@ -88,6 +88,21 @@ public sealed partial class ConflictResolverViewModel : ObservableObject public IReadOnlyList BinaryFilePaths => Files.Where(f => f.IsBinary).Select(f => f.Path).ToList(); public bool HasBinaryFiles => Files.Any(f => f.IsBinary); + public bool HasMultipleFiles => Files.Count > 1; + + /// Cross-file progress shown in the editor: how many files still have unresolved + /// (or binary) conflicts, so you can see how many more need attention. + public string FilesSummary + { + get + { + var total = Files.Count; + if (total == 0) return ""; + var unresolved = Files.Count(f => !f.AllResolved); + return unresolved == 0 ? $"All {total} files resolved" : $"{unresolved} of {total} files unresolved"; + } + } + public string ContinueHint => HasBinaryFiles ? "Binary conflicts must be resolved externally — abort and resolve in your editor." : ""; @@ -171,6 +186,8 @@ public sealed partial class ConflictResolverViewModel : ObservableObject OnPropertyChanged(nameof(TotalConflicts)); OnPropertyChanged(nameof(BinaryFilePaths)); OnPropertyChanged(nameof(HasBinaryFiles)); + OnPropertyChanged(nameof(HasMultipleFiles)); + OnPropertyChanged(nameof(FilesSummary)); RecomputeCanContinue(); if (_flat.Count > 0) MoveTo(0); // also sets ActiveFile via MoveTo @@ -193,6 +210,7 @@ public sealed partial class ConflictResolverViewModel : ObservableObject OnPropertyChanged(nameof(ResolvedCount)); OnPropertyChanged(nameof(PositionText)); OnPropertyChanged(nameof(ActiveResultText)); + OnPropertyChanged(nameof(FilesSummary)); } } diff --git a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml index 7a023eb..6cc5302 100644 --- a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml +++ b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml @@ -104,7 +104,7 @@ -