Summary
showMessageBox() is typed as returning MessageBoxResult ({ response: number }), but on the native path it returns Craft's raw result, which is { buttonIndex: number }. Nothing reconciles the two, so result.response is undefined at runtime whenever a Craft bridge is present — while the web fallback in the same function does return { response }.
The result is a silent, type-clean wrong answer: TypeScript is satisfied, the browser works, and the packaged desktop app reads undefined off every dialog.
Evidence
The declared contract — @stacksjs/desktop/dist/dialogs.d.ts:
export declare function showMessageBox(options: MessageBoxOptions): Promise<MessageBoxResult>;
export declare interface MessageBoxResult {
response: number
}The implementation returns the bridge result untouched (from the bundled native-dialog build):
if (y()) { // craft bridge present?
try {
return await window.craft.dialog.showMessageBox(Z); // <- raw, not normalised
} catch (P) {
console.warn("[stx-dialog] Failed to show native message box:", P);
}
}
// ...web fallback below returns { response: confirm(z) ? X : G }
return { response: confirm(z) ? X : G };Craft answers with buttonIndex — strings on the shipped craft-runtime (0.0.90):
{"canceled":false,"color":"#{"buttonIndex":And the adapter never mentions it. grep -c buttonIndex over the whole bundled @stacksjs/desktop output: 0. The only buttonIndex in the package's types belongs to ModalResult (types.d.ts), a different API.
So the two branches of one function return different shapes, and the typed one is the branch that is wrong.
Impact
Any if (result.response === 0) / buttons[result.response] check silently takes the "not that button" path on desktop, no matter what the user pressed.
In our app (stacksjs/system-cleaner) this made every confirm dialog answer itself No: the sheet drew correctly, the user pressed the action button, and the answer was discarded on the way back. Clean Selected cleaned nothing, Empty Trash emptied nothing, and neither reported anything — there is no console in a packaged app, so it presented as "the button does nothing". It survived two rounds of fixes aimed at button ordering (defaultButton/cancelButton) before we traced it, because the ordering reasoning was sound and could never work while the index was read off a key the host never sets.
Suggested fix
Normalise in the adapter, so the declared type holds on both paths:
const raw = await window.craft.dialog.showMessageBox(opts)
const response = typeof raw.response === 'number' ? raw.response : raw.buttonIndex
return { response }Alternatively widen MessageBoxResult to { response?: number, buttonIndex?: number } so the ambiguity is at least visible to callers — though normalising seems better, since response is the documented field and the one the examples in dialogs.d.ts use.
Worth a look at the sibling dialogs too (showOpenDialog, the color picker) in case they pass other Craft payloads through unnormalised.
Versions
@stacksjs/desktop0.2.281craft-native0.0.90- macOS 26.6.2, arm64