From 70934a7553489723cd41465aa7d31d426fdbdff3 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 27 Sep 2026 20:13:06 +0200 Subject: [PATCH] feat(theseus): Theseus's own prompts use the dialog sheet too Install-this-app, remove-app, the extension install flow (install, already installed, installed, not found, failed) and the add-on restart question were still bare OS message boxes titled "theseus-navigator" after page dialogs moved to the sheet. askSheet() is a drop-in for dialog.showMessageBox with the same options and result: title as the headline, detail under it, the caller's buttons with the default first, an optional checkbox, and the app's or extension's icon when there is one. Tone follows the box type (error, warning) or the wording. The native box stays as the fallback when the browser window is not there, and for the two synchronous cases (beforeunload, app windows). --- js-dialog-preload.js | 3 +- js-dialog.html | 291 +++++++++++++++++++++++++------------------ main.js | 49 ++++++-- webapps.js | 6 +- 4 files changed, 218 insertions(+), 131 deletions(-) diff --git a/js-dialog-preload.js b/js-dialog-preload.js index f69a44a1..7d37b5a2 100644 --- a/js-dialog-preload.js +++ b/js-dialog-preload.js @@ -4,5 +4,6 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("jsdialog", { onShow: (cb) => ipcRenderer.on("jsdialog-show", (_e, req) => cb(req)), - answer: (reqId, ok, value) => ipcRenderer.invoke("jsdialog-answer", reqId, !!ok, value == null ? null : String(value)), + // value: a string for prompt(); { response, checkboxChecked } for one of Theseus's own prompts. + answer: (reqId, ok, value) => ipcRenderer.invoke("jsdialog-answer", reqId, !!ok, value == null ? null : (typeof value === "object" ? value : String(value))), }); diff --git a/js-dialog.html b/js-dialog.html index beb6d700..f8cea12e 100644 --- a/js-dialog.html +++ b/js-dialog.html @@ -1,121 +1,170 @@ - - - - -Dialog - - - - - - + + + + +Dialog + + + + + + diff --git a/main.js b/main.js index 8372063a..371c39d5 100644 --- a/main.js +++ b/main.js @@ -2163,8 +2163,7 @@ function initAddons() { restartApp: async (addonName) => { const who = String(addonName || "An add-on").slice(0, 60); console.log(`[restart] ${who} asked to relaunch Theseus`); - const parent = win && !win.isDestroyed() ? win : undefined; - const { response } = await dialog.showMessageBox(parent, { + const { response } = await askSheet({ type: "question", title: "Restart Theseus?", message: `${who} wants to restart Theseus.`, detail: "Usually to finish installing its own update. Open tabs are restored after the restart. If you choose Later, the update still applies the next time Theseus starts.", @@ -4191,12 +4190,12 @@ async function installExtensionWithConsent(id, requester) { try { card = (await fetchCommunityCatalog()).find((e) => e.id === id) || null; } catch {} const parent = win && !win.isDestroyed() ? win : undefined; if (!card) { - await dialog.showMessageBox(parent, { type: "warning", title: "Extension not found", message: `"${id}" is not in the community catalog.`, buttons: ["OK"] }); + await askSheet({ type: "warning", title: "Extension not found", message: `"${id}" is not in the community catalog.`, buttons: ["OK"] }); return { ok: false, error: "not in the catalog" }; } const installed = addonHost ? addonHost.snapshot().installed.find((a) => a.id === id) : null; if (installed && addonUpdater.cmpVer(card.latest, installed.version) <= 0) { - const { response: r0 } = await dialog.showMessageBox(parent, { + const { response: r0 } = await askSheet({ type: "info", title: "Already installed", message: `${card.name || id} ${installed.version} is already installed.`, detail: "Newer signed versions arrive through the regular update check. Manage it under Settings › Extensions.", @@ -4206,7 +4205,7 @@ async function installExtensionWithConsent(id, requester) { return { ok: true, version: installed.version, publisher: installed.publisher || card.publisher, alreadyInstalled: true }; } const from = requester ? `Requested by ${requester}.\n\n` : ""; - const { response } = await dialog.showMessageBox(parent, { + const { response } = await askSheet({ type: "question", title: "Install extension", message: installed ? `Update ${card.name || id} ${installed.version} → ${card.latest}?` @@ -4218,7 +4217,7 @@ async function installExtensionWithConsent(id, requester) { if (response !== 0) return { ok: false, error: "cancelled" }; const r = await installCommunityById(id); if (r.ok) { - const { response: after } = await dialog.showMessageBox(parent, { + const { response: after } = await askSheet({ type: "info", title: "Extension installed", message: `${card.name || id} ${r.version} is installed and active.`, detail: `Signed by ${r.publisher || card.publisher}. Manage it under Settings › Extensions.`, @@ -4226,7 +4225,7 @@ async function installExtensionWithConsent(id, requester) { }); if (after === 1) openSettingsTab("addons"); } else { - await dialog.showMessageBox(parent, { type: "error", title: "Install failed", message: `${card.name || id} was not installed.`, detail: r.error || "unknown error", buttons: ["OK"] }); + await askSheet({ type: "error", title: "Install failed", message: `${card.name || id} was not installed.`, detail: r.error || "unknown error", buttons: ["OK"] }); } return r; } finally { installPromptOpen = false; } @@ -4456,6 +4455,14 @@ function jsDialogWho(sender, tab) { } function jsDialogReply(item, ok, value) { const { kind } = item.req; + if (item.resolve) { // one of Theseus's own prompts (askSheet) + const v = value && typeof value === "object" ? value : {}; + const buttons = item.req.buttons || []; + let response = ok ? Number(v.response) : item.req.cancelId; + if (!Number.isInteger(response) || response < 0 || response >= buttons.length) response = item.req.cancelId; + item.resolve({ response, checkboxChecked: !!v.checkboxChecked }); + return; + } const out = kind === "confirm" ? (ok ? "1" : "0") : kind === "prompt" ? (ok ? String(value ?? "") : null) : ""; try { item.e.returnValue = { handled: true, value: out }; } catch {} } @@ -4504,6 +4511,33 @@ function finishJsDialog(ok, value) { try { const t = cur.tabId != null ? tabById(cur.tabId) : null; if (t && t.id === activeId) t.view.webContents.focus(); } catch {} pumpJsDialog(); } +// Theseus's own prompts — install this app / extension, remove, restart — +// in the same sheet as page dialogs. Same option shape as +// dialog.showMessageBox (title, message, detail, buttons, defaultId, +// cancelId, checkboxLabel, checkboxChecked, icon, type), same result; the +// native box remains the fallback when the browser window is not there. +function askSheet(opts) { + const o = opts || {}; + const buttons = Array.isArray(o.buttons) && o.buttons.length ? o.buttons.map(String) : ["OK"]; + const defaultId = Number.isInteger(o.defaultId) && o.defaultId >= 0 && o.defaultId < buttons.length ? o.defaultId : 0; + const cancelId = Number.isInteger(o.cancelId) && o.cancelId >= 0 && o.cancelId < buttons.length ? o.cancelId : (buttons.length > 1 ? buttons.length - 1 : 0); + if (!jsDialogPop || !winAlive()) { + const parent = win && !win.isDestroyed() ? win : undefined; + return dialog.showMessageBox(parent, { ...o, noLink: true }).catch(() => ({ response: cancelId, checkboxChecked: false })); + } + let iconUrl = null; + try { if (o.icon && typeof o.icon.toDataURL === "function" && !o.icon.isEmpty()) iconUrl = o.icon.toDataURL(); } catch {} + const req = { + reqId: ++jsDialogSeq, kind: "app", who: { label: "Theseus", kind: "app" }, + title: String(o.title || ""), message: String(o.message || ""), detail: String(o.detail || ""), + buttons, defaultId, cancelId, type: String(o.type || "none"), iconUrl, + checkbox: o.checkboxLabel ? { label: String(o.checkboxLabel), checked: !!o.checkboxChecked } : null, + }; + return new Promise((resolve) => { + jsDialogQueue.push({ resolve, tabId: null, req }); + pumpJsDialog(); + }); +} // A tab closing (or the window going away) must not leave its renderer // blocked inside a sendSync that nobody will answer. function dismissJsDialogFor(tabId) { @@ -6361,6 +6395,7 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) { initAddons(); webapps.init({ parentWindow: () => (win && !win.isDestroyed() ? win : undefined), + ask: askSheet, openInTab: openInMainTab, targetUrlFor, isBnsHost, prepareContents: (wc) => { styleScrollbars(wc); try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {} try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {} applyFingerprint(wc); }, diff --git a/webapps.js b/webapps.js index a46cebd1..b51cbb61 100644 --- a/webapps.js +++ b/webapps.js @@ -269,7 +269,8 @@ async function install(desc, ctx = {}) { const parent = deps.parentWindow ? deps.parentWindow() : undefined; let icon; // best effort preview in the dialog try { const raw = await fetchBytes(desc.iconUrl || ""); const i = nativeImage.createFromBuffer(raw); if (!i.isEmpty()) icon = i.resize({ width: 64, height: 64 }); } catch {} - const { response, checkboxChecked } = await dialog.showMessageBox(parent, { + const ask = (o) => (deps.ask ? deps.ask(o) : dialog.showMessageBox(parent, o)); + const { response, checkboxChecked } = await ask({ type: "question", title: "Install app", icon, message: `Install ${desc.name}?`, detail: `${desc.host}\n\nIt opens in its own window and gets a Start Menu entry. It keeps running inside Theseus, with your names, add-ons and settings.`, @@ -302,7 +303,8 @@ async function uninstall(key, ask = true) { const entry = find(key); if (!entry) return false; if (ask) { const parent = deps.parentWindow ? deps.parentWindow() : undefined; - const { response } = await dialog.showMessageBox(parent, { + const ask = (o) => (deps.ask ? deps.ask(o) : dialog.showMessageBox(parent, o)); + const { response } = await ask({ type: "question", title: "Remove app", message: `Remove ${entry.name} from Theseus?`, detail: "Its window and shortcuts go away. The site itself and your data on it are untouched.", buttons: ["Remove", "Cancel"], defaultId: 0, cancelId: 1, noLink: true,