2026-09-27 11:24:36 +02:00
|
|
|
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),
|
Theseus: move a tab to a new window
Right-click a tab and pick Move to new window, or drag it out of the tab
bar and let go off the window or over the page. The tab's own page moves
over, so sign-ins, scroll position and typed text stay as they were.
Each window has its own toolbar and tabs. The quick links, side panels
and sidebar stay in the first window and pass to another one if it
closes; the shared pop-ups (site info, downloads, password fill, page
dialogs, approvals) open in whichever window asks. Messages from a
window and events from its tabs run in that window's state, so the
existing single-window code keeps working. Closing a window's last tab
closes that window. The session file keeps every window with its place
on screen, and still opens in older builds as the first window.
2026-10-07 23:08:20 +02:00
|
|
|
// 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),
|
2026-09-27 11:24:36 +02:00
|
|
|
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)),
|
2026-09-30 02:16:11 +02:00
|
|
|
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)),
|
2026-09-27 11:24:36 +02:00
|
|
|
// 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),
|
2026-10-07 19:47:16 +02:00
|
|
|
setChromeOverlay: (h) => ipcRenderer.invoke("set-chrome-overlay", h),
|
2026-09-27 11:24:36 +02:00
|
|
|
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),
|
2026-10-03 14:14:47 +02:00
|
|
|
tabKeepRunning: (id, on) => ipcRenderer.invoke("tab-keep-running", id, on),
|
2026-09-27 11:24:36 +02:00
|
|
|
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"),
|
2026-09-27 11:24:36 +02:00
|
|
|
});
|