Theseus 0.3.31 rewrite — UI improvements + defensive hash-verify, spawn flags unchanged

Same 0.3.31 version, new binary. Rebuilds the shipped 0.3.31 with the
salvageable content from the reverted 0.3.32-0.3.34 track:

  chrome.html
    - light-mode chrome strip: --bg #e6e8ec, inactive tab #f2f4f7,
      active tab #ffffff. Fixes the "tabs disappear into the light
      Windows title bar" report.
    - bookmark chips shrunk: 130px max-width, 11px text, 12px favicon,
      22px row (was 26). ~40% more chips fit in the same width.
    - bookmark chips draggable with the tab-strip's left/right-half
      drop convention; new .dropbefore/.dropafter accent.
    - light-mode .tor + .logo + .upchip chips: from illegible white-
      on-#253A49 (at 12-13px) to #eef1f5 with #253A49 ink. Both readable
      now. .tor.connecting/.on keep amber/purple hue in light fills.

  main.js
    - will-download update handler now streams the saved setup .exe
      through crypto.createHash("sha256"), compares to the manifest's
      updateAvailable.setupHash before marking ready. Rejects and
      deletes the file on mismatch or on empty manifest hash. Test C
      in the previous session proved this catches truncated payloads
      Electron reports as "completed" (a real class of failure the
      Ariadne addon updater has always guarded against here).
    - new bookmark-move IPC: splices the list, no-ops on self-drop
      or missing entry.

  preload.js
    - moveBookmark(fromUrl, targetUrl, place) exposed for chrome.

Deliberately NOT changed: install-update-now still spawns setup with
["/S"] alone. The 0.3.32 --updated /S --force-run change was proven
in the previous session's real-install E2E to not address the actual
"browser vanished on D:\Program Files install" symptom — every flag
combination (/S alone, --updated /S --force-run, /S /currentuser,
/S /D=<install>) exits 0 without upgrading anything on that specific
install path. That's a separate open bug; not touched here.

Version stays 0.3.31 — this is a binary rewrite of 0.3.31, not a new
release. Existing 0.3.31 installs won't see an update chip (version
compare returns false), which is intentional given the auto-update
path is still broken for non-default install locations.
This commit is contained in:
Local Dev 2026-09-08 21:26:42 +02:00
parent 347e63de0c
commit 3e784e299c
3 changed files with 126 additions and 23 deletions

View file

@ -13,7 +13,12 @@
}
@media (prefers-color-scheme: light) {
:root {
--bg:#e9ecf1; --surface:#ffffff; --surface2:#f1f3f7; --active:#dbe1ec;
/* Chrome strip is a soft neutral grey — flat white was too bright next
to the browsed page, and pure #ffffff tabs disappeared into the
window title bar on light Windows themes. --bg tints the whole
top strip; --surface (inactive tab) sits a hair above it; --active
(foreground tab) pops a bit brighter so the current tab reads. */
--bg:#e6e8ec; --surface:#f2f4f7; --surface2:#eaecf0; --active:#ffffff;
--ink:#1a1f28; --mut:#3c4453; --dim:#697280; --faint:#98a1b0;
--line:rgba(0,0,0,.18); --line2:rgba(0,0,0,.11); --hover:rgba(0,0,0,.06);
/* Darker acid for light backgrounds — same hue family, ~5.5:1
@ -293,8 +298,10 @@
padding: 6px 12px; border: 1px solid color-mix(in srgb, var(--acid) 20%, transparent); border-radius: 8px; cursor: pointer; background: rgb(from var(--acid) r g b / .08); font-size: 13px; }
.logo:hover { background: rgb(from var(--acid) r g b / .16); }
.logo .gear { font-size: 12px; opacity: .8; }
/* favorites bar — new-tab page only, single compact row */
.bookmarks { display: flex; align-items: center; gap: 4px; padding: 3px 10px 5px; height: 26px; overflow: hidden; }
/* favorites bar — new-tab page only, single compact row. Chips are
tight so many fit in the row; the whole strip is 22px tall (down
from 26) so the chrome view gains a bit of vertical space. */
.bookmarks { display: flex; align-items: center; gap: 3px; padding: 2px 10px 4px; height: 22px; overflow: hidden; }
/* Ctrl+F find-in-page bar. Sits below the address bar (above tabs body),
~32px tall so it's part of the chrome view height like .tordisc / .bcnrbar.
syncHeight in the tabs handler picks it up automatically. */
@ -307,11 +314,14 @@
.findbar .findbtn { border: 1px solid var(--line); background: var(--surface2); color: var(--ink); border-radius: 6px;
padding: 3px 8px; font-size: 13px; cursor: pointer; }
.findbar .findbtn:hover { background: var(--surface); border-color: color-mix(in srgb, var(--acid) 35%, transparent); }
.bm { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 3px 9px; border-radius: 6px;
background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 12px; cursor: pointer; white-space: nowrap; }
.bm { display: flex; align-items: center; gap: 5px; max-width: 130px; padding: 2px 7px; border-radius: 5px;
background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 11px; cursor: grab; white-space: nowrap; }
.bm:hover { background: var(--active); color: var(--ink); }
.bm .bmfav { width: 14px; height: 14px; flex: none; object-fit: contain; border-radius: 2px; }
.bm .bt { overflow: hidden; text-overflow: ellipsis; }
.bm.dragging { opacity: .45; cursor: grabbing; }
.bm.dropbefore { box-shadow: -2px 0 0 var(--acid); }
.bm.dropafter { box-shadow: 2px 0 0 var(--acid); }
.bm .bmfav { width: 12px; height: 12px; flex: none; object-fit: contain; border-radius: 2px; }
.bm .bt { overflow: hidden; text-overflow: ellipsis; max-width: 96px; }
/* In-chrome prompt modal (window.prompt is disabled in Electron BrowserViews,
so bookmark rename etc. use this instead). */
.promptmask { position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 10000;
@ -344,15 +354,21 @@
@media (prefers-color-scheme: light) {
.ctxmenu { background: #ffffff; border-color: rgba(0,0,0,.15); }
.ctxmenu .mi:hover { background: rgba(0,0,0,.05); }
/* BCH dark #253A49 as a light-mode accent so the toolbar isn't
all-white-on-white. .logo (⛓ Theseus) becomes a dark chip; the
download / update pill picks it up so both stand out. */
.logo { background: #253A49; color: #F8FDFF; border-color: #253A49; }
.logo:hover { background: #1a2c39; }
.logo .gear { color: #F8FDFF; opacity: .85; }
.upchip { background: #253A49; color: #F8FDFF; border-color: #253A49; }
.upchip .upcore:hover { background: #1a2c39; }
.upchip .updismiss { border-left-color: rgba(255,255,255,.20); color: #F8FDFF; }
/* The old light-mode chips used the BCH dark #253A49 as their fill.
At small font sizes on light Windows the white-on-dark-navy label
fell below legible, so both .logo (⛓ Theseus) and .tor switch to
a light-grey chip with dark ink — same footprint, readable text.
Update pill picks up the same treatment. */
.logo { background: #eef1f5; color: #253A49; border-color: rgba(0,0,0,.14); }
.logo:hover { background: #e2e6ec; }
.logo .gear { color: #253A49; opacity: .85; }
.tor { background: #eef1f5; color: #253A49; border-color: rgba(0,0,0,.14); }
.tor:hover { background: #e2e6ec; }
.tor.connecting { background: #fbe9b5; color: #5a3f00; border-color: rgba(0,0,0,.14); }
.tor.on { background: #e6d9f5; color: #3f1f6b; border-color: rgba(0,0,0,.14); }
.upchip { background: #eef1f5; color: #253A49; border-color: rgba(0,0,0,.14); }
.upchip .upcore:hover { background: #e2e6ec; }
.upchip .updismiss { border-left-color: rgba(0,0,0,.14); color: #253A49; }
}
.tordisc { font-size: 11.5px; color: #d9c7f2; background: #2a1c40; border-top: 1px solid #6b3fa055; padding: 5px 14px; }
.tordisc a { color: var(--acid); }
@ -793,7 +809,7 @@
const fav = b.favicon
? `<img class="bmfav" src="${String(b.favicon).replace(/"/g,"&quot;")}" onerror="this.remove()">`
: `<span class="bmfav" style="display:inline-block"></span>`;
return `<div class="bm" data-i="${i}" title="${(b.url||"").replace(/"/g,"&quot;")}">${fav}<span class="bt">${(b.title||b.url).replace(/</g,"&lt;")}</span><span class="bx" data-x="${i}"></span></div>`;
return `<div class="bm" draggable="true" data-i="${i}" data-url="${(b.url||"").replace(/"/g,"&quot;")}" title="${((b.title||b.url)+" "+(b.url||"")).replace(/"/g,"&quot;")}">${fav}<span class="bt">${(b.title||b.url).replace(/</g,"&lt;")}</span><span class="bx" data-x="${i}"></span></div>`;
}).join("")
: `<span class="bm-empty">No saved pages yet — click ☆ or right-click here to add one.</span>`;
box.querySelectorAll(".bm").forEach((el) => el.onclick = (e) => {
@ -801,6 +817,40 @@
if (e.target.dataset.x !== undefined) T.removeBookmark(bookmarks[i].url);
else T.navigate(bookmarks[i].url);
});
// Drag-reorder chips — same shape as the tab strip. Drop side is chosen
// by whether the pointer is on the left/right half of the target chip.
// dragUrl is the identity we hand to bookmark-move; renderBookmarks
// fires again after the reorder, which rebinds these handlers cleanly.
let dragUrl = null;
box.querySelectorAll(".bm").forEach((row) => {
row.addEventListener("dragstart", (e) => {
dragUrl = row.dataset.url;
try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", dragUrl); } catch {}
row.classList.add("dragging");
});
row.addEventListener("dragend", () => {
row.classList.remove("dragging");
box.querySelectorAll(".bm").forEach((r) => r.classList.remove("dropbefore", "dropafter"));
dragUrl = null;
});
row.addEventListener("dragover", (e) => {
if (!dragUrl || row.dataset.url === dragUrl) return;
e.preventDefault(); e.dataTransfer.dropEffect = "move";
const r = row.getBoundingClientRect();
const before = (e.clientX - r.left) < r.width / 2;
row.classList.toggle("dropbefore", before);
row.classList.toggle("dropafter", !before);
});
row.addEventListener("dragleave", () => row.classList.remove("dropbefore", "dropafter"));
row.addEventListener("drop", (e) => {
e.preventDefault();
const targetUrl = row.dataset.url;
if (!dragUrl || dragUrl === targetUrl) return;
const r = row.getBoundingClientRect();
const before = (e.clientX - r.left) < r.width / 2;
T.moveBookmark && T.moveBookmark(dragUrl, targetUrl, before ? "before" : "after");
});
});
// Backfill: older bookmarks stored before favicon-support have no icon.
// If we're currently on that URL and have a favicon in hand, patch it in
// once so the next render draws it.

64
main.js
View file

@ -1984,15 +1984,51 @@ function installDownloadTracker() {
emitUpdateAvailable();
});
item.once("done", (_ev, state) => {
if (state === "completed") {
updateDownloadPath = item.getSavePath() || dst;
updateDownloadState = "ready";
console.log(`[update] silent fetch complete: ${updateDownloadPath}`);
} else {
if (state !== "completed") {
updateDownloadState = "failed";
console.warn(`[update] silent fetch ${state}`);
emitUpdateAvailable();
return;
}
emitUpdateAvailable();
// NEVER mark "ready" without verifying the file hashes to what the
// manifest promised. Electron's DownloadItem has been observed to
// fire done/completed on truncated payloads (bad Content-Length,
// CDN cache truncation, mid-stream TLS reset the runtime swallowed),
// and 0.3.31's in-app updater then spawned a half-file as setup —
// NSIS integrity check failed silently and the browser was gone.
const savedPath = item.getSavePath() || dst;
const expected = String(updateAvailable && updateAvailable.setupHash || "").toLowerCase();
if (!expected) {
updateDownloadState = "failed";
console.warn(`[update] no manifest hash for ${savedPath} — refusing to arm install`);
try { fs.unlinkSync(savedPath); } catch {}
emitUpdateAvailable();
return;
}
const crypto = require("node:crypto");
const hash = crypto.createHash("sha256");
const rs = fs.createReadStream(savedPath);
rs.on("data", (c) => hash.update(c));
rs.once("error", (e) => {
updateDownloadState = "failed";
console.warn(`[update] hash read failed: ${e.message}`);
try { fs.unlinkSync(savedPath); } catch {}
emitUpdateAvailable();
});
rs.once("end", () => {
const got = hash.digest("hex").toLowerCase();
if (got !== expected) {
updateDownloadState = "failed";
console.warn(`[update] SHA-256 mismatch: got ${got}, want ${expected} — refusing to arm install`);
try { fs.unlinkSync(savedPath); } catch {}
emitUpdateAvailable();
return;
}
updateDownloadPath = savedPath;
updateDownloadState = "ready";
console.log(`[update] silent fetch complete + verified: ${savedPath}`);
emitUpdateAvailable();
});
});
return;
}
@ -3725,6 +3761,22 @@ ipcMain.handle("bookmark-remove", (_e, url) => {
saveBookmarks(); emitBookmarks();
return bookmarks;
});
// Reorder: pull `fromUrl` out of the list and reinsert it before or after
// `targetUrl`. Renderer picks the side by which half of the target chip the
// pointer is on, same convention the tab strip uses. A missing entry or a
// self-drop is a no-op, so noisy drag events don't corrupt the list.
ipcMain.handle("bookmark-move", (_e, fromUrl, targetUrl, place) => {
if (typeof fromUrl !== "string" || typeof targetUrl !== "string" || fromUrl === targetUrl) return bookmarks;
const from = bookmarks.findIndex((b) => b.url === fromUrl);
if (from < 0) return bookmarks;
const [moved] = bookmarks.splice(from, 1);
let to = bookmarks.findIndex((b) => b.url === targetUrl);
if (to < 0) { bookmarks.splice(from, 0, moved); return bookmarks; }
if (place === "after") to += 1;
bookmarks.splice(to, 0, moved);
saveBookmarks(); emitBookmarks();
return bookmarks;
});
ipcMain.handle("settings-get", () => settings);
ipcMain.handle("settings-set", (_e, key, val) => {
if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); }

View file

@ -48,6 +48,7 @@ contextBridge.exposeInMainWorld("theseus", {
addBookmark: (bm) => ipcRenderer.invoke("bookmark-add", bm),
updateBookmark: (url, patch) => ipcRenderer.invoke("bookmark-update", url, patch),
removeBookmark: (url) => ipcRenderer.invoke("bookmark-remove", url),
moveBookmark: (fromUrl, targetUrl, place) => ipcRenderer.invoke("bookmark-move", fromUrl, targetUrl, place),
onBookmarks: (cb) => ipcRenderer.on("bookmarks", (_e, d) => cb(d)),
onNav: (cb) => ipcRenderer.on("nav", (_e, d) => cb(d)),
onTor: (cb) => ipcRenderer.on("tor", (_e, d) => cb(d)),