feat: conflict resolution helper view (gx)#848
Conversation
…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.
There was a problem hiding this comment.
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
conflictsview 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.
| // If no operation is in progress or no conflicts, show a fallback message. | ||
| if (!loading && (operationType === 'none' || conflictedFiles.length === 0)) { |
There was a problem hiding this comment.
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.
| // 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.' | ||
| )) |
There was a problem hiding this comment.
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.
| '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}` } | ||
| }, |
There was a problem hiding this comment.
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.
| * `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. |
| /** | ||
| * 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' } | ||
| } |
|
|
||
| if (options.activeView === 'conflicts') { | ||
| return { | ||
| contextual: ['↑/↓ files', 'enter diff', 's stage', 'u theirs', 'U ours', 'o edit', 'C continue', 'esc back'], |
There was a problem hiding this comment.
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.
| // `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' }] |
There was a problem hiding this comment.
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.
| : undefined, | ||
| worktreeDirty, | ||
| conflictFileCount: context.operation?.conflictedFiles.length, | ||
| conflictSelectedPath: context.operation?.conflictedFiles[state.selectedConflictFileIndex]?.path, |
There was a problem hiding this comment.
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)
What
Implements #780 — a dedicated promoted view (
g xchord) 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 viag 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:
EnterosuUCStates:
Files changed
inkViewModel.ts—'conflicts'view type,selectedConflictFileIndexstate,moveConflictFileactioninkKeymap.ts—navigateConflictscommand (gx), footer hints, chord continuationinkInput.ts—gxchord handler, up/down navigation, per-row key handlers, context fieldsinkRuntime.ts—renderConflictsSurface, workflow handlers for resolve/continueoperationActions.ts—resolveConflictOurs,resolveConflictTheirs,stageConflictResolvedDesign decisions
UU) and human-readable labels — follows the lazygit pattern of pairing letters with meaning (never color-alone signaling)>indicator with bold/dim for selected/unselected — consistent with other promoted viewsC(continue) only fires whenconflictFileCount === 0— prevents accidental continue with unresolved conflictsrefreshContextTesting
Closes #780