From 15dfde50cc7be9084843f94847a62da23a8c7cad Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 2 Nov 2026 16:53:20 +0000 Subject: [PATCH] feat(pwa): CoinPay, LogicSRC and TronBrowser installed by default + new-tab Apps widget - tron pwa add/remove/installed: put pwamart apps in the app drawer as a .desktop entry running `tron --app=` (icon fetched, https only, % escaped per the desktop spec). sync leaves these entries alone. - tron pwa defaults: CoinPay, LogicSRC, TronBrowser once per profile; the launcher runs it in the background on every start. A removed default stays removed; TRONBROWSER_NO_DEFAULT_APPS=1 skips it. - tron pwa host/register-host: native messaging host dev.tronbrowser.pwa, registered in /NativeMessagingHosts for the unpacked extension id (no root). - New-tab "Apps" strip: Profullstack's apps from pwamart.com, defaults pinned first, filter box, one tap = install (or Open once installed). Without the host (macOS/Windows/Tor) it opens the app in its own window. Why not the engine's own installer: on ungoogled-chromium 152 the PWA DevTools domain is absent, navigator.install is undefined, --policy is ignored and chrome.management.generateAppForLink makes a tab-only hosted app. Verified headless on the real engine: the widget tap wrote tronbrowser-pwa-coinpay.desktop plus its icon through the native host. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/extensions/ai-sidebar/apps.js | 62 +++ .../extensions/ai-sidebar/manifest.json | 3 +- apps/desktop/extensions/ai-sidebar/newtab.css | 17 + .../desktop/extensions/ai-sidebar/newtab.html | 10 + apps/desktop/extensions/ai-sidebar/newtab.js | 54 +++ apps/desktop/launcher/tron-pwa | 368 +++++++++++++++++- apps/desktop/launcher/tronbrowser | 6 + apps/desktop/test/pwa-apps.test.ts | 158 ++++++++ apps/web/public/install.sh | 8 +- 9 files changed, 683 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/extensions/ai-sidebar/apps.js create mode 100644 apps/desktop/test/pwa-apps.test.ts diff --git a/apps/desktop/extensions/ai-sidebar/apps.js b/apps/desktop/extensions/ai-sidebar/apps.js new file mode 100644 index 00000000..cf6ebd45 --- /dev/null +++ b/apps/desktop/extensions/ai-sidebar/apps.js @@ -0,0 +1,62 @@ +// New-tab "Apps" widget: Profullstack's apps on pwamart.com, one tap to install. +// +// Installing goes through `tron pwa` over native messaging (host +// dev.tronbrowser.pwa, registered by the launcher inside the profile). It puts +// the app in the system app drawer as a standalone TronBrowser window, which is +// the only real install the engine allows us without root. Where the host is +// not there (macOS, Windows, a Tor profile) the button opens the app in its own +// window instead, and says so. + +import { fetchWithTimeout } from './net.js'; + +export const PWAMART = 'https://pwamart.com'; +export const PUBLISHER = 'profullstack'; +export const HOST = 'dev.tronbrowser.pwa'; +// Pinned first, and installed for every new profile by the launcher. +export const DEFAULT_APPS = ['coinpay', 'logicsrc', 'tronbrowser']; +const TTL = 6 * 60 * 60 * 1000; + +export function host(msg) { + return new Promise((resolve) => { + try { + chrome.runtime.sendNativeMessage(HOST, msg, (reply) => { + const err = chrome.runtime.lastError; + resolve(err ? { ok: false, error: err.message, noHost: true } : reply || { ok: false, error: 'no reply' }); + }); + } catch (e) { + resolve({ ok: false, error: String(e), noHost: true }); + } + }); +} + +// Defaults first (in their order), then the rest by pwamart's own order. +export function orderApps(apps) { + const rank = (a) => { + const i = DEFAULT_APPS.indexOf(a.slug); + return i === -1 ? DEFAULT_APPS.length : i; + }; + return apps + .filter((a) => a && /^[a-z0-9][a-z0-9-]{0,58}[a-z0-9]$/.test(a.slug) && /^https:\/\//.test(a.start_url || a.url || '')) + .map((a, i) => ({ a, i })) + .sort((x, y) => rank(x.a) - rank(y.a) || x.i - y.i) + .map(({ a }) => a); +} + +export async function loadApps() { + const { pwamartApps } = await chrome.storage.local.get('pwamartApps'); + if (pwamartApps && Date.now() - pwamartApps.at < TTL) return pwamartApps.apps; + try { + const res = await fetchWithTimeout(`${PWAMART}/api/v1/publishers/${PUBLISHER}`, {}, 10000); + // A 402 (pwamart's bot gate answers headless browsers with x402) or any + // other failure must not be cached as "no apps" for the next six hours. + if (!res.ok) throw new Error(`pwamart ${res.status}`); + const data = await res.json(); + const apps = orderApps(data.apps || []).map((a) => ({ + slug: a.slug, name: a.name, summary: a.summary || '', icon: a.icon || '', start_url: a.start_url || a.url, + })); + if (apps.length) await chrome.storage.local.set({ pwamartApps: { at: Date.now(), apps } }); + return apps; + } catch { + return pwamartApps ? pwamartApps.apps : []; + } +} diff --git a/apps/desktop/extensions/ai-sidebar/manifest.json b/apps/desktop/extensions/ai-sidebar/manifest.json index 1f453476..748ebb49 100644 --- a/apps/desktop/extensions/ai-sidebar/manifest.json +++ b/apps/desktop/extensions/ai-sidebar/manifest.json @@ -22,7 +22,8 @@ "notifications", "alarms", "contentSettings", - "declarativeNetRequestWithHostAccess" + "declarativeNetRequestWithHostAccess", + "nativeMessaging" ], "host_permissions": [ "https://api.openai.com/*", diff --git a/apps/desktop/extensions/ai-sidebar/newtab.css b/apps/desktop/extensions/ai-sidebar/newtab.css index 859d2b8d..6a45ae4e 100644 --- a/apps/desktop/extensions/ai-sidebar/newtab.css +++ b/apps/desktop/extensions/ai-sidebar/newtab.css @@ -37,6 +37,23 @@ body { border-radius: 10px; padding: 14px 16px; white-space: pre-wrap; font-size: 13px; color: var(--fg); } .motd-body a { color: var(--cyan); text-decoration: none; } +/* Apps widget */ +.apps-q { margin-left: auto; background: #04060c; color: var(--fg); border: 1px solid var(--line); + border-radius: 6px; padding: 3px 10px; font: inherit; font-size: 12px; width: min(220px, 40vw); } +.apps-q:focus { outline: none; border-color: var(--cyan); } +.apps-note { color: var(--muted); font-size: 12px; margin: 8px 0 0; } +.appgrid { margin-top: 12px; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); + max-height: 340px; overflow-y: auto; } +.app { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 8px; + display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; min-width: 0; } +.app.def { border-color: color-mix(in srgb, var(--cyan) 45%, var(--line)); } +.app img, .app-noicon { width: 40px; height: 40px; border-radius: 10px; object-fit: contain; background: #04060c; } +.app b { font-size: 12px; color: #fff; font-weight: 600; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.app button { background: var(--cyan); color: #04060c; border: 0; border-radius: 999px; padding: 3px 12px; + font: inherit; font-size: 12px; font-weight: 700; cursor: pointer; } +.app.on button { background: transparent; color: var(--cyan); border: 1px solid var(--line); } +.app button:disabled { opacity: .5; cursor: progress; } + /* Embeddable player modal */ .modal { position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,.82); display: grid; place-items: center; } .modal[hidden] { display: none; } diff --git a/apps/desktop/extensions/ai-sidebar/newtab.html b/apps/desktop/extensions/ai-sidebar/newtab.html index 5db90b91..d52da80f 100644 --- a/apps/desktop/extensions/ai-sidebar/newtab.html +++ b/apps/desktop/extensions/ai-sidebar/newtab.html @@ -39,6 +39,16 @@ + +