diff --git a/TODO.md b/TODO.md
index 5f58303..89c6b7b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,3 +1,26 @@
+- [x] improve: conflict resolution view.
+ - two-pane mode:
+ - currently, we display the two panes as if it were a diff, where the left side will be displayed as if the code in question will be removed, and the left pane will display the code in question as if it will be added.
+ - this doesn't really make sense, since code on either side (either version) will be added/removed.
+ - i'm wondering if it would make sense to display in both panels both changes that would and changes that would add code, meaning:
+ - if some code would be removed, it would display in red with a minus
+ - if some code would be added, it would display in green with a plus
+ - this would be the case for both panes
+ - probably something similar for three-pane mode would make sense.
+- [ ] improve: code commenting format. example of comment in current format:
+
+ Comment 14 [unresolved] source/sema/resolve.hon (after) 22:9–22:19 @ working:a3b4159a340b3546e2108bc2dc0e7c302fc6d84b
+ [8]Builtin
+ can't we do `[_]Builtin` here instead for array size inference? if not, this is probably an oversight in the brolang compiler 🤔
+
+ - would probably be nice to instead do this though, if possible, i.e. wrapping the code snippet in question in a codeblock annotated with the right language, e.g.:
+
+ Comment 14 [unresolved] source/sema/resolve.hon (after) 22:9–22:19 @ working:a3b4159a340b3546e2108bc2dc0e7c302fc6d84b
+ ```hon
+ [8]Builtin
+ ```
+ can't we do `[_]Builtin` here instead for array size inference? if not, this is probably an oversight in the brolang compiler 🤔
+
- [ ] feature: diff view - GitHub code review integration after the local review UX is usable.
- publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes.
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
diff --git a/docs/development.md b/docs/development.md
index 378d1dc..7bcb1e1 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -117,9 +117,11 @@ Conflicted-revision regressions preserve unchanged symlinks, defer oversized uns
Resolver checks use isolated real jj repositories: read-only discovery, marker-free all-side/base projections preserving outside edits and EOF, initial seed undo, hunk choice/undo, retained resolved files, jj-authoritative status, checkout/external-write refusal, cancellation, nontext bytes/types/modes/deletion, and directory-child protection. Render checks cover aligned gaps/highlights, context folding, native Draft focus/search/selection, separate History, hidden/on-demand explorer, viewport retention and narrow layouts. Live PTY smoke exercises frozen marker-free alternatives, History/Draft switching, hunk choice and guarded save confirmation without touching user repositories.
+Base-relative resolver regressions cover red display-only removals and green native additions on both sides, deletion/replacement gaps, add/add without a base, native-coordinate round trips and EOF rows. The rendered workspace check exercises both split orientations and unified mode, native edits, hunk choice/undo, and both History layouts. Live PTY smoke checks actual red `-` and green `+` cells on both candidates, opens columns/base-below History, verifies browsing leaves conflict bytes untouched, and takes/saves an Alternative with exact output bytes and jj confirmation.
+
Conflict History layout checks load `resolve_history_layout` through the real Ruby configuration and render both left/base/right columns and left/right over base. They cover source contents, full-width base geometry, Tab/directional focus, synchronized wheel scrolling, read-only base protection, narrow/short fallbacks and preserved Draft text. Live PTY smoke opens both layouts, focuses/scrolls the base, returns to Resolution, takes a hunk and saves; jj confirms the checkout is no longer conflicted.
-Shared-workspace parity checks compare ordinary and resolver diff geometry/content under identical chrome, focus and source cursors. Cover native picker selection/remaps, Tree/List and dock/floating actions, excerpt initialization, folded search, focus transfer on wheel, layout controls, loading/History/pane-rebuild transitions, and preserved per-file viewport state. Live PTY smoke uses Space e, Down/Enter file selection and guarded saving through the same explorer path.
+Shared-workspace checks exercise ordinary and resolver diffs with native chrome, focus and source cursors. Cover native picker selection/remaps, Tree/List and dock/floating actions, excerpt initialization, folded search, focus transfer on wheel, layout controls, loading/History/pane-rebuild transitions, and preserved per-file viewport state. Live PTY smoke uses Space e, Down/Enter file selection and guarded saving through the same explorer path.
Repository gutter checks cover LF/CRLF normalization, additions/replacements/deletion anchors, staged/disk/unsaved Git text, unborn and missing named files, ignored/unsupported paths, read-only colocated/non-colocated jj queries, stale results and per-buffer isolation, plus rendered modes, colors, current-line precedence, wrap/scroll and narrow/split geometry. Live PTY smoke verifies all four modes, edit/undo refresh, external Git HEAD changes, and jj parent changes without editor restart.
diff --git a/docs/editing.md b/docs/editing.md
index 37f3843..24cf00c 100644
--- a/docs/editing.md
+++ b/docs/editing.md
@@ -573,6 +573,8 @@ Run **`:resolve`** in Normal mode for the current jj working copy. It opens a de
The resolver shows the editable **Resolution** on the left and a read-only **Other version** on the right, with aligned rows, line/character highlighting, context folds and synchronized scrolling. Both are projected from the current working buffer, including pre-existing unsaved edits: only conflict-marker regions are replaced by their selected alternatives. Already-merged text and edits outside those regions appear on both sides. The initial output is seeded from the first side as a normal undoable buffer change; opening never writes it to disk. References remain frozen while later edits change only the output.
+Both panes show their own changes **against the selected base**, not a left-to-right removal/addition comparison: removed base lines are red with `-`, and added candidate lines are green with `+`, on either side. Replacements show the removed base text followed by the candidate text; unchanged text stays neutral. Removed base lines are display-only, with no candidate line number, caret, or editable selection. With no base (an add/add conflict), both candidates are additions against empty text. The Resolution baseline uses the same frozen working-text projection as its alternatives, preserving already-merged text and pre-existing outside edits. `resolve.base` selects the baseline through History; returning to Resolution uses that selection. Hunk navigation and `resolve.use_source` still operate on differences between the actual Draft and Alternative.
+
Pane borders label the editable output **Resolution** and its frozen reference **Other version**. Paths and branch-comparison metadata are omitted from these titles; unsaved state belongs only to the output, and source identity appears with the reference. Explorer **C** marks files jj still considers conflicted, including while edited/checking or confirmation is unavailable; it clears only when jj confirms resolution.
| Command | Behavior |
@@ -588,6 +590,8 @@ Pane borders label the editable output **Resolution** and its frozen reference *
**History** is separate from editing: it compares the original inputs and their base, with all sides and bases selectable for multi-sided conflicts. `config.resolve_history_layout = "columns"` (default) shows left/base/right; `"base_below"` shows left/right above a full-width base. Both layouts retain aligned changes and synchronized vertical scrolling. Tab/Shift+Tab cycles in layout order; directional pane actions follow the displayed geometry. Narrow History shows the focused source, as does base-below History when too short to display both rows of panes. Draft text, undo, caret and viewport survive view switches. Resolution uses the ordinary diff workspace, including its configured explorer, split/unified/orientation controls, source-line navigation, excerpts, focus and scrolling. Read-only Alternatives and History reject edits and saves.
+Each outer History pane uses the same red-removal/green-addition presentation against the selected base. The base pane highlights original text changed by either input; addition-only rows leave a gap there. This applies to both History layouts.
+
Input indexes are one-based and include bases. Conflict-specific actions appear in the palette and through corresponding `config.core.resolve` methods. File navigation uses the existing picker/explorer actions: Space o opens floating search, Space e focuses the dock, Ctrl+t switches Tree/List, Ctrl+w closes it, and configured tree/selection bindings apply unchanged. Enter selects a relevant conflict file and focuses the editable Draft. Normal-mode Tab/Shift+Tab, native motions/search, context expansion and layout actions follow normal diff behavior. The complete seeded Draft can be saved even when intentional differences remain against the Alternative: those differences are not unresolved conflict markers. There is no automatic “accept both”.
**`:w` saves and checks jj**, asynchronously. Before writing, the resolver locks the working copy and validates the frozen checkout and exact on-disk file state. Checkout changes, external writes and unsafe filesystem ancestors refuse the write, including force-save. Successful writes trigger jj's normal snapshot operation, which may capture other on-disk changes. The list distinguishes unresolved, unsaved, checking, resolved and unconfirmed states. jj—not marker scanning or a manual checkbox—confirms resolution. A failed confirmation keeps saved contents and reports unconfirmed status. Resolved files remain in the session for review. Dirty drafts prevent destructive resolver/pane/tab closes.
diff --git a/src/diff.rs b/src/diff.rs
index 2662e51..142be2f 100644
--- a/src/diff.rs
+++ b/src/diff.rs
@@ -356,6 +356,9 @@ pub(crate) struct ColumnLine<'a> {
pub side: Focus,
pub source_row: usize,
pub numbers: [Option; 2],
+ /// Change polarity is independent of the native source/pane.
+ pub polarity: Focus,
+ pub native: bool,
pub changed: bool,
pub emphasize: bool,
pub newline: bool,
@@ -409,6 +412,46 @@ impl<'a> ColumnView<'a> {
DisplayRow::Line(index) => index,
};
let row = self.diff.rows.get(index)?;
+ let side = self.diff.display_focus(display).unwrap_or(self.info.focus);
+ if let Some(conflict) = &self.diff.conflict {
+ let Some(line) = &conflict.rows[index][usize::from(side == Focus::After)] else {
+ return Some(ColumnRow::Gap);
+ };
+ let source = if line.removed {
+ &conflict.base
+ } else if side == Focus::Before {
+ &self.file.before
+ } else {
+ &self.file.after
+ };
+ return Some(ColumnRow::Text(ColumnLine {
+ text: source.get(line.source).map(String::as_str).or(Some("")),
+ side,
+ source_row: line.source,
+ numbers: if line.removed {
+ [None; 2]
+ } else {
+ [row.before, row.after]
+ },
+ polarity: if line.removed {
+ Focus::Before
+ } else {
+ Focus::After
+ },
+ native: !line.removed,
+ changed: line.changed,
+ emphasize: true,
+ newline: line.source + 1 < source.len()
+ || if line.removed {
+ conflict.base.last().is_some_and(String::is_empty)
+ } else if side == Focus::Before {
+ self.file.before_eof
+ } else {
+ self.file.after_eof
+ },
+ changes: Cow::Borrowed(&line.changes),
+ }));
+ }
let side = if self.info.unified {
self.diff.display_focus(display).unwrap_or(self.info.focus)
} else {
@@ -436,6 +479,8 @@ impl<'a> ColumnView<'a> {
self.diff.display_source_row(Focus::Before, display),
self.diff.display_source_row(Focus::After, display),
],
+ polarity: side,
+ native: true,
changed: row.kind == RowKind::Changed
|| (before && row.kind == RowKind::Removed)
|| (!before && row.kind == RowKind::Added),
@@ -459,6 +504,35 @@ impl<'a> ColumnView<'a> {
}))
}
+ /// Wrapping includes display-only removals without computing character diffs.
+ /// Removed base rows occupy a disjoint cache-key space from native rows.
+ pub fn text_row(&self, display: usize) -> Option<(usize, &'a str)> {
+ let DisplayRow::Line(index) = *self.diff.display_rows.get(display)? else {
+ return None;
+ };
+ let side = self.info.focus;
+ if self
+ .diff
+ .display_focus(display)
+ .is_some_and(|focus| focus != side)
+ {
+ return None;
+ }
+ if let Some(conflict) = &self.diff.conflict {
+ let line = conflict.rows[index][usize::from(side == Focus::After)].as_ref()?;
+ if line.removed {
+ return Some((usize::MAX - line.source, &conflict.base[line.source]));
+ }
+ }
+ let source = self.diff.display_source_row(side, display)?;
+ let lines = if side == Focus::Before {
+ &self.file.before
+ } else {
+ &self.file.after
+ };
+ Some((source, lines.get(source).map_or("", String::as_str)))
+ }
+
/// Caret mapping stays on this view's native side, even in unified display.
pub fn native_row(&self, display: usize) -> Option {
self.diff.display_source_row(self.info.focus, display)
@@ -489,9 +563,105 @@ impl<'a> ColumnView<'a> {
}
}
+/// Base-relative candidate patches with display-only removed base lines.
+#[derive(Debug)]
+struct ConflictChanges {
+ base: Vec,
+ rows: Vec<[Option; 2]>,
+ base_rows: Vec