Skip to content

feat: conflict resolution helper view (gx)#848

Merged
gfargo merged 3 commits intomainfrom
feat/conflict-resolution-view
May 5, 2026
Merged

feat: conflict resolution helper view (gx)#848
gfargo merged 3 commits intomainfrom
feat/conflict-resolution-view

Conversation

@gfargo
Copy link
Copy Markdown
Owner

@gfargo gfargo commented May 5, 2026

What

Implements #780 — a dedicated promoted view (g x chord) that surfaces the in-progress operation's conflicted files and routes the user through resolving them.

Why

During a merge / rebase / cherry-pick conflict, the user currently has to navigate the noisy status view with conflicted files mixed in with everything else. This focused surface speeds up the resolve flow with per-file actions.

Implementation

New view: conflicts (accessed via g x)

Header: Shows operation type + conflict count (merge — 3 conflicts remaining)

File list: Each conflicted path with its index/worktree status codes and a human-readable label:

  • UU src/file.ts (both modified)
  • DD src/file.ts (both deleted)
  • DU src/file.ts (deleted by us)
  • UD src/file.ts (deleted by them)

Per-row keys:

Key Action
Enter Open the file's worktree diff
o Open in $EDITOR
s Stage (mark conflict resolved)
u Keep theirs (incoming changes)
U Keep ours (current branch)
C Continue operation (when all conflicts resolved)

States:

  • Loading state while operation data refreshes
  • Empty state when no operation is in progress
  • "All resolved" state with hint to continue the operation

Files changed

  • inkViewModel.ts'conflicts' view type, selectedConflictFileIndex state, moveConflictFile action
  • inkKeymap.tsnavigateConflicts command (gx), footer hints, chord continuation
  • inkInput.tsgx chord handler, up/down navigation, per-row key handlers, context fields
  • inkRuntime.tsrenderConflictsSurface, workflow handlers for resolve/continue
  • operationActions.tsresolveConflictOurs, resolveConflictTheirs, stageConflictResolved

Design decisions

  • Status codes shown as both raw codes (UU) and human-readable labels — follows the lazygit pattern of pairing letters with meaning (never color-alone signaling)
  • Cursor uses > indicator with bold/dim for selected/unselected — consistent with other promoted views
  • Windowed list with center-cursor scrolling — same pattern as status/branches/tags surfaces
  • C (continue) only fires when conflictFileCount === 0 — prevents accidental continue with unresolved conflicts
  • View auto-refreshes after each resolve action via the existing workflow runner's refreshContext

Testing

  • 24 new tests across 4 test files
  • All 5058 tests pass
  • Build compiles cleanly
  • Lint passes

Closes #780

gfargo added 2 commits May 5, 2026 11:09
…ine (#845)

First chunk of the #845 perf overhaul: a reproducible benchmark
harness so every later PR can show concrete before/after numbers
instead of hand-waving about "should be faster". Three pieces:

1. Telemetry persistence in `observability.ts`. When COCO_BENCH=1
   is set (or any non-`0` value), every llm call accumulates into
   a narrow `LlmBenchCall` buffer; `flushLlmBenchRun` writes the
   record to `<cwd>/.coco-bench.json` (overridable via
   COCO_BENCH_FILE). Best-effort: write failures are silent and
   the buffer self-clears after each flush.

2. Synthetic diff fixtures at
   `src/lib/parsers/default/__fixtures__/`. Three sizes:
     - tiny  ( 5 files,  ~790 tokens)  — early-exit path
     - medium (25 files, ~36k tokens)  — typical commit
     - large  (50 files, ~83k tokens)  — initial-commit shape
   Content comes from a seeded LCG so before/after runs compare
   the same input. Each fixture exports a fully-populated DiffNode
   tree so `summarizeDiffs` runs without a real git repo.

3. `bin/benchmark.ts` runner (`npm run bench`). Plugs the fixtures
   into `summarizeDiffs` with a duck-typed mock chain that simulates
   per-call latency proportional to input size (deterministic so
   PR diffs are apples-to-apples, not real-world wall-clock).
   Captures stage timings + per-call telemetry. `--update`
   overwrites `.bench/baseline.json`; `--fixture=<name>` narrows
   to a single fixture for tighter feedback loops.

Baseline numbers committed at `.bench/baseline.json` against
current `main`:

| fixture | wall-clock | llm calls | llm total ms | prompt tokens |
|---------|------------|-----------|--------------|---------------|
| tiny    |     2 ms   |     0     |     0 ms     |       0       |
| medium  | 30,213 ms  |    20     | 102,723 ms   |    91,766     |
| large   | 70,048 ms  |    41     | 236,818 ms   |   220,199     |

The 3.4× spread between large fixture's wall-clock and total LLM
time (236 s of model work in 70 s wall) reflects the existing
`maxConcurrent=6` parallelism. Subsequent PRs in the #845 sprint
will move these numbers and the deltas will land directly in PR
descriptions.
Implements #780 — a dedicated promoted view for resolving merge/rebase/
cherry-pick/revert conflicts.

New view (gx chord):
- Lists conflicted files with status codes (UU, DD, AU, etc.)
- Per-row keys: Enter (open diff), o (open in $EDITOR), s (stage/resolve),
  u (keep theirs), U (keep ours)
- Header shows operation type + conflict count
- Mounts only when an operation is in progress with conflicts
- Shows fallback message when no operation is active
- Surfaces C (continue operation) when all conflicts are resolved

Implementation:
- inkViewModel.ts: Added 'conflicts' to LogInkView, selectedConflictFileIndex
  state field, moveConflictFile action + reducer
- inkKeymap.ts: Added navigateConflicts command (gx), footer hints, chord
  continuation
- inkInput.ts: Added gx chord handler, ↑/↓ navigation, per-row key handlers
  (Enter/o/s/u/U/C), conflictFileCount/conflictSelectedPath context fields
- inkRuntime.ts: Added renderConflictsSurface with windowed file list,
  status labels, loading/empty states; workflow handlers for resolve-conflict-
  ours/theirs/stage/open-diff/continue-operation
- operationActions.ts: Added resolveConflictOurs, resolveConflictTheirs,
  stageConflictResolved git operations

Tests added for all new functionality across operationActions, inkViewModel,
inkInput, and inkKeymap test files.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a dedicated “conflicts” promoted view to the interactive coco log TUI (via g x) with per-file conflict-resolution actions, and also introduces an LLM/diff-condensing benchmark harness + telemetry persistence utilities.

Changes:

  • Introduces a new conflicts view with cursor state and navigation (selectedConflictFileIndex, moveConflictFile), keybindings (gx), and rendering (renderConflictsSurface).
  • Adds git operation helpers to resolve conflicts by keeping ours/theirs and staging (resolveConflictOurs, resolveConflictTheirs, stageConflictResolved) and wires them into Ink workflows.
  • Adds bench-mode LLM call capture + JSON flushing plus a synthetic-fixture benchmark runner and a committed baseline.

Reviewed changes

Copilot reviewed 13 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/lib/parsers/default/fixtures/index.ts Adds synthetic DiffNode fixtures for benchmarking the diff-condensing pipeline.
src/lib/langchain/utils/observability.ts Adds bench-mode LLM call recording and disk flush utilities.
src/commands/log/operationActions.ts Adds git conflict-resolution actions (ours/theirs/stage).
src/commands/log/operationActions.test.ts Adds unit tests for the new conflict-resolution actions.
src/commands/log/inkViewModel.ts Adds conflicts view type and conflict cursor state/action.
src/commands/log/inkViewModel.test.ts Adds tests for conflicts view navigation state.
src/commands/log/inkRuntime.ts Renders conflicts surface and wires new workflow actions (resolve/stage/continue).
src/commands/log/inkKeymap.ts Adds gx navigation + conflicts footer hints.
src/commands/log/inkKeymap.test.ts Tests conflicts hints and chord continuation registry.
src/commands/log/inkInput.ts Adds gx chord handling, conflicts navigation, and per-row conflict key handlers.
src/commands/log/inkInput.test.ts Adds interaction tests for conflicts view key behaviors.
package.json Adds bench script entry.
bin/benchmark.ts Adds benchmark runner for diff-condensing pipeline with baseline diffing.
.gitignore Ignores benchmark run outputs and .coco-bench.json sidecar.
.bench/baseline.json Adds committed benchmark baseline results.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/commands/log/inkRuntime.ts Outdated
Comment on lines +3338 to +3339
// If no operation is in progress or no conflicts, show a fallback message.
if (!loading && (operationType === 'none' || conflictedFiles.length === 0)) {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — split the early return into two separate branches: one for operationType === 'none' (no operation) and one for conflictedFiles.length === 0 with an active operation (all resolved). The view now keys visibility primarily off the conflict list presence, with operation type as display-only context in the header.

Comment thread src/commands/log/inkRuntime.ts Outdated
Comment on lines +3338 to +3356
// If no operation is in progress or no conflicts, show a fallback message.
if (!loading && (operationType === 'none' || conflictedFiles.length === 0)) {
return h(Box, {
borderColor: focusBorderColor(theme, focused),
borderStyle: theme.borderStyle,
flexDirection: 'column',
flexShrink: 0,
paddingX: 1,
width,
},
h(Box, { justifyContent: 'space-between' },
h(Text, { bold: true }, panelTitle('Conflicts', focused)),
h(Text, { dimColor: true }, 'no operation in progress')
),
h(Text, { key: 'conflicts-empty', dimColor: true },
operationType === 'none'
? 'No merge, rebase, cherry-pick, or revert in progress.'
: 'All conflicts resolved. Use the operation panel to continue.'
))
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the "all resolved" state now has its own dedicated early-return branch that renders the C continue hint properly. The dead hintLines block has been removed.

Comment on lines +1914 to +1928
'resolve-conflict-open-diff': async () => {
// Push the diff view for the conflicted file so the user can
// inspect conflict markers in context. We find the file's index
// in the worktree file list and navigate to its diff.
const path = payload?.trim()
if (!path) return { ok: false, message: 'No conflict file selected' }
const worktreeFiles = context.worktree?.files || []
const fileIndex = worktreeFiles.findIndex((f) => f.path === path)
if (fileIndex >= 0) {
dispatch({ type: 'navigateOpenDiffForWorktreeFile', fileIndex })
return { ok: true, message: `Viewing diff for ${path}` }
}
// Fallback: open in editor if not in worktree list
return { ok: true, message: `Opening ${path}` }
},
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the comment to accurately describe the behavior. The fallback now says not in worktree diff list rather than claiming to open the editor. The file may not be diffable (e.g. deleted-by-us conflicts) so a status message is the correct behavior here — the user can press o explicitly to open in $EDITOR.

Comment on lines +23 to +27
* `COCO_BENCH=1` (or a path) is set, then flushed to disk by
* `flushLlmBenchRun` at the end of the command. The structure stays
* narrow on purpose — fields the runner actually compares before /
* after, nothing more — so different runs with different model /
* provider mixes can still diff against the baseline cleanly.
Comment on lines +201 to +223
/**
* Persist the current bench run to a JSON file. No-op when bench
* mode is inactive (so production runs don't pay for disk I/O).
*
* The file path comes from `COCO_BENCH_FILE` if set, otherwise
* defaults to `<cwd>/.coco-bench.json`. Each call appends to the
* `runs` array of the file (creates the file if missing) so a single
* benchmark session that triggers multiple commands ends up with one
* file containing the full sequence.
*
* Best-effort: write failures are swallowed silently. The bench
* runner reports back the failure mode via the return value.
*/
export function flushLlmBenchRun(
options: {
command?: string
totalElapsedMs?: number
stages?: LlmBenchRunStage[]
} = {}
): { ok: boolean; filePath?: string; error?: string } {
if (!isBenchModeActive()) {
return { ok: false, error: 'COCO_BENCH not set' }
}
Comment thread src/commands/log/inkKeymap.ts Outdated

if (options.activeView === 'conflicts') {
return {
contextual: ['↑/↓ files', 'enter diff', 's stage', 'u theirs', 'U ours', 'o edit', 'C continue', 'esc back'],
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the hint to C continue* (with asterisk) to signal conditional availability. The asterisk convention is compact enough for the footer while hinting that the key has a precondition.

Comment on lines +1791 to +1793
// `C` continues the in-progress operation (available when no conflicts remain).
if (inputValue === 'C' && state.activeView === 'conflicts' && context.conflictFileCount === 0) {
return [{ type: 'runWorkflowAction', id: 'continue-operation' }]
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — C is now always intercepted on the conflicts view. When conflicts remain, it shows a status message ("Resolve all conflicts before continuing") instead of falling through to the global C (Create PR) binding.

Comment thread src/commands/log/inkRuntime.ts Outdated
: undefined,
worktreeDirty,
conflictFileCount: context.operation?.conflictedFiles.length,
conflictSelectedPath: context.operation?.conflictedFiles[state.selectedConflictFileIndex]?.path,
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — conflictSelectedPath now clamps the index against the live conflictedFiles.length before dereferencing. After a resolve shrinks the list, the clamped index ensures we always point at a valid file (or undefined when the list is empty), preventing fallthrough to unrelated global bindings.

- Split early return into separate 'no operation' and 'all resolved'
  branches so the C-continue hint is reachable (#r3191166195)
- Remove dead hintLines block that was unreachable after early return
- Clamp conflictSelectedPath index to prevent out-of-bounds access
  after resolving a conflict shrinks the list (#r3191166417)
- Always intercept C key on conflicts view to prevent fallthrough to
  global C (Create PR) binding when conflicts remain (#r3191166390)
- Update footer hint to 'C continue*' to signal conditional
  availability (#r3191166357)
- Fix resolve-conflict-open-diff fallback comment to match behavior
  (#r3191166243)
@gfargo gfargo merged commit d4362e6 into main May 5, 2026
9 checks passed
@gfargo gfargo deleted the feat/conflict-resolution-view branch May 5, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TUI shell · conflict resolution helper view

2 participants