diff --git a/electron/main/services/performanceConfig.ts b/electron/main/services/performanceConfig.ts index c35ef64d..11113ece 100644 --- a/electron/main/services/performanceConfig.ts +++ b/electron/main/services/performanceConfig.ts @@ -776,6 +776,7 @@ export function listPerformancePresets(): PerformancePresetSummary[] { ref: release.ref, refKind: release.refKind, commit: release.commit, + historyCommit: release.historyCommit, date: release.date, settingCount: release.convars.length + release.sectionOps.length, optIn: release.optIn.map((control) => ({ diff --git a/electron/main/services/performanceConfigData.ts b/electron/main/services/performanceConfigData.ts index 78f6aeb6..989c0f36 100644 --- a/electron/main/services/performanceConfigData.ts +++ b/electron/main/services/performanceConfigData.ts @@ -80,6 +80,9 @@ export interface PresetRelease { refKind: 'tag' | 'prose'; /** Immutable pin. This, not `ref`, is what was fetched. */ commit: string; + /** Last commit at or before the pin that touched this preset's file. + * Prose history is path-scoped, so this is its row identity. */ + historyCommit: string; /** Upstream release date, yyyy-mm-dd. Tag names do not reliably sort * into release order, so this is what the picker shows to disambiguate. */ date: string; @@ -240,6 +243,7 @@ const SqookyDefault: PerformancePresetFamily = { ref: '2.9', refKind: 'prose', commit: '96ff42d1db29ca9fe44afb3df749fa763bb87b87', + historyCommit: 'ca74b464b16d4c2e2eee38d8a23b67efd98e4975', date: '2026-08-17', sha256: 'b31825fea70a48412b304b5f3dd06c24d5c29d9feb98d559d337eaef928457f3', sectionOps: [ @@ -549,6 +553,7 @@ const SqookyDefault: PerformancePresetFamily = { ref: '2.8.2', refKind: 'prose', commit: 'a7ba9d63dc6bb660208374ba2f18955e77042442', + historyCommit: 'a7ba9d63dc6bb660208374ba2f18955e77042442', date: '2026-08-02', sha256: '6c3753fabda7c724a01c9a9747e4a14c8618fe3815ecddae929623252e60db5c', sectionOps: [ @@ -866,6 +871,7 @@ const SqookyDefault: PerformancePresetFamily = { ref: '2.7', refKind: 'prose', commit: '271befb5cf9f8c9431da81e39ea4147895604d3e', + historyCommit: '271befb5cf9f8c9431da81e39ea4147895604d3e', date: '2026-07-25', sha256: 'c34801cc66cdd93e3dc0bfea275c98fa9c2a3cc29ee17a1f0397314a2310aeca', sectionOps: [ @@ -1182,6 +1188,7 @@ const SqookyTesting: PerformancePresetFamily = { ref: '2.9', refKind: 'prose', commit: '96ff42d1db29ca9fe44afb3df749fa763bb87b87', + historyCommit: '96ff42d1db29ca9fe44afb3df749fa763bb87b87', date: '2026-08-17', sha256: '362c7c577c37f821cf9f484f4505c7a21afecc014e3064e77798ede620189d55', sectionOps: [ @@ -1528,6 +1535,7 @@ const SqookyTesting: PerformancePresetFamily = { ref: '2.8.2', refKind: 'prose', commit: 'a7ba9d63dc6bb660208374ba2f18955e77042442', + historyCommit: '43c6aca8583524e87e54977ed963aebc0e89b678', date: '2026-08-02', sha256: '7cb2c5d0f7100d90e6f4ddd4fa8b067533d2bcd3a520ebe936fec75263c4084b', sectionOps: [ @@ -1895,6 +1903,7 @@ const SqookyTesting: PerformancePresetFamily = { ref: '2.7', refKind: 'prose', commit: '271befb5cf9f8c9431da81e39ea4147895604d3e', + historyCommit: '48383a17d0cb00927dd8713fe0a6b04c4b218f5a', date: '2026-07-25', sha256: 'b8e2e34ae293d9a6d3a5bd115d6f92626d19662684033beb906401f6a7b6f0bf', sectionOps: [ @@ -2283,6 +2292,7 @@ const BootMaxFps: PerformancePresetFamily = { ref: '2.9', refKind: 'prose', commit: '96ff42d1db29ca9fe44afb3df749fa763bb87b87', + historyCommit: '91ca1cb14f4e8fcbb77d310d09fd6683edab68dc', date: '2026-08-17', sha256: '6dbc82150afe2ec409646025b2437b093aab18d852a7329b80aded2ffb7cf0e5', // Byte-identical upstream in 2.8.2, 2.7, collapsed into this entry. @@ -2737,6 +2747,7 @@ const KaizuMinSpec: PerformancePresetFamily = { ref: '2.9', refKind: 'prose', commit: '96ff42d1db29ca9fe44afb3df749fa763bb87b87', + historyCommit: '528ded17e7b89480fe1b896615099e4fa3511676', date: '2026-08-17', sha256: '16e9e739b90941543f2fabeea8161fd15327df5f16d812377fbd2bf4e75a98a4', sectionOps: [ @@ -2999,6 +3010,7 @@ const KaizuMinSpec: PerformancePresetFamily = { ref: '2.8.2', refKind: 'prose', commit: 'a7ba9d63dc6bb660208374ba2f18955e77042442', + historyCommit: '068f6d01b7920675e2360a7fca4f120dd07c05bd', date: '2026-08-02', sha256: 'e4be5933e6f24de0c368c77957dd7579ef8e2e8169fe4e9740977c09db08fa68', sectionOps: [ @@ -3430,6 +3442,7 @@ const KaizuMinSpec: PerformancePresetFamily = { ref: '2.7', refKind: 'prose', commit: '271befb5cf9f8c9431da81e39ea4147895604d3e', + historyCommit: '983d51a5c7567be9e6efbdd445304e51146793c5', date: '2026-07-25', sha256: 'f27e7d6fd479472f4aa127d4dd5eadb8ec6805e71e3f561e6f02ff879354c1c9', sectionOps: [ @@ -3857,6 +3870,7 @@ const OptilockFps: PerformancePresetFamily = { ref: 'v4.6', refKind: 'tag', commit: '6caab13036711d09ae439b4de4406bdb9972e148', + historyCommit: '6caab13036711d09ae439b4de4406bdb9972e148', date: '2026-08-15', sha256: 'ce1b084d986de7807f1a456c6845b232f08396e545ff0b133fdfc4a135df16c8', sectionOps: [ @@ -4280,6 +4294,7 @@ const OptilockFps: PerformancePresetFamily = { ref: 'v4.2', refKind: 'tag', commit: 'fdfce93a95db176c539d3dbcb601238e181008b9', + historyCommit: 'fdfce93a95db176c539d3dbcb601238e181008b9', date: '2026-07-15', sha256: '34873789e25b552829cff843b111b7c5f7f3127651a31d6e3f7fd1d67a2e1291', sectionOps: [ @@ -4799,6 +4814,7 @@ const OptilockFps: PerformancePresetFamily = { ref: 'v4.0d', refKind: 'tag', commit: '465590a35d8d47d84b78031837fa209f5a2b5ce7', + historyCommit: '465590a35d8d47d84b78031837fa209f5a2b5ce7', date: '2026-07-12', sha256: 'a0017f99282415dc027a687fdf01b06de707d6d28c5fbd161339493db4f1f618', sectionOps: [ @@ -5343,6 +5359,7 @@ const OptilockMax: PerformancePresetFamily = { ref: 'v4.6', refKind: 'tag', commit: '6caab13036711d09ae439b4de4406bdb9972e148', + historyCommit: '6caab13036711d09ae439b4de4406bdb9972e148', date: '2026-08-15', sha256: '5c3d4956117ffdf7c583042b23f34185ca95f91f346eb77ab86764106722f703', sectionOps: [ @@ -5876,6 +5893,7 @@ const OptilockMax: PerformancePresetFamily = { ref: 'v4.2', refKind: 'tag', commit: 'fdfce93a95db176c539d3dbcb601238e181008b9', + historyCommit: 'fdfce93a95db176c539d3dbcb601238e181008b9', date: '2026-07-15', sha256: '69032638fda021655d21e5ff8aaf8c53f903c32d55853a576575efd4dfe13326', // Byte-identical upstream in v4.0d, collapsed into this entry. diff --git a/scripts/gen-performance-presets.mjs b/scripts/gen-performance-presets.mjs index 9efdad1a..dbbe2b94 100644 --- a/scripts/gen-performance-presets.mjs +++ b/scripts/gen-performance-presets.mjs @@ -100,6 +100,26 @@ async function commitDate(repo, commit) { return (await res.json()).commit.author.date.slice(0, 10); } +// A prose "release" pin is a repository snapshot, but the history modal lists +// only commits that touched one preset's file. Resolve the last such commit at +// the snapshot so the renderer can identify bundled rows without fetching +// every historical file or guessing from commit-message prose. +async function lastPathCommit(repo, commit, path) { + const query = new URLSearchParams({ sha: commit, path, per_page: '1' }); + const res = await fetch(`https://api.github.com/repos/${repo}/commits?${query}`, GH); + if (!res.ok) { + fail( + `Could not resolve path history for ${repo}@${commit.slice(0, 8)} ${path} ` + + `(${res.status})` + ); + } + const commits = await res.json(); + if (!Array.isArray(commits) || !commits[0]?.sha) { + fail(`No path history for ${repo}@${commit.slice(0, 8)} ${path}`); + } + return commits[0].sha; +} + // A source pinned with refKind 'tag' claims, in the UI, that the preset comes // from a published release. Tags are mutable, so that claim can quietly become // false. The commit is still what we fetch and the sha256 still gates content: @@ -254,6 +274,9 @@ function emit(manifest, presets) { L.push(` refKind: 'tag' | 'prose';`); L.push(` /** Immutable pin. This, not \`ref\`, is what was fetched. */`); L.push(` commit: string;`); + L.push(` /** Last commit at or before the pin that touched this preset's file.`); + L.push(` * Prose history is path-scoped, so this is its row identity. */`); + L.push(` historyCommit: string;`); L.push(` /** Upstream release date, yyyy-mm-dd. Tag names do not reliably sort`); L.push(` * into release order, so this is what the picker shows to disambiguate. */`); L.push(` date: string;`); @@ -350,6 +373,7 @@ function emit(manifest, presets) { L.push(` ref: ${q(r.ref)},`); L.push(` refKind: ${q(r.refKind)},`); L.push(` commit: ${q(r.commit)},`); + L.push(` historyCommit: ${q(r.historyCommit)},`); L.push(` date: ${q(r.date)},`); L.push(` sha256: ${q(r.sha256)},`); if (r.supersedes.length) { @@ -512,6 +536,11 @@ async function main() { continue; } + const historyCommit = + source.refKind === 'prose' + ? await lastPathCommit(source.repo, release.commit, path) + : release.commit; + const label = source.releases.length > 1 ? `${entry.id}@${release.ref}` : entry.id; const { sectionOps, convars, optIn } = diffAgainstBaseline( baseline, @@ -527,6 +556,7 @@ async function main() { ref: release.ref, refKind: source.refKind, commit: release.commit, + historyCommit, date: release.date, sha256: expected, supersedes: [], diff --git a/src/components/performance/GameplayOptIns.tsx b/src/components/performance/GameplayOptIns.tsx index 9c846eb5..13f1b7fa 100644 --- a/src/components/performance/GameplayOptIns.tsx +++ b/src/components/performance/GameplayOptIns.tsx @@ -49,12 +49,12 @@ export default function GameplayOptIns({ })).filter((g) => g.controls.length > 0); return ( -
+
@@ -567,67 +582,70 @@ export default function PerformanceConfigCard() { )} {selected && selectedRelease && ( - - ), - kofi: ( - - ), - }} - /> - ) : null +
+ + + + Ko-fi + + + ) : null + } + /> +
+ )} + + +
+
+ void onToggleTrackLatest(on)} + label={ + + {t('performance.trackLatest.label')} + } + disabled={busy} + /> + {latestLine &&

{latestLine}

} +
+ + {selectedRelease && ( + void onChangeOptIns(keys)} + disabled={busy} /> )} -
- -
- void onToggleTrackLatest(on)} - label={t('performance.trackLatest.label')} - description={t('performance.trackLatest.description')} - disabled={busy} - /> - {latestLine && ( - // Indent past the switch (w-11 + gap-3) so the line reads as part - // of the toggle's description column. -

{latestLine}

- )} -
- - {selectedRelease && ( - void onChangeOptIns(keys)} - disabled={busy} - /> - )} - -
+ + +
-
-

- {look &&

+
+ {look &&
+ {showStatusDetail && ( +

{status ? performanceStatusMessage( status, @@ -637,17 +655,17 @@ export default function PerformanceConfigCard() { latestUsable ? latest!.version! : undefined ) : t('performance.checkingGameinfo')} - -

+

+ )} {pinnedOlder && !pendingVersion && ( -

+

{t('performance.version.pinned', { version: selectedVersion, latest: selected?.versions[0].version ?? '', })}

)} - {openError &&

{openError}

} + {openError &&

{openError}

}
@@ -729,8 +747,7 @@ export default function PerformanceConfigCard() {
)} -

{t('performance.shadowsHint')}

-
+
{pickerOpen && ( onSelect(e.target.value)}> {presets.map((preset) => ( ))} diff --git a/src/components/performance/PresetSummary.tsx b/src/components/performance/PresetSummary.tsx index c107adfe..586d567e 100644 --- a/src/components/performance/PresetSummary.tsx +++ b/src/components/performance/PresetSummary.tsx @@ -47,7 +47,7 @@ export default function PresetSummary({ preset, release, creditSlot }: PresetSum : t('performance.preset.pinnedCommit', { ref: release.ref }); return ( -
+
{t(`performance.preset.tier.${preset.tier}`)} @@ -60,35 +60,34 @@ export default function PresetSummary({ preset, release, creditSlot }: PresetSum
-

+

{t(`performance.preset.tierBlurb.${preset.tier}`)}

{preset.unstable && ( -

+

)} -

- {t('performance.preset.credit', { - credit: preset.upstream.credit, - license: preset.upstream.license, - })}{' '} - {creditSlot}{' '} - - -

+
+ {preset.upstream.credit} · {preset.upstream.license} +
+ {creditSlot && {creditSlot}} + + +
+
); } diff --git a/src/components/performance/VersionHistoryModal.test.ts b/src/components/performance/VersionHistoryModal.test.ts new file mode 100644 index 00000000..506bda0d --- /dev/null +++ b/src/components/performance/VersionHistoryModal.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import type { PerformancePresetVersion, PerformanceRemoteVersion } from '../../types/electron'; +import { + bundledPerformanceVersionFor, + performanceHistoryRowCopy, +} from '../../lib/performanceHistory'; + +function entry(label: string | null, ref = '96ff42d1'): PerformanceRemoteVersion { + return { + ref, + version: ref, + commit: ref.padEnd(40, '0'), + date: '2026-08-17', + label, + }; +} + +describe('performanceHistoryRowCopy', () => { + it('promotes a prose release number and removes it from the detail', () => { + expect(performanceHistoryRowCopy(entry('2.9.1 release'))).toEqual({ + primary: '2.9.1', + detail: 'release', + }); + }); + + it('uses the known bundled version when its commit message has no version', () => { + expect(performanceHistoryRowCopy(entry('readme update'), '2.8.2')).toEqual({ + primary: '2.8.2', + detail: 'readme update', + }); + }); + + it('keeps the short commit when no release number is known', () => { + expect(performanceHistoryRowCopy(entry('minor documentation update', 'be2d3889'))).toEqual({ + primary: 'be2d3889', + detail: 'minor documentation update', + }); + }); +}); + +describe('bundledPerformanceVersionFor', () => { + it('matches a prose history row by the commit that touched its config path', () => { + const remote = entry('2.9 update', '9c3517c7'); + const bundled: PerformancePresetVersion = { + version: '2.9', + ref: '2.9', + refKind: 'prose', + commit: '96ff42d1db29ca9fe44afb3df749fa763bb87b87', + historyCommit: remote.commit!, + date: '2026-08-17', + settingCount: 282, + optIn: [], + }; + + expect(bundledPerformanceVersionFor(remote, [bundled])).toBe('2.9'); + }); +}); diff --git a/src/components/performance/VersionHistoryModal.tsx b/src/components/performance/VersionHistoryModal.tsx index 499dfa75..88431777 100644 --- a/src/components/performance/VersionHistoryModal.tsx +++ b/src/components/performance/VersionHistoryModal.tsx @@ -4,6 +4,10 @@ import { CircleAlert, HardDriveDownload, Loader2, Package } from 'lucide-react'; import { Modal } from '../common/Modal'; import { ModalHeader } from '../common/ui'; import { fetchPerformanceRemoteVersion, listPerformanceRemoteVersions } from '../../lib/api'; +import { + bundledPerformanceVersionFor, + performanceHistoryRowCopy, +} from '../../lib/performanceHistory'; import type { PerformanceLatestInfo, PerformancePresetSummary, @@ -42,7 +46,8 @@ export default function VersionHistoryModal({ const [fetchingRef, setFetchingRef] = useState(null); const [fetchError, setFetchError] = useState(null); - const bundled = new Set(preset.versions.map((v) => v.version)); + const bundledVersionFor = (entry: PerformanceRemoteVersion) => + bundledPerformanceVersionFor(entry, preset.versions); useEffect(() => { let cancelled = false; @@ -62,8 +67,9 @@ export default function VersionHistoryModal({ const pick = async (entry: PerformanceRemoteVersion) => { setFetchError(null); - if (bundled.has(entry.version)) { - onPickBundled(entry.version); + const bundledVersion = bundledVersionFor(entry); + if (bundledVersion) { + onPickBundled(bundledVersion); return; } setFetchingRef(entry.ref); @@ -107,8 +113,10 @@ export default function VersionHistoryModal({

{t('performance.history.empty')}

)} {versions?.map((entry) => { - const isBundled = bundled.has(entry.version); - const isSelected = entry.version === selectedVersion; + const bundledVersion = bundledVersionFor(entry); + const row = performanceHistoryRowCopy(entry, bundledVersion); + const isBundled = !!bundledVersion; + const isSelected = (bundledVersion ?? entry.version) === selectedVersion; const isFetching = fetchingRef === entry.ref; return (