Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

# Bolt CWS listing private key (predetermines extension ID) — never commit
bolt-key.pem
5 changes: 5 additions & 0 deletions extension/BOLT-LISTING-ID.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Bolt Chrome Extension — new Chrome Web Store listing identity
Extension ID (predetermined via bolt-key.pem): ndndcckllfokpejkgnecbjpaplbbgmip
Store URL (after publish): https://chromewebstore.google.com/detail/bolt-chrome-extension/ndndcckllfokpejkgnecbjpaplbbgmip
First upload: the built ZIP includes bolt-key.pem (as key.pem) at the extension root so Google locks in this exact ID.
Keep bolt-key.pem private (gitignored). Needed only to reproduce this ID for a fresh first-upload; not needed for CWS updates.
5 changes: 4 additions & 1 deletion extension/icons/GENERATED-FROM-APP-ICON.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
Generated from resources/icons/app.png.
Generated by scripts/build-browser-extension-brand-assets.mjs.

These are the Bolt lightning-bolt drawn as an outline (idle = white),
tinted per connection state. They are NOT derived from app.png.

Do not hand-edit icon-black-*.png, icon-gray-*.png, icon-blue-*.png, icon-green-*.png, or icon-red-*.png.
Regenerate with: pnpm run extension:assets
Binary file modified extension/icons/icon-black-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-black-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-black-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-black-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-blue-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-blue-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-blue-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-blue-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-gray-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-gray-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-gray-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-gray-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-green-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-green-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-green-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-green-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-red-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-red-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-red-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icons/icon-red-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"manifest_version": 3,
"name": "Interpreter Chrome Extension",
"name": "Bolt Chrome Extension",
"version": "0.0.103",
"description": "Connect Interpreter to your existing Chrome tabs without launching a separate browser.",
"description": "Connect Bolt to your existing Chrome tabs without launching a separate browser.",
"permissions": [
"debugger",
"scripting",
Expand All @@ -16,7 +16,7 @@
"type": "module"
},
"action": {
"default_title": "Click to connect Interpreter",
"default_title": "Click to connect Bolt",
"default_icon": {
"16": "icons/icon-gray-16.png",
"32": "icons/icon-gray-32.png",
Expand Down
101 changes: 87 additions & 14 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,59 @@ import { handleGhostBrowserCommand, type GhostBrowserCommandParams } from 'playw

const RELAY_HOST = '127.0.0.1'
const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988
// Desktop overlay's summon listener (relay port + 1). When the user toggles a
// tab to the connected state we ping this so the overlay launches immediately,
// independent of relay/CDP state syncing.
const OVERLAY_SUMMON_PORT = RELAY_PORT + 1

function summonOverlay(tab?: chrome.tabs.Tab): void {
// Fire-and-forget; the desktop app may not be running, which is fine. Include
// the connected tab's URL/title so the desktop can decide the permission card
// without depending on relay state (which is empty when no agent is driving).
void fetch(`http://${RELAY_HOST}:${OVERLAY_SUMMON_PORT}/summon`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: tab?.url ?? null, title: tab?.title ?? null }),
signal: AbortSignal.timeout(2000),
}).catch(() => {})
}

// Keep the desktop overlay's input placeholder ("What's next in the <tab> tab?")
// live: the relay's cached tab state only refreshes on window-focus changes, so
// push the focused window's active tab whenever it changes (in-Chrome tab
// switches, title/url updates, window focus). Pushed directly (no setTimeout —
// MV3 can suspend the worker before a short timer fires) and de-duped by
// url+title; the desktop only applies it while the overlay targets a browser.
//
// IMPORTANT: prefer the tab object carried by the event (onActivated's tabId,
// onUpdated's tab) over re-querying `{active:true}`. A query inside onActivated
// can resolve against stale state and return the tab being *left*, which — with
// the de-dup — makes the placeholder lag one switch behind.
let lastActiveTabPushKey = ''
async function pushTab(tab: chrome.tabs.Tab | null | undefined): Promise<void> {
if (!tab) {
return
}
const key = `${tab.url ?? ''}\u0000${tab.title ?? ''}`
if (key === lastActiveTabPushKey) {
return
}
lastActiveTabPushKey = key
await fetch(`http://${RELAY_HOST}:${OVERLAY_SUMMON_PORT}/active-tab`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: tab.url ?? null, title: tab.title ?? null }),
signal: AbortSignal.timeout(2000),
}).catch(() => {})
}
async function pushActiveTabNow(): Promise<void> {
try {
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
await pushTab(tab)
} catch {
// best-effort only
}
}

type NavigatorWithUaData = Navigator & {
userAgentData?: {
Expand Down Expand Up @@ -170,7 +223,7 @@ class ConnectionManager {
}

if (store.getState().connectionState === 'extension-replaced') {
throw new Error('Another Interpreter Chrome Extension is already connected')
throw new Error('Another Bolt Chrome Extension is already connected')
}

// Reuse in-progress connection attempt - prevents races between user clicks and maintain loop
Expand Down Expand Up @@ -501,7 +554,7 @@ class ConnectionManager {
store.setState({
tabs: new Map(),
connectionState: 'extension-replaced',
errorText: 'Another Interpreter Chrome Extension took over the connection',
errorText: 'Another Bolt Chrome Extension took over the connection',
})
return
}
Expand All @@ -511,7 +564,7 @@ class ConnectionManager {
store.setState({
tabs: new Map(),
connectionState: 'extension-replaced',
errorText: 'Another Interpreter Chrome Extension is actively in use',
errorText: 'Another Bolt Chrome Extension is actively in use',
})
return
}
Expand Down Expand Up @@ -617,7 +670,7 @@ class ConnectionManager {
if (error.message === 'Extension Already In Use') {
store.setState({
connectionState: 'extension-replaced',
errorText: 'Another Interpreter Chrome Extension is actively in use',
errorText: 'Another Bolt Chrome Extension is actively in use',
})
} else {
store.setState({ connectionState: 'idle' })
Expand Down Expand Up @@ -3023,7 +3076,7 @@ async function connectTab(tabId: number): Promise<void> {
// Extension in use: set global 'extension-replaced' state to enter polling mode
const isExtensionInUse =
error.message === 'Extension Already In Use' ||
error.message === 'Another Interpreter Chrome Extension is already connected'
error.message === 'Another Bolt Chrome Extension is already connected'

const isWsError =
error.message === 'Server not available' ||
Expand All @@ -3038,7 +3091,7 @@ async function connectTab(tabId: number): Promise<void> {
return {
tabs: newTabs,
connectionState: 'extension-replaced',
errorText: 'Another Interpreter Chrome Extension is actively in use',
errorText: 'Another Bolt Chrome Extension is actively in use',
}
})
} else if (isWsError) {
Expand Down Expand Up @@ -3120,7 +3173,8 @@ async function resetDebugger(): Promise<void> {

// Our extension IDs - allow attaching to our own extension pages for debugging
const OUR_EXTENSION_IDS = [
'bboaaphdpllilofamfpommlbafpellnb', // Production extension (Chrome Web Store)
'ndndcckllfokpejkgnecbjpaplbbgmip', // Bolt production extension (Chrome Web Store)
'bboaaphdpllilofamfpommlbafpellnb', // Interpreter production extension (legacy, kept for transition)
'pebbngnfojnignonigcnkdilknapkgid', // Dev extension (stable ID from manifest key)
]

Expand Down Expand Up @@ -3153,7 +3207,7 @@ const icons = {
'48': '/icons/icon-green-48.png',
'128': '/icons/icon-green-128.png',
},
title: 'This tab is connected to Interpreter - Click to disconnect',
title: 'This tab is connected to Bolt - Click to disconnect',
badgeText: '',
badgeColor: [64, 64, 64, 255] as [number, number, number, number],
},
Expand All @@ -3164,7 +3218,7 @@ const icons = {
'48': '/icons/icon-gray-48.png',
'128': '/icons/icon-gray-128.png',
},
title: 'Waiting for the local Interpreter relay. Start the app, then try again.',
title: 'Waiting for the local Bolt relay. Start the app, then try again.',
badgeText: '...',
badgeColor: [64, 64, 64, 255] as [number, number, number, number],
},
Expand All @@ -3175,7 +3229,7 @@ const icons = {
'48': '/icons/icon-blue-48.png',
'128': '/icons/icon-blue-128.png',
},
title: 'Connecting this tab to Interpreter...',
title: 'Connecting this tab to Bolt...',
badgeText: '...',
badgeColor: [64, 64, 64, 255] as [number, number, number, number],
},
Expand All @@ -3186,7 +3240,7 @@ const icons = {
'48': '/icons/icon-black-48.png',
'128': '/icons/icon-black-128.png',
},
title: 'Interpreter is ready. Click to connect this tab.',
title: 'Bolt is ready. Click to connect this tab.',
badgeText: '',
badgeColor: [64, 64, 64, 255] as [number, number, number, number],
},
Expand All @@ -3208,7 +3262,7 @@ const icons = {
'48': '/icons/icon-red-48.png',
'128': '/icons/icon-red-128.png',
},
title: 'Another Interpreter Chrome Extension connected - Click to retry',
title: 'Another Bolt Chrome Extension connected - Click to retry',
badgeText: '!',
badgeColor: [220, 38, 38, 255] as [number, number, number, number],
},
Expand Down Expand Up @@ -3282,6 +3336,14 @@ async function onTabRemoved(tabId: number): Promise<void> {

async function onTabActivated(activeInfo: chrome.tabs.TabActiveInfo): Promise<void> {
store.setState({ currentTabId: activeInfo.tabId })
// Use the authoritative tab id from the event (not a query, which can race and
// return the tab being left — causing a one-switch lag in the placeholder).
try {
const tab = await chrome.tabs.get(activeInfo.tabId)
void pushTab(tab)
} catch {
// tab may have been closed; ignore
}
}

async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
Expand All @@ -3302,6 +3364,7 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
if (connectionState === 'extension-replaced') {
logger.debug('Clearing extension-replaced state, attempting to reconnect')
store.setState({ connectionState: 'idle', errorText: undefined })
summonOverlay(tab)
await connectTab(tab.id)
return
}
Expand All @@ -3320,6 +3383,7 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
if (tabInfo?.state === 'connected') {
await disconnectTab(tab.id)
} else {
summonOverlay(tab)
await connectTab(tab.id)
}
}
Expand All @@ -3333,7 +3397,7 @@ chrome.contextMenus
.finally(() => {
chrome.contextMenus?.create({
id: 'playwriter-pin-element',
title: 'Copy Interpreter Element Reference',
title: 'Copy Bolt Element Reference',
contexts: ['all'],
visible: false,
})
Expand Down Expand Up @@ -3417,8 +3481,17 @@ checkMemory()
chrome.tabs.onRemoved.addListener(onTabRemoved)
chrome.tabs.onActivated.addListener(onTabActivated)
chrome.action.onClicked.addListener(onActionClicked)
chrome.tabs.onUpdated.addListener(() => {
chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
void updateIcons()
// Keep the overlay placeholder current when the active tab's title/url changes.
// Use the tab object the event provides (authoritative, no query race).
if (tab?.active && (changeInfo.title !== undefined || changeInfo.url !== undefined)) {
void pushTab(tab)
}
})
chrome.windows.onFocusChanged.addListener(() => {
// Switching windows (or back to Chrome) changes the focused active tab.
void pushActiveTabNow()
})

chrome.contextMenus?.onClicked.addListener(async (info, tab) => {
Expand Down
14 changes: 7 additions & 7 deletions extension/src/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Interpreter Chrome Extension</title>
<title>Bolt Chrome Extension</title>
<style>
:root {
--bg: #ffffff;
Expand Down Expand Up @@ -159,10 +159,10 @@
<body>
<main>
<section class="intro">
<p class="label">Interpreter Chrome Extension</p>
<h1>Use Interpreter with the tab you choose.</h1>
<p class="label">Bolt Chrome Extension</p>
<h1>Use Bolt with the tab you choose.</h1>
<p class="lead">
Open the site you want, then click the extension on that tab. Interpreter will ask before taking
Open the site you want, then click the extension on that tab. Bolt will ask before taking
browser actions that need permission.
</p>
</section>
Expand All @@ -171,22 +171,22 @@ <h1>Use Interpreter with the tab you choose.</h1>
<li>
<span class="number">1</span>
<div>
<strong>Open Interpreter</strong>
<strong>Open Bolt</strong>
<p>Keep the app running while you use the extension.</p>
</div>
</li>
<li>
<span class="number">2</span>
<div>
<strong>Open the site you want to use</strong>
<p>Go to the page you want Interpreter to see and use.</p>
<p>Go to the page you want Bolt to see and use.</p>
</div>
</li>
<li>
<span class="number">3</span>
<div>
<strong>Click the extension on that tab</strong>
<p>That connects the current tab to Interpreter. Click it again to turn that same tab off.</p>
<p>That connects the current tab to Bolt. Click it again to turn that same tab off.</p>
</div>
</li>
</ol>
Expand Down
3 changes: 2 additions & 1 deletion playwriter/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { fileURLToPath } from 'node:url'

// Extension IDs used for validation and Chrome flag commands.
export const EXTENSION_IDS = [
'bboaaphdpllilofamfpommlbafpellnb', // Production (Chrome Web Store)
'ndndcckllfokpejkgnecbjpaplbbgmip', // Bolt (Chrome Web Store) — current production listing
'bboaaphdpllilofamfpommlbafpellnb', // Interpreter (Chrome Web Store) — legacy, kept trusted for transition
'pebbngnfojnignonigcnkdilknapkgid', // Dev extension (stable ID from manifest key)
]

Expand Down
Binary file modified store-assets/chrome-store-screenshot-1280x800.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified store-assets/chrome-store-screenshot-640x400.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified website/public/favicon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified website/public/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified website/public/logo-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified website/public/logo-512.png
Binary file modified website/public/screenshot@2x.png