theseus/preload.js

138 lines
9.4 KiB
JavaScript
Raw Normal View History

const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("theseus", {
navigate: (input) => ipcRenderer.invoke("navigate", input),
search: (q) => ipcRenderer.invoke("search", q),
newTab: () => ipcRenderer.invoke("new-tab"),
closeTab: (id) => ipcRenderer.invoke("close-tab", id),
switchTab: (id) => ipcRenderer.invoke("switch-tab", id),
moveTab: (id, targetId, place) => ipcRenderer.invoke("move-tab", id, targetId, place),
// A tab dragged out of the bar and dropped where nothing took it: main opens
// it in a new window if the point (screen coordinates) is off the toolbar.
detachTab: (id, x, y) => ipcRenderer.invoke("tab-detach", id, x, y),
suggestAddress: (query, rect) => ipcRenderer.invoke("suggest-address", query, rect),
closeAddressPicker: () => ipcRenderer.invoke("close-address-picker"),
addressCursor: (dir) => ipcRenderer.invoke("address-cursor", dir),
togglePwFill: (rect) => ipcRenderer.invoke("toggle-pw-fill", rect),
onPwAvailability: (cb) => ipcRenderer.on("pw-availability", (_e, d) => cb(d)),
// Update chip: chrome subscribes to update-available; clicking the chip's
// download button opens the URL in the system browser; ✕ dismisses for
// the current session.
onUpdateAvailable: (cb) => ipcRenderer.on("update-available", (_e, d) => cb(d)),
// Signed extension updates staged by the background check: { staged:
// [{id,name,version}] }. They apply on the next launch, so the chrome
// offers a restart. stagedAddons() asks once at boot.
onAddonUpdates: (cb) => ipcRenderer.on("addon-updates", (_e, d) => cb(d)),
stagedAddons: () => ipcRenderer.invoke("addons-list-staged"),
applyStagedAddons: () => ipcRenderer.invoke("addons-apply-staged"),
restartApp: () => ipcRenderer.invoke("app-restart"),
openUpdateDownload: (url) => ipcRenderer.invoke("open-update-download", url),
installUpdateNow: () => ipcRenderer.invoke("install-update-now"),
dismissUpdate: () => ipcRenderer.invoke("dismiss-update"),
goHome: () => ipcRenderer.invoke("go-home"),
back: () => ipcRenderer.invoke("go-back"),
forward: () => ipcRenderer.invoke("go-forward"),
// Press-and-hold / right-click on Back or Forward: native menu of the
// tab's history entries in that direction; picking one jumps to it.
navHistoryMenu: (dir, rect) => ipcRenderer.invoke("nav-history-menu", dir, rect),
reload: (hard) => ipcRenderer.invoke("reload", !!hard),
stop: () => ipcRenderer.invoke("stop"),
toggleTor: () => ipcRenderer.invoke("toggle-tor"),
openSettings: (section) => ipcRenderer.invoke("open-settings", section),
getSettings: () => ipcRenderer.invoke("settings-get"),
setSetting: (key, val) => ipcRenderer.invoke("settings-set", key, val),
onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)),
systemLocale: () => ipcRenderer.invoke("system-locale"),
websiteLanguageMenu: (rect) => ipcRenderer.invoke("website-language-menu-popup", rect),
Theseus: in-page translator (LibreTranslate client) The Website-language setting only tells servers what the user prefers via Accept-Language — many static sites (including names on BCDN) serve one language and ignore it, so e.g. hello.bch loads in English for every user, in every language. This adds a translator that converts the page's visible text in place, so a Lithuanian user reads hello.bch in Lithuanian without asking the server for anything. The URL-bar grows a translate chip next to the website-language globe. The chip lights up when the page's declared `<html lang>` differs from the user's preferred language. Click it once to translate in place; click again to revert — originals are kept in a renderer-local state slot and swapped back without a reload. Right-click opens the chip menu (change target / translator settings). The engine lives behind a swappable adapter in main — this ships with the LibreTranslate backend (POST /translate with {q, source, target, format}). The endpoint defaults to the LibreTranslate public tier but is settable in Settings › General › Translate pages, so a user with a self-hosted LibreTranslate (or Silent Mode's own translate.silentmode.st once it is up) swaps it there without a code change. On-device Bergamot (the WASM engine Firefox Translations uses) will plug into the same adapter in a later release — same contract (array of texts in, array of translations out), the chip and revert path are already engine-agnostic. The fetch goes through session.defaultSession.fetch so Tor and add-on proxy rules apply uniformly, chunks the batch at ~3.8 KB per POST so a large page spreads across several requests, times each one out at 45 s, and reports a failure to the chip's tooltip so a dead endpoint reads as such and not as a silent no-op. The injected walker skips SCRIPT / STYLE / CODE / PRE / NOSCRIPT / TEXTAREA and contentEditable subtrees, keeps a reference to each text node and the original text, and reverts by restoring from that pair.
2026-10-03 15:01:40 +02:00
// Page translator chip: click translates to the user's language, click
// again reverts. The chip shows the target code; its "on" state is driven
// by the translated flag arriving via onPageTranslateState.
pageTranslate: (target) => ipcRenderer.invoke("page-translate", target),
pageTranslateRevert: () => ipcRenderer.invoke("page-translate-revert"),
pageTranslateState: () => ipcRenderer.invoke("page-translate-state"),
pageTranslateMenu: (rect) => ipcRenderer.invoke("page-translate-menu-popup", rect),
onPageTranslateState: (cb) => ipcRenderer.on("page-translate-state", (_e, d) => cb(d)),
// Find-in-page. main.js fires 'find-open' on Ctrl+F; chrome renderer
// owns the bar UI and drives findInPage / stopFindInPage via these
// wrappers. Match count / active ordinal comes back through onFindResult.
onFindOpen: (cb) => ipcRenderer.on("find-open", () => cb()),
findInPage: (query, opts) => ipcRenderer.invoke("find-in-page", query, opts || {}),
findStop: () => ipcRenderer.invoke("find-stop"),
onFindResult: (cb) => ipcRenderer.on("find-result", (_e, d) => cb(d)),
toggleSiteInfo: (rect) => ipcRenderer.invoke("toggle-site-info", rect),
switchToBcnr: () => ipcRenderer.invoke("switch-to-bcnr"),
setChromeHeight: (h) => ipcRenderer.invoke("set-chrome-height", h),
setChromeOverlay: (h) => ipcRenderer.invoke("set-chrome-overlay", h),
getSearchEngines: () => ipcRenderer.invoke("search-engines"),
setSearchEngine: (id) => ipcRenderer.invoke("set-search-engine", id),
addEngine: (eng) => ipcRenderer.invoke("add-engine", eng),
removeEngine: (id) => ipcRenderer.invoke("remove-engine", id),
toggleEnginePicker: (rect) => ipcRenderer.invoke("toggle-engine-picker", rect),
onEngines: (cb) => ipcRenderer.on("engines", (_e, d) => cb(d)),
getBookmarks: () => ipcRenderer.invoke("bookmarks-get"),
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)),
onTabs: (cb) => ipcRenderer.on("tabs", (_e, d) => cb(d)),
tabReload: (id) => ipcRenderer.invoke("tab-reload", id),
tabDuplicate: (id) => ipcRenderer.invoke("tab-duplicate", id),
tabMute: (id, on) => ipcRenderer.invoke("tab-mute", id, on),
tabKeepRunning: (id, on) => ipcRenderer.invoke("tab-keep-running", id, on),
tabGroup: (id, color) => ipcRenderer.invoke("tab-group", id, color),
tabGroupToggle: (color) => ipcRenderer.invoke("tab-group-toggle", color),
tabBookmark: (id) => ipcRenderer.invoke("tab-bookmark", id),
onAddressPicked: (cb) => ipcRenderer.on("address-picked", (_e, url) => cb(url)),
onBcnrOffer: (cb) => ipcRenderer.on("bcnr-offer", (_e, d) => cb(d)),
// Collision-mode (BCNR ↔ ICANN) live switcher for the active tab
collisionSwitch: (arg) => ipcRenderer.invoke("collision-switch", arg),
collisionState: () => ipcRenderer.invoke("collision-state"),
// Downloads — the toolbar button subscribes to `downloads` to update its
// badge, and toggleDownloads opens/closes the floating panel.
getDownloads: () => ipcRenderer.invoke("downloads-get"),
toggleDownloads: (rect) => ipcRenderer.invoke("toggle-downloads", rect),
onDownloads: (cb) => ipcRenderer.on("downloads", (_e, d) => cb(d)),
// Add-on sidebar: toggle, open on a specific panel, close, or subscribe
// to state (visibility + which panel is active + the panel list). The
// extension dock in chrome.html renders one button per panel and calls
// openSidebar / closeSidebar accordingly.
toggleSidebar: () => ipcRenderer.invoke("sidebar-toggle"),
openSidebar: (panelId) => ipcRenderer.invoke("sidebar-open", panelId),
closeSidebar: () => ipcRenderer.invoke("sidebar-close"),
sidebarState: () => ipcRenderer.invoke("sidebar-state"),
onSidebarState: (cb) => ipcRenderer.on("sidebar-state", (_e, d) => cb(d)),
// Extension dock: drag-reorder (key = "p:<panelId>" | "m:<addonId>") and
// the right-click menu (native; main owns the actions).
dockMove: (key, targetKey, place) => ipcRenderer.invoke("dock-move", key, targetKey, place),
dockMenu: (key, rect) => ipcRenderer.invoke("dock-menu", key, rect),
// Toolbar-menu (dropdown from an add-on's dock icon): dispatch the picked
// item id to the add-on's "menu-select" handler.
addonMenuSelect: (addonId, itemId) => ipcRenderer.invoke("addon-menu-select", addonId, itemId),
// Pop the dock-icon dropdown as a NATIVE menu. Renderer-side DOM popovers
// get clipped by chrome.html's own WebContentsView height (CHROME_H) and
// then covered by the tab view below it — a native Menu.popup escapes that
// layering entirely. Pass the button's viewport-rect so main can anchor.
toolbarMenuPopup: (addonId, rect) => ipcRenderer.invoke("toolbar-menu-popup", addonId, rect),
tabContextMenuPopup: (tabId, rect) => ipcRenderer.invoke("tab-context-menu-popup", tabId, rect),
// Ariadne's Thread menu behind the registry button at the end of the
// address bar (BCDN/ICANN switch, remembered choices, policy, settings).
registryMenuPopup: (rect) => ipcRenderer.invoke("registry-menu-popup", rect),
// Page zoom on the active tab: step +1 / -1 along Chrome's ladder, or reset to 100 %.
zoomStep: (dir) => ipcRenderer.invoke("zoom-step", dir),
zoomReset: () => ipcRenderer.invoke("zoom-reset"),
// Install-as-app chip in the address bar: main decides between the
// install dialog and the open/remove menu (anchored at rect).
webappChip: (rect) => ipcRenderer.invoke("webapp-chip", rect),
// Chrome listens so it can drop the "active" tint when the native menu
// dismisses (Esc, outside click, item picked — main fires all three).
onToolbarMenuClosed: (cb) => ipcRenderer.on("toolbar-menu-closed", () => cb()),
Theseus: Claude connector, an opt-in MCP server for the signed-in profile Claude sessions could not work inside the user's own profile: the desktop app's browser lost synthetic pastes and typing in Medium's editor, and a browser-wide --remote-debugging-port would hand every session to any local process. The connector is a built-in extension, off until turned on in Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install token and drives one tab at a time through that tab's webContents.debugger. - Host: a "tab-control" capability (built-in add-ons only) for listing, opening, navigating, closing and focusing tabs, handing out web tabs' webContents (never Settings, extension or home tabs), and a toolbar chip plus tab outline while a client is connected; the chip's X stops it. Built-in add-ons can ship "defaultEnabled": false. - Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot, read_page_text, snapshot (accessibility tree with refs), click, type (insertText + real Enter), press_key, paste_html / paste_text (system clipboard + webContents.paste, clipboard restored), upload_file (DOM.setFileInputFiles), wait_for, and evaluate behind its own switch. - Safety: first use of each site asks the user; password and one-time-code fields are refused; uploads only from folders the user added; requests from web pages or foreign Host headers are refused; every action is logged with a timestamp in the panel and actions.log. - Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node mode. - Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a throwaway profile that types, pastes HTML with a heading and a link, and uploads a PNG on a local page.
2026-10-23 17:04:03 +03:00
// Claude connector indicator: {connected, busy, clients, tabIds}. The chip
// opens the connector's panel; its ✕ stops the connector.
agentControlState: () => ipcRenderer.invoke("agent-control-state"),
onAgentControl: (cb) => ipcRenderer.on("agent-control", (_e, d) => cb(d)),
agentControlOpen: () => ipcRenderer.invoke("agent-control-open"),
agentControlStop: () => ipcRenderer.invoke("agent-control-stop"),
});