Skip to content

Add "Resolve Script Conflicts" action + merge panel to the Vortex extension - #21

Merged
TheValiantOne merged 3 commits into
mainfrom
feature/vortex-resolve-conflicts-action
Aug 11, 2026
Merged

Add "Resolve Script Conflicts" action + merge panel to the Vortex extension#21
TheValiantOne merged 3 commits into
mainfrom
feature/vortex-resolve-conflicts-action

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

Summary

This is Unit H of the Vortex-extension work: the user-facing "Resolve Script Conflicts" action and its preview/confirm panel, driving WSM's MCP server via WsmMcpClient (Unit E) and the tool acquired by Unit F.

  • Part 1 — src/mcpClient.ts: MergeConflictsResult was stale versus the real merge_conflicts MCP tool (WitcherScriptMerger.Core/Mcp/WsmMcpTools.cs), which now also returns functionLevelDecisions (verified directly against that file and FileMerger.HeadlessMergeSummary.FunctionLevelDecisions, a List<string>). Added functionLevelDecisions: string[] to the interface — no other parsing changes needed, since callTool already does a generic JSON parse into the typed interface.
  • Part 2 — the action + panel: two new files, src/resolveAction.ts and src/mergePanel.ts, wired into src/index.ts with one additive registerResolveScriptConflictsAction(context) call.
    • context.registerAction('mod-icons', ...) — the Mods page toolbar group, confirmed against real, current Vortex source (gh search code 'registerAction("mod-icons"' --repo Nexus-Mods/Vortex turns up exactly this group for global toolbar buttons like Deploy/Purge and open-directory's "Open Mod Staging Folder") rather than guessed.
    • Clicking it spawns a WsmMcpClient, previews via mergeConflicts({dryRun: true}), and shows a Markdown dialog (built by mergePanel.ts) with merged/skipped counts and — surfaced prominently, right after the counts, not buried — the functionLevelDecisions audit trail when non-empty.
    • On confirmation ("Merge Now"), a second, freshly-spawned client runs the real merge (mergeConflicts({dryRun: false})) and shows the result. Two separate client instances, not one held open across the user's read-the-dialog wait — matches mcpClient.ts's own "spawn per user-initiated workflow, tear down when done" lifecycle policy.
    • No separate "launch the GUI" fallback for skipped files. Per this unit's own task description, checked directly against DiffPlexMergeEngine.MergeHeadless: a real (non-dry-run) merge_conflicts call already writes a git/diff3-style conflict-marker sidecar under DiffPlexConflicts/ and opens it in the OS's default editor as a side effect. The panel's messaging for skipped files just says so, rather than building a redundant launch mechanism (which would also need its own binary-acquisition step, since Unit F only downloads the GUI-less Headless build).

Deliberate v1 scope choices (noted per the task description)

  • "Merge everything" onlymergeConflicts is called with no relativePaths/orderOverrides, i.e. no per-file selection/reordering UI. Explicitly optional for v1.
  • No custom React component (MergePanel.tsx). This project's TypeScript toolchain has no JSX support wired up (tsconfig.json sets no jsx option, no @types/react dependency, react itself present only indirectly). Standing up a full React/JSX toolchain was bigger, separate scope from this unit's actual job. Instead, mergePanel.ts builds plain vortex-api IDialogContent (api.showDialog), whose md field renders Markdown natively — sufficient for a headed, bulleted audit trail. If a later unit needs real interactive controls (e.g. per-file checkboxes), that's the point to revisit this.

Code review findings fixed

An internal multi-angle code review caught three real issues in mergePanel.ts, all fixed:

  1. functionLevelDecisions was read unguarded; an older, already-acquired WSM binary predating this field would throw a TypeError escaping resolveScriptConflicts's try/catch entirely, making the action silently do nothing. Fixed with a defensive ?? [] default, plus an outer safety-net try/catch in resolveAction.ts for any other unexpected failure in dialog-building/showing.
  2. File paths were escaped for Markdown and wrapped in backtick code spans — but per CommonMark, backslash escapes don't work inside code spans, so this rendered literal backslashes (e.g. mod0000\_MergedFiles\a\_b.ws) instead of the clean path. Fixed by not escaping backtick-wrapped paths (code spans already suppress Markdown interpretation); the test that had baked in the wrong expected output was corrected.
  3. Minor duplication in the heading+list-building code, consolidated into one pathListSection helper with an optional intro-text parameter.

Test plan

  • npm run typecheck — clean
  • npm run build — clean
  • npm run lint — clean
  • npm test (fast unit tests) — 66 passed, including new src/mergePanel.test.ts (12 tests, dialog-content building) and src/resolveAction.test.ts (8 tests, full preview/confirm/error orchestration against fake clients)
  • npm run test:integration — 72 passed, including two new real, no-mock tests in test/mcpClient.integration.test.ts that build/spawn the real WitcherScriptMerger.Headless.exe against a scratch mods folder with genuine conflicting .ws/.xml content: one pair (.ws, disjoint-line edits) that cleanly auto-solves, one pair (.xml, same-line edit, deliberately not .ws so the function-level fallback can never rescue it) that stays genuinely skipped. Confirms real dry-run/real-run merged/skipped/functionLevelDecisions data is sensible, that a dry run writes nothing, and that a real run writes the merged file and a DiffPlexConflicts sidecar.
    • Known side effect: the real-run integration test triggers WSM's own documented behavior of opening the conflict-marker sidecar in the OS's default associated program (FileOpener.Open) — a window may briefly appear during this test. This is genuine, intentional WSM behavior being exercised end-to-end, not a test bug; the afterAll cleanup is best-effort (matches the codebase's own convention for a lock the test doesn't control) in case that program is still holding the file.
  • Manual/unverified: real panel UX/rendering inside an actual Vortex session, and whether the conflict-marker sidecar really pops open an editor in a real Vortex desktop environment — no real Vortex+Witcher3 install available in this environment. The integration test above is the closest available proof (the real WSM process, the real FileOpener.Open call, just not inside Vortex itself).

AI-assisted development

This PR was substantially produced by Claude Code (Claude Sonnet 5), per this repo's CONTRIBUTING.md disclosure requirement. I reviewed the diff, ran an internal code-review pass that caught and fixed three real bugs (see above), and verified the test plan above directly.

Chris Knight and others added 3 commits August 10, 2026 16:00
…xtension.

Fix the stale MergeConflictsResult interface in mcpClient.ts (missing
functionLevelDecisions, the function-level merge fallback's audit trail) and add
a new registerAction entry that previews a merge via mergeConflicts({dryRun:
true}), shows merged/skipped counts and the function-level decisions prominently
in a Markdown dialog, and runs the real merge on confirmation via a second,
freshly-spawned WsmMcpClient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Both units add their own registration to index.ts's main(): Unit G's
did-deploy conflict-scan handler (registered inside context.once, an
event listener) and Unit H's "Resolve Script Conflicts" action
(registered directly in main(), per Vortex's own documented contract
that register calls must not be deferred through context.once).
Combined both, updating the shared header comment to describe all three
registrations accurately.

index.test.ts needed the union of both branches' test additions: Unit
G's "did-deploy conflict scanning" describe block plus Unit H's action-
registration test, and fakeContext's combined signature (registerAction
mock, profileId-keyed profiles, onAsync support).

Verified after resolution: typecheck, build, lint, 110 unit tests, and
118 total tests including integration all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
Unit I's merge-history dashlet merged to main since this branch's last
resolution. Both it and Unit H's "Resolve Script Conflicts" action
register directly in main() (per the same register-outside-context.once
contract both units independently discovered) - combined the imports,
header comment (now describing dashlet as the third registration and
the resolve action as the fourth), and both context.register* calls.

index.test.ts's fakeContext needed both registerAction and
registerDashlet mock support together.

Verified after resolution: typecheck, build, lint, 120 unit tests, and
129 total tests including integration all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
@TheValiantOne
TheValiantOne merged commit 84b262a into main Aug 11, 2026
1 check passed
@TheValiantOne
TheValiantOne deleted the feature/vortex-resolve-conflicts-action branch August 11, 2026 00:30
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.

1 participant