The chrome stub from 19.D-β-1 finds the bundle window by walking `_NET_CLIENT_LIST` and matching `WM_CLASS.res_name` against the bundle id. That works for native MoonBase apps because we control how they spawn. But the whole point of Legacy Mode is to wrap an unmodified Qt or GTK binary in a `.app`, and an unmodified kate will set its `res_name` to "kate", not "show.blizzard.testqt". The chrome would never find its window.
19.D-β-2a is the fix: a one-key TOML hint in `Info.appc` that tells `moonbase-launch` how to make the inner toolkit cooperate.
[wrap] toolkit = "qt6" # or qt5, gtk3, gtk4, native (default)
The launcher reads it, validates against the allowlist (anything else is a hard error per the schema's "unknown value in an enumerated key" rule), and rewrites bwrap's argv accordingly:
* native — no rewrites. Native MoonBase apps already call `moonbase_window_create()` and the chrome stub finds them via the IPC handshake (a post-β path; today's stub still uses WM_CLASS for native too, but that's fine because native apps already set their own res_name).
* qt5 / qt6 — bwrap gets `--argv0 <bundle.id>` AND the inner exec gets the trailing argv `-name <bundle.id>`. Qt's `-name` switch is documented to set `XClassHint.res_name`. The `--argv0` is belt-and-suspenders — Qt actually keys off `-name` first, but a future toolkit might not.
* gtk3 / gtk4 — `--argv0 <bundle.id>` only. GTK derives `g_get_prgname()` from `g_path_get_basename(argv[0])`, which in turn seeds `WM_CLASS.res_name`. There's no equivalent of `-name` on the GTK side, but `--argv0` is enough.
Forward-compat is preserved both ways. Newer launchers that don't understand a future `[wrap].toolkit = "qt7"` value will hard-error (the schema says enum values are strict). But older launchers that don't understand `[wrap]` at all will skip the whole table entirely under the schema's "unknown top-level table → warn, do not error" rule and just exec the bundle without the WM_CLASS stamp. The chrome won't find the window on those older launchers, but the bundle still runs — graceful degradation instead of a hard refusal.
Smoke test on the Legion: built a tiny C binary inside `/tmp/wraptest.appdev` that just printed its argv, then ran it under the launcher with three different `[wrap].toolkit` values:
qt6 → argv = [show.blizzard.testqt, -name, show.blizzard.testqt] native → argv = [/path/to/exe] (no rewrites) gtk3 → argv = [show.blizzard.testqt] (argv0 only)
Three lines of TOML pluming through to three different argv shapes, exactly the matrix the schema doc promises.
What this unblocks: 19.D-β-2b (DBusMenu import) can now assume the bundle window's WM_CLASS instance equals the bundle id, so the chrome stub can call `com.canonical.AppMenu.Registrar.GetMenuForWindow(xid)` and import the legacy app's menu tree using the bundle id as the lookup key.
What this doesn't change: menubar's app-name lookup, moonrock's window-rules, and inputsession's per-window context all key on `WM_CLASS.res_class` (the second string), not `res_name`. Both strings get stamped — Qt sets res_name from `-name` and Qt's default res_class from the binary's basename, so the class naturally stays toolkit-conventional. Side note: there's a single edge case at `moonrock_plugin.c:768` where user-configured plugin rules also key on `WM_CLASS`, so a user who hand-wrote a rule matching `kate` won't see it apply when kate is wrapped in a bundle with a stamped res_name. Non-blocking, worth a CLAUDE.md addendum later.