Interactive Conflict Picker
A terminal UI built for resolving real conflicts in seconds.
Overview
aura merge --interactive drops you into the conflict picker — a keyboard-driven terminal UI designed around a single premise: resolving a conflict should feel like triaging an email, not editing a file. You see the three candidate versions (ancestor, ours, theirs) side by side, pick a resolution with one key, and move on. The picker handles scrolling, diffing, secret masking, and saving state. You handle the decision.
The picker is optional — every action is available as a scriptable CLI verb documented in conflict resolution. But for interactive work, especially on merges with more than two or three conflicts, it is an order of magnitude faster than opening the file in an editor and hunting through diff markers.
The picker is opinionated about one thing: resolution is a decision, not a text-editing task. Design accordingly.
How It Works
Layout
The picker renders three panes and a status bar:
+- src/auth.ts :: AuthService.login (1 of 3) -----------------+
| ANCESTOR | OURS (Alice, 2h) | THEIRS (Bob, 40m) |
| async login(u,p) { | async login(u,p) { | async login(u,p) { |
| return this.db | const h = bcrypt.h(p) | this.audit.track( |
| .verify(u, p); | return this.db | "login_attempt" |
| } | .verify(u, h); | ); |
| | } | return this.db |
| | | .verify(u, p); |
| | | } |
+--------------------------+--------------------------+---------------------+
| reason: modify/modify hint: disjoint statements — try semantic-union |
| [o]urs [t]heirs [b]oth [u]nion [e]dit [r]evert [d]efer [?]help |
+---------------------------------------------------------------------------+
On narrow terminals the picker stacks panes vertically. On wide terminals it adds a fourth pane showing the proposed merge preview — a live render of what semantic-union or the currently-hovered verb would produce.
Navigation
| Key | Action |
|--------------|--------|
| j / k | Scroll the focused pane down / up by line. |
| J / K | Jump by block (statement for code, entry for JSON/YAML/env). |
| tab | Cycle focus across panes (ancestor → ours → theirs → preview). |
| n / p | Next / previous conflict in the session. |
| g / G | Jump to first / last conflict. |
| f | Toggle full-screen on the focused pane. |
| / | Search within the focused pane. |
| : | Command prompt (:q to quit, :w to save progress). |
Resolution keys
| Key | Verb | Notes |
|-----|---------------|-------|
| o | keep-ours | Apply ours and advance. |
| t | take-theirs | Apply theirs and advance. |
| b | keep-both | Available when the node kind allows (overloads, sequence items). Greyed out otherwise. |
| u | semantic-union | Apply the semantic-union strategy to this single node. Shown only when the hint fires. |
| e | edit | Open $EDITOR on the merged preview; save to accept. |
| r | revert | Use the ancestor version. |
| d | defer | Leave merge markers in the file and advance. |
| s | skip | Advance without resolving (picker returns to it at the end). |
| U | undo | Undo the most recent resolution. Stack is unbounded within the session. |
| ? | help overlay | Key reference. |
Secret handling
When the current conflict touches a value Aura classifies as a secret (see .env merge), values in all three panes render masked:
DATABASE_URL=<redacted:postgres://…prod-a>
Press R (capital, deliberate) to reveal values for the current conflict. Revealed values stay visible only while the conflict is focused; moving to the next conflict re-masks automatically. Every reveal is logged to the session audit trail with a timestamp and the terminal identity.
Preview pane
The preview pane (visible on wide terminals, toggleable with v) shows what the currently-hovered verb would produce. Hover o and the preview mirrors ours; hover u and the preview renders the semantic-union output. When the verb is edit, the preview is an editable buffer; pressing enter from that state is equivalent to e.
For structured file types, the preview renders the reformatted merged output — exactly what Aura will write on disk — so there are no formatting surprises.
Session persistence
The picker writes its state to .aura/merge/<session-id>/picker.json after every resolution. If you close the terminal, ssh session drops, or a tmux pane dies, aura merge --interactive resumes where you left off. Partially-resolved sessions survive reboots.
Examples
A: Walking a three-conflict session
$ aura merge feature/login --interactive
3 conflicts detected. 11 files auto-merged.
[1/3] src/auth.ts :: AuthService.login modify/modify
hint: disjoint statements - try semantic-union
press u
[2/3] src/auth.ts :: AuthService.logout modify/delete
ours modified, theirs deleted
press o
[3/3] package.json :: /version modify/modify
ours: 1.5.0, theirs: 2.0.0
press t
all resolved. applying merge commit...
Three keystrokes. One merge.
B: Editing a resolution inline
On the login conflict, the union hint fires but you want to adjust the merged body. Press u to apply the union, then press e to open the result in $EDITOR. Save and close: the picker re-parses the edited buffer, confirms it is valid, and records the custom resolution.
If the edit produces a parse error, the picker refuses to accept it and returns you to the same conflict with the error summary in the status bar.
C: Undo
You pressed t on the version bump but meant o. Press U. The picker rewinds the resolution (and the underlying file) and returns you to the conflict. Resolutions are not committed to Git until the whole session finalizes, so undo is always safe.
D: Keep-both on overloads
[1/1] src/api.ts :: fetchUser insert/insert
ours: function fetchUser(id: number): Promise<User>
theirs: function fetchUser(email: string): Promise<User>
[b] keep-both (create overloads)
Both sides added a function with the same name but different signatures. b produces:
function fetchUser(id: number): Promise<User>;
function fetchUser(email: string): Promise<User>;
function fetchUser(key: number | string): Promise<User> { /* ... */ }
The picker generates the union signature stub and opens it in the edit buffer for you to fill in the implementation.
E: Deferring for external tooling
You use a visual merge tool for complex YAML. Press d to defer: Aura writes the file with <<<<<<< markers, exits the picker, and leaves the session in-progress. Resolve in your tool, save, then run aura merge continue. Aura re-parses the file, confirms the markers are gone and the result parses, and advances.
Edge Cases
Terminals without color. The picker detects capability via tput and falls back to symbolic diff markers (+, -, ~) when colors are unavailable. All keystrokes work identically.
Narrow terminals. Below 100 columns, panes stack vertically. Below 60 columns, the picker refuses to render and recommends the non-interactive CLI.
Very large conflicts. A conflict on a 5000-line generated file is unusable in a side-by-side view. The picker detects over-sized nodes, collapses them to a summary (+ 4200 lines, 18 imports changed), and offers e as the only reasonable resolution.
Paste-heavy resolutions. Pasting into the edit buffer works, but very large pastes can confuse terminals that buffer poorly. The picker warns and recommends $EDITOR for pastes over 1k lines.
Headless environments. CI, sandbox, or non-TTY shells cannot run the picker. aura merge --interactive in those environments falls through to a scripted mode that reads resolutions from .aura/merge/<session>/script.json if present, or exits with a non-zero status listing the unresolved conflicts.
Multi-user resolutions. In Mothership Mode, two teammates can open the picker against the same session. The picker polls for peer resolutions every two seconds; conflicts resolved by someone else disappear from your queue with a banner (resolved by bob@team).
Theming. The picker honors a small theme file at ~/.aura/theme.json — foreground, background, diff colors for ours/theirs/ancestor panes, status-bar accent. Defaults match the terminal's native palette so the picker looks native on both light and dark backgrounds.
Stateless mode. aura merge --interactive --stateless disables session persistence for ephemeral use — useful on shared CI runners or throwaway containers where the .aura/merge/ directory should not outlive the process.
Accessibility. The picker respects NO_COLOR, FORCE_COLOR, and standard screen-reader hooks where the terminal supports them. Focus changes are announced via ANSI title updates; resolution verbs have both single-key and spelled-out forms (:keep-ours, :take-theirs) for users who prefer typing over mnemonic chords.
Mouse mode. When the terminal supports xterm mouse events, clicking a pane focuses it and clicking a verb in the status bar applies it. The keyboard remains the primary interface; mouse is additive, not a replacement.
Scripting handoff. Hitting :script in the picker writes the currently-resolved and currently-pending conflicts to .aura/merge/<session>/script.json and exits. Running aura merge continue --script replays the script in a headless environment — useful for CI rehearsal of the same resolution pattern.
Editor crashes mid-edit. The picker saves the edit buffer to .aura/merge/<session>/edits/<conflict-id>.tmp on every keystroke. If the editor crashes, Aura recovers the buffer on the next launch.
See Also
- Conflict resolution — the model and CLI behind the picker
- Merge strategies
- How AST merge works
- .env merge — secret masking details
- Limitations and edge cases