Fix QAM fresh-open tab selection lifecycle - #522
Conversation
onehoon
left a comment
There was a problem hiding this comment.
Blocking findings before merge:
- The hotfix still guesses the QAM-open lifecycle by requiring
onFocusNavActivated.
The work order explicitly says not to infer an onFocusNavActivated twin merely because onFocusNavDeactivated is the discovery marker. This PR does exactly that in patchQamLifecycle() and the PR body still says real-device validation is pending, so there is no evidence that this callback pair is the current Windows GamepadUI open/close authority. If onFocusNavActivated is absent or does not represent QAM visibility, the whole hotfix fails open and we are back to Steam restoring its last native tab.
There is already a smaller lifecycle seam available at the outer QAM renderer: the current QAM renderer receives a visibility value in its render props. Use the exact live-verified visibility property (or instrument that bounded renderer first if the current build differs) and drive only the false -> true / true -> false transition from it. Do not patch an unverified callback pair.
Example shape:
function updateQamSurfaceVisibility(visible) {
if (visible === true) {
activateQamSurface();
} else if (visible === false) {
deactivateQamSurface();
}
}
const patchedType = preservePatchedFunctionShape(function patchedType(...args) {
const result = originalType.apply(this, args);
if (!state.installed) return result;
// Use the exact current-Steam visibility field verified on this renderer.
updateQamSurfaceVisibility(args[0]?.visible);
try {
patchTabsProducer(result, React, native);
} catch (error) {
logOnce("outerAugmentationFailed", `QAM outer augmentation failed: ${String(error)}`);
}
return result;
}, originalType);Also make the selection prerequisite order insensitive. At the moment trySelectAddonTabForFreshOpen() is called only from activation. If the open/visible transition is observed before ensureAddonTabs() publishes qamSelectionContext, that open is consumed without a selection attempt. Calling the same guarded helper after publishing the descriptor context is enough; the existing qamInitialSelectionRequested flag still guarantees at most one write:
state.qamSelectionContext = { descriptors };
trySelectAddonTabForFreshOpen();This is especially relevant for an already-visible QAM during script/QamHost reinjection and does not require timers, polling, epochs, or retries.
MenuStore.OpenQuickAccessMenu(key, false)is not a verified authority for the Addon's string descriptor keys.
The Addon descriptors use:
"steam-input-addon-device"
"steam-input-addon-profile"but Steam/Decky exposes OpenQuickAccessMenu as taking a QuickAccessTab identifier; Decky's injected tab uses its own numeric tab id (Decky = 999) and passes that id to OpenQuickAccessMenu. There is no evidence in this PR that the current Steam store accepts arbitrary injected string descriptor keys. The extra false argument is also not part of the public current interface. This can make the new selection write a no-op even when lifecycle detection is correct.
Please use the exact current-Steam tab-selection seam that is verified to accept the Addon's descriptor identity. If the live tab owner exposes a key callback, keep the PR #521 string keys and call that exact verified callback rather than routing them through QuickAccessTab. Example shape (property names must match the live inspected owner; do not restore a broad candidate list):
function resolveNativeTabSelection(owner) {
const props = owner?.props;
if (!props || typeof props.onTabSelected !== "function") return null;
return {
set: key => {
if (key !== ADDON_DEVICE_TAB_KEY && key !== ADDON_PROFILE_TAB_KEY) return;
props.onTabSelected(key);
},
};
}If the real supported build instead requires a numeric QuickAccessTab id, document and implement that exact contract consistently with descriptor identity before using OpenQuickAccessMenu; do not pass the existing string keys into an enum-oriented API and assume it will select them.
After these changes, the mandatory Claw acceptance from the work order still needs to be run: native tab selected -> close -> reopen must land on Device/Profile every time, while manual Steam-tab selection must remain untouched until the next close/reopen. The rest of the PR remains appropriately scoped and does not need controller/Full1902 changes.
|
Addressed both blocking findings in commit 78a8363.
Validation: JavaScript check passed; Debug/Release builds passed with 0 warnings and 0 errors; focused QAM tests 34/34; full Release tests 2745/2745; git diff --check passed. MSI Claw hardware acceptance remains pending in this environment. |
|
Follow-up: the latest head 78a8363 now has a passing GitHub Actions build-and-test run (34726123761). All CI build, test, release metadata, publish verification, and startup smoke steps passed. The PR remains Draft only because the mandatory MSI Claw/current Steam hardware acceptance is still pending. |
|
Post-merge real-device acceptance failed on MSI Claw (Google Drive Observed behavior:
The runtime log proves OEM1 -> SteamQuickAccess was invoked repeatedly (for example 09:11:40.976, 09:11:44.118, 09:11:44.972, 09:11:54.346, 09:11:55.171), so the product is generating real QAM open/close interactions. However the QamHost log never emits This means the current root cause is before Please make the next change a bounded diagnostic / follow-up hotfix that discovers the actual supported current-Steam open signal. A useful diagnostic is to log only visibility/open/focus/nav-related outer-render props once per distinct shape, and separately probe the already-verified function logQamOuterLifecycleShape(props) {
if (!props || typeof props !== "object") return;
const entries = Object.keys(props)
.filter(key => /visible|open|active|focus|nav/i.test(key))
.map(key => {
const value = props[key];
const suffix = typeof value === "boolean" ? `=${value}` : "";
return `${key}:${typeof value}${suffix}`;
});
logStateChange("qamOuterLifecycleShape", entries.join("|"),
`QAM outer lifecycle props: ${entries.join(", ") || "none"}`);
}
const patchedType = preservePatchedFunctionShape(function patchedType(...args) {
const result = originalType.apply(this, args);
if (!state.installed) return result;
logQamOuterLifecycleShape(args[0]);
// Do not call updateQamSurfaceVisibility until the real boolean seam is proven.
try {
patchTabsProducer(result, React, native);
} catch (error) {
logOnce("outerAugmentationFailed", `QAM outer augmentation failed: ${String(error)}`);
}
return result;
}, originalType);Also probe the existing native MenuStore authority instead of assuming visibility is the only usable lifecycle source: function installOpenQuickAccessMenuProbe(menuStore) {
const original = menuStore.OpenQuickAccessMenu;
if (typeof original !== "function") return null;
function probed(...args) {
log(`QAM MenuStore call: key=${String(args[0])} open=${String(args[1])} argc=${args.length}`);
return original.apply(this, args);
}
menuStore.OpenQuickAccessMenu = probed;
return () => {
if (menuStore.OpenQuickAccessMenu === probed) {
menuStore.OpenQuickAccessMenu = original;
}
};
}If live evidence confirms a deterministic difference such as A follow-up PR should not be merged until the exact regression passes on hardware: |
Summary
Validation
Manual validation