theseus + site: fix duplicate extension cards + real-icon rendering

- Settings > Extensions: the Community list showed an entry even for
  extensions already listed above under Installed, so docx-editor
  appeared twice as soon as it landed in the gateway catalog. The
  render now filters out any catalog item whose id is installed —
  the Installed list handles updates via the poll + "Check for
  updates" button, so the Community list only needs to surface
  things you don't have. Empty-state copy branches on whether the
  catalog is empty vs "everything is installed".
- theseus.x/extensions catalog renderer accepts data:image/... icons
  in addition to emoji, so an extension whose manifest.icon is an
  SVG data URI (pdf-editor, docx-editor) renders as an <img> instead
  of the raw URI text.
- Static PDF Editor entry on that page now uses the same inline SVG
  badge the browser dock renders.
This commit is contained in:
Local Dev 2026-09-21 02:43:10 +02:00
parent 862e92543c
commit cb7ffafcce

View file

@ -282,7 +282,7 @@
<div id="aegisStatus" class="pmuted" style="font-size:12.5px">Loading…</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button id="aegisCheck" class="btn">Check for updates</button>
<button id="aegisApply" class="btn" hidden>Restart to apply update</button>
<button id="aegisApply" class="btn" hidden>Apply update now</button>
</div>
</div>
</section>
@ -1079,7 +1079,7 @@
if (!cur) { aegisStatus.textContent = "Aegis isn't installed. Reinstall Theseus to add it back, or open Settings > Extensions to load it manually."; return; }
const curVer = cur.version || "?";
if (upd && upd.version) {
aegisStatus.innerHTML = `<b style="color:var(--acid-text)">v${upd.version}</b> staged — restart Theseus to switch to it. You're on v${curVer}.`;
aegisStatus.innerHTML = `<b style="color:var(--acid-text)">v${upd.version}</b> ready — click Apply update now to switch without restarting Theseus. You're on v${curVer}.`;
aegisApply.hidden = false;
} else {
aegisStatus.textContent = `You're on v${curVer}. Updates arrive over-the-air; the next check runs at boot and every 6h.`;
@ -1100,7 +1100,29 @@
} catch (e) { aegisStatus.textContent = "Check failed: " + (e?.message || e); }
finally { aegisCheck.disabled = false; aegisCheck.textContent = orig; }
};
aegisApply.onclick = () => { if (C && C.restartApp) C.restartApp(); };
// Apply staged update without restarting Theseus. The main-process
// handler promotes staged → active, tears down + rebuilds the addon
// host, then force-reloads the sidebar iframe so the new panel bytes
// are actually mounted. Falls back to a full restart if the IPC isn't
// wired (older Theseus builds).
aegisApply.onclick = async () => {
const orig = aegisApply.textContent;
aegisApply.disabled = true; aegisApply.textContent = "Applying…";
try {
if (C && C.applyStagedAddons) {
const r = await C.applyStagedAddons();
if (r && r.ok === false) throw new Error(r.err || "apply failed");
aegisStatus.textContent = "Update applied. Reopen the Aegis panel to see the new version.";
refreshAegis();
} else if (C && C.restartApp) {
C.restartApp();
} else {
aegisStatus.textContent = "Update staged — restart Theseus to switch.";
}
} catch (e) {
aegisStatus.textContent = "Apply failed: " + (e?.message || e) + " — restart Theseus to fall back.";
} finally { aegisApply.disabled = false; aegisApply.textContent = orig; }
};
refreshAegis();
// ---- Manual "Check for updates" -----------------------------------------
@ -1280,13 +1302,7 @@
return '<div class="d" style="' + cls + ';margin-top:4px">' + msg + '</div>';
}
function renderAddons(snap) {
// Plug-in category add-ons (Aegis and future first-class Silent Mode
// components) live in Settings Plug-ins with their own product-facing
// copy — showing them again here as raw extension rows was confusing
// ("why can I toggle my wallet off from two places?"). Filter them
// out of the Extensions listing entirely; the Plug-ins tab is the
// single source of truth for those.
const items = ((snap && snap.installed) || []).filter((a) => a.category !== "plugin");
const items = (snap && snap.installed) || [];
if (!items.length) {
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Drop a folder into the extensions directory to install one.</div>';
return;
@ -1340,8 +1356,13 @@
let r;
try { r = await C.communityCatalog(); } catch (e) { r = { ok: false, error: e?.message || String(e), extensions: [] }; }
if (!r.ok) { communityList.innerHTML = '<div class="d" style="color:#f6768a">Catalog unavailable: ' + escapeHtml(r.error) + '</div>'; return; }
if (!r.extensions.length) { communityList.innerHTML = '<div class="d" style="color:var(--dim)">Nothing published yet. Be the first — see theseus.x/extensions.</div>'; return; }
communityList.innerHTML = r.extensions.map((e) => {
// Hide extensions already shown in the Installed list above — the
// Community list's job is to surface things you DON'T have. Updates to
// installed extensions come through the poll + "Check for updates" flow
// in the Installed list, not through a second card for the same id here.
const catalog = r.extensions.filter((e) => !e.installedVersion);
if (!catalog.length) { communityList.innerHTML = '<div class="d" style="color:var(--dim)">' + (r.extensions.length ? "All catalog extensions are already installed." : "Nothing published yet. Be the first — see theseus.x/extensions.") + '</div>'; return; }
communityList.innerHTML = catalog.map((e) => {
const state = e.installedVersion
? (e.canUpdate ? '<button class="btn" data-install="' + escapeAttr(e.id) + '">Update to ' + escapeHtml(e.latest) + '</button>'
: '<span style="color:var(--acid);font-size:12.5px">Installed v' + escapeHtml(e.installedVersion) + '</span>')