Add Vortex merge-history dashlet (Unit I) - #20
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unit I of the Vortex companion extension: a read-only dashboard tile (
context.registerDashlet) listing every merge already recorded inMergeInventory.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.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).index.tschange: one import + oneregisterMergeHistoryDashlet(context);call (see "Registration timing" below for why it's placed where it is).test/testUtils/vortexApiStub.tsnow exports a trivialDashletstand-in, sincemergeHistoryDashlet.tsimports it as a value.test:integrationnow passes--no-file-parallelismto vitest (see "Incidental fix" below).Data source: option (a),
WsmMcpClient.listMerges()Chose MCP's
list_mergesover directMergeInventory.xmlparsing, for two independent reasons found in the existing codebase rather than assumed:mcpClient.ts's own doc comment already names "a merge-history dashlet" as one of the intended per-workflow callers ofWsmMcpClient.docs/vortex-extension-design.mdsection 5 explicitly recommendslist_merges"for parity/simplicity" now that it exists, over this extension re-parsing the XML itself (which would duplicateWitcherScriptMerger.Core/Inventory/MergeInventory.cs'sXmlSerializerschema 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
WsmMcpClientper fetch, closed in afinally, permcpClient.ts's documented lifecycle policy ("spawn per user-initiated workflow, tear down when the caller is done with it").Registration timing:
main(), notcontext.onceindex.ts's own doc comment (written ahead of this unit, by Unit F) says latercontext.register*calls should go insidecontext.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'sIExtensionContextdoc comment (~line 3578):So
registerDashletis called synchronously inmain().tryRegisterWsmTool's existing use ofcontext.onceis unaffected and remains correct - it dispatches a Redux action viaapi.store, which needs the store to exist, a genuineonce()-shaped requirement, not a register call. Live gating still works the same way either way, viaisVisible: () => isWitcher3Active(context.api)(registerDashlet's own liveisVisiblecallback, 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 ownregisterMergeHistoryDashletdoc comment carries the citation/reasoning instead, andindex.tshas 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 formergeHistoryDashlet.ts).npm run test:integration- all pass (60 tests across 12 files, including the newmergeHistory.integration.test.ts), against a real, compiledWitcherScriptMerger.Headlessmcpprocess:MergeInventory.xmlby hand (schema confirmed directly againstMergeInventory.cs/Merge.cs/ModFile.cs/FileHash.cs, not guessed) with one recorded merge and two source mods.fetchMergeHistory- deliberately not just re-provingmcpClient.integration.test.ts's existing empty-list case, sinceMergeInventory.Load's barecatch { inventory = new MergeInventory(); }means a malformed fixture would also silently produce[], indistinguishable from "no merges" unless a test actually asserts non-empty content.connectseam, mirroringtoolAcquisition.ts's ownclient/extractorseams) thatclose()is called even whenlistMerges()rejects - the "close in afinally" lifecycle contract specifically.Not verified (honest gap): the dashlet actually mounting/rendering/updating on a real Vortex dashboard.
vitest.config.tshas no jsdom, so nothing here rendersMergeHistoryDashlet- only its data-fetch logic (fetchMergeHistory) and itsregisterDashletcall shape/gating are tested directly. This needs a manual check against a real Vortex install before shipping.Incidental fix:
test:integrationbuild raceAdding this unit's third integration-test file (alongside
mcpClient.integration.test.tsandtoolAcquisition.integration.test.ts) made a latent race visible: vitest runs integration test files in parallel by default, and multiple simultaneousdotnet build/dotnet publishinvocations across those files collide on the sharedWitcherScriptMerger.Corebuild 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 withvitest run test --no-file-parallelism. Fixed by adding that flag to thetest:integrationnpm script - one-line change,package.jsononly.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 intoolAcquisition.ts; not factored into a shared helper since that would require touchingtoolAcquisition.ts/storage.tsbeyond 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.