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,