Skip to content

Add Vortex merge-history dashlet (Unit I) - #20

Merged
TheValiantOne merged 2 commits into
mainfrom
worktree-agent-af59f3297a5b824b1
Aug 11, 2026
Merged

Add Vortex merge-history dashlet (Unit I)#20
TheValiantOne merged 2 commits into
mainfrom
worktree-agent-af59f3297a5b824b1

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

Summary

Unit I of the Vortex companion extension: a read-only dashboard tile (context.registerDashlet) listing every merge already recorded in MergeInventory.xml - relative path, which mod folder holds the merged result, and each source mod's recorded hash - gated on Witcher 3 being the active game.

  • New files: vortex-extension/src/mergeHistoryDashlet.ts (component + fetchMergeHistory/resolveWsmExePath/registerMergeHistoryDashlet), vortex-extension/src/mergeHistoryDashlet.test.ts (unit tests), vortex-extension/test/mergeHistory.integration.test.ts (real end-to-end test).
  • Minimal index.ts change: one import + one registerMergeHistoryDashlet(context); call (see "Registration timing" below for why it's placed where it is).
  • Test-stub addition: test/testUtils/vortexApiStub.ts now exports a trivial Dashlet stand-in, since mergeHistoryDashlet.ts imports it as a value.
  • Unrelated fix discovered along the way: test:integration now passes --no-file-parallelism to vitest (see "Incidental fix" below).

Data source: option (a), WsmMcpClient.listMerges()

Chose MCP's list_merges over direct MergeInventory.xml parsing, for two independent reasons found in the existing codebase rather than assumed:

  1. mcpClient.ts's own doc comment already names "a merge-history dashlet" as one of the intended per-workflow callers of WsmMcpClient.
  2. docs/vortex-extension-design.md section 5 explicitly recommends list_merges "for parity/simplicity" now that it exists, over this extension re-parsing the XML itself (which would duplicate WitcherScriptMerger.Core/Inventory/MergeInventory.cs's XmlSerializer schema outside the C# codebase).

The tradeoff: this spawns a short-lived WSM process per fetch instead of a plain file read. For a dashlet that only fetches on mount and manual "Refresh" clicks (not a polling timer), that's a handful of times per Vortex session, not a hot path - one WsmMcpClient per fetch, closed in a finally, per mcpClient.ts's documented lifecycle policy ("spawn per user-initiated workflow, tear down when the caller is done with it").

Registration timing: main(), not context.once

index.ts's own doc comment (written ahead of this unit, by Unit F) says later context.register* calls should go inside context.once(...). I deviated from that, and want to flag it explicitly since it contradicts existing guidance in the file.

Verified directly against node_modules/@nexusmods/vortex-api/lib/api.d.ts's IExtensionContext doc comment (~line 3578):

"once-callback. This is a callback that will be run after all extensions have been initialized and all register functions have been evaluated. [...] It should be used for all your extension setup except for the register calls (i.e. installing event handlers, doing startup calculations)."

So registerDashlet is called synchronously in main(). tryRegisterWsmTool's existing use of context.once is unaffected and remains correct - it dispatches a Redux action via api.store, which needs the store to exist, a genuine once()-shaped requirement, not a register call. Live gating still works the same way either way, via isVisible: () => isWitcher3Active(context.api) (registerDashlet's own live isVisible callback, re-evaluated by Vortex on every render check, independent of when the registration call itself happens).

I deliberately did not edit index.ts's existing large doc comment to correct this (it's shared with sibling units G/H/J's branches, and reflowing that paragraph risks a merge-conflict for no functional reason) - mergeHistoryDashlet.ts's own registerMergeHistoryDashlet doc comment carries the citation/reasoning instead, and index.ts has a short pointer comment at the actual call site.

Verification

  • npm run typecheck && npm run build && npm run lint && npm test - all pass (55 unit tests, including 10 new for mergeHistoryDashlet.ts).
  • npm run test:integration - all pass (60 tests across 12 files, including the new mergeHistory.integration.test.ts), against a real, compiled WitcherScriptMerger.Headless mcp process:
    • Writes a scratch MergeInventory.xml by hand (schema confirmed directly against MergeInventory.cs/Merge.cs/ModFile.cs/FileHash.cs, not guessed) with one recorded merge and two source mods.
    • Asserts a non-empty, field-matched result comes back through fetchMergeHistory - deliberately not just re-proving mcpClient.integration.test.ts's existing empty-list case, since MergeInventory.Load's bare catch { inventory = new MergeInventory(); } means a malformed fixture would also silently produce [], indistinguishable from "no merges" unless a test actually asserts non-empty content.
    • Also unit-tests (with an injectable connect seam, mirroring toolAcquisition.ts's own client/extractor seams) that close() is called even when listMerges() rejects - the "close in a finally" lifecycle contract specifically.

Not verified (honest gap): the dashlet actually mounting/rendering/updating on a real Vortex dashboard. vitest.config.ts has no jsdom, so nothing here renders MergeHistoryDashlet - only its data-fetch logic (fetchMergeHistory) and its registerDashlet call shape/gating are tested directly. This needs a manual check against a real Vortex install before shipping.

Incidental fix: test:integration build race

Adding this unit's third integration-test file (alongside mcpClient.integration.test.ts and toolAcquisition.integration.test.ts) made a latent race visible: vitest runs integration test files in parallel by default, and multiple simultaneous dotnet build/dotnet publish invocations across those files collide on the shared WitcherScriptMerger.Core build output (CSC : error CS2012: Cannot open '...WitcherScriptMerger.Core.dll' for writing [...] file may be locked by 'Microsoft Defender Antivirus Service'). Reproduced reliably with all three files present; disappears with vitest run test --no-file-parallelism. Fixed by adding that flag to the test:integration npm script - one-line change, package.json only.

What was NOT touched (per this unit's scope)

src/toolAcquisition.ts, src/discoveredTool.ts, src/storage.ts, src/wsmEnv.ts, src/githubRelease.ts, src/archiveExtractor.ts - read only, not modified. resolveWsmExePath's exe-path computation duplicates a two-line computation already present twice in toolAcquisition.ts; not factored into a shared helper since that would require touching toolAcquisition.ts/storage.ts beyond reading them, which is out of scope for this unit - flagged in a code comment for whichever later unit next touches those files.

AI-assisted: this PR was substantially produced by Claude Code, per this repo's CONTRIBUTING.md "AI-assisted development" section.

Chris Knight and others added 2 commits August 10, 2026 15:44
Adds a read-only dashboard tile listing merges already recorded in
MergeInventory.xml (relative path, merged-mod folder, per-source-mod
hashes), gated on Witcher 3 being the active game.

Data source: WsmMcpClient.listMerges() (mcpClient.ts already names a
merge-history dashlet as an intended caller, and the design doc's
section 5 recommends list_merges "for parity/simplicity" over a
direct XML re-parse) via a short-lived client per fetch, closed in a
finally per that file's documented lifecycle policy.

registerDashlet is called synchronously in main(), not inside
context.once - vortex-api's own IExtensionContext doc comment says
once() must not be used for register calls, contradicting a prior
assumption baked into index.ts's own comment; tryRegisterWsmTool's
existing use of once() is unaffected (it needs the store, which is a
legitimate once()-shaped requirement).

Also fixes a pre-existing integration-test build race
(test:integration now passes --no-file-parallelism to vitest) that
this unit's third integration test file made more likely to trigger:
parallel dotnet build/publish invocations across integration test
files were colliding on WitcherScriptMerger.Core's shared build
output.

AI-assisted: substantially produced with Claude Code per this repo's
CONTRIBUTING.md AI-assisted-development section.

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 I's merge-history dashlet (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's fakeContext needed the union of both branches' additions
(a registerDashlet no-op stub, and profileId-keyed profiles/onAsync
support) - combined into one signature/doc comment.

Verified after resolution: typecheck, build, lint, 99 unit tests, and
106 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 1151d81 into main Aug 11, 2026
1 check passed
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