The extension reloads itself when its files on disk change (#1711) - #1712
Conversation
Every edit to the unpacked extension needed a click on chrome://extensions before Chrome re-read its files. The worker now fingerprints the extension's own files (every file Chrome loads for it) when it starts and again each beat, and calls chrome.runtime.reload() when any changed — never mid-cycle, recorded as the last cycle's outcome. The fingerprint lives in a plain script the worker imports and check.mjs pins: the watched list equals what the manifest, importScripts and the options page load; one edit names its file; an unreadable file refuses the fingerprint rather than counting as a change, so a transient read failure cannot loop. Measured 2026-08-26 in Chrome for Testing 150: fetch(chrome.runtime.getURL(f)) from the worker returns the current disk file, and reload() boots a new worker on the edited files. With developer mode off, Chrome 137+ disables an unpacked extension on reload instead; the README says to leave it on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Dogfood, on the real extension in Suleiman's Chrome, after one last manual reload (the one that put this branch's worker in place). Every timestamp below is the daemon's own ( Edit 1 — a comment in
That alone does not tell a reloaded extension from a reloaded page — the worker reloads the Driver page once a minute anyway — so: Edit 2 — the hello note itself, 02:19:08:
A string that existed only on disk reached the daemon 81 seconds after it was written: one beat (≤30 s) to notice and reload, then the Driver's next page load. Both edits are reverted; the revert is itself a third reload, which is the feature working as intended. 🤖 automated · Fable 5, effort high |
* The daemon runs its own browser for the bridge (#1332) Option A from the spike, on top of the Driver tab: a preference makes the daemon launch a Chrome for Testing of its own — downloaded once into ~/.the-framework-browser, a persistent profile, the extension installed over CDP (Extensions.loadUnpacked, which also grants its site access), developer mode switched on so #1712's self-reload keeps working, the daemon URL and bridge token seeded into the extension's storage, the pinned Driver tab opened on claude.ai's session list, and the window minimized. Headed because Cloudflare rejects headless; minimized because macOS clamps any off-screen position back onto the screen, and minimized passes. Settings gets the switch and a line: the launch step (download percentage included), running with Show/Hide for the one-time sign-in — the daemon reads its own tab's URL to say a sign-in is needed, since the bridge's last hello may be the user's own Chrome's — and stopped with the reason plus Restart. A browser a dead daemon left holding the profile is stopped first; the daemon's shutdown closes the browser. With two Drivers possible (the user's Chrome and this browser), /_bridge/answer now claims the answer on read for 90 s, so a session served by both is never answered twice. * The Driver overlay stands aside on claude.ai's sign-in page (#1332) The overlay covered the sign-in page too, so the one thing a person has to do in the daemon's bridge browser — sign in, once — was impossible. On /login and /logout the overlay is taken down and not drawn; it returns with the next page. Harness case added. * The bridge browser keeps its cookie key out of the OS keychain (#1332) macOS asked for the login password on every launch of Chrome for Testing ("Chromium Safe Storage"), which an unattended browser can never answer. --use-mock-keychain and --password-store=basic, as Puppeteer launches: the profile directory's permissions guard the sign-in instead. * Settings: one bridge switch, then which browser does the work (#1332) "Browser bridge" and "Bridge browser" as two toggles read as anagrams. There is one feature and one real decision — which browser drives claude.ai — so the page shows the bridge switch and, under it, a two-way choice: a browser the daemon runs (recommended, with its status and window controls) or your own Chrome (with the token panel). Same boolean preference underneath.
Closes #1711.
Every change to the unpacked extension needed a click on chrome://extensions before Chrome re-read its files (#1703, #1709, #1710 each went through that click). Now the extension reloads itself: the service worker takes a fingerprint of the extension's own files when it starts and again every beat (30 s), and calls
chrome.runtime.reload()when any of them changed on disk — never while a cycle is running, and recorded as the last cycle's outcome naming the changed files. The Driver tab's content script is orphaned by the reload as by a manual one, and the next cycle's page load replaces it.The spike, measured 2026-08-26 in Chrome for Testing 150 with a throwaway extension (unattended, no click):
fetch(chrome.runtime.getURL('probe.js'))from the worker returns the current disk file, same worker, plain fetch — no cache to defeat. Option 1 from the issue, as recommended.chrome.runtime.reload()from the worker boots a new worker on the edited files (verified by the new worker's baseline hash being the edited file's).DISABLE_UNSUPPORTED_DEVELOPER_EXTENSION— instead of reloading it, and only a click there brings it back. Developer mode is on for anyone who loaded the extension unpacked; the README now says to leave it on. (A--load-extensioncommand-line install is disabled the same way regardless, so the daemon-owned Chrome for Testing of Spike: Is it possible to make the extension headless? #1332 will need the CDPExtensions.loadUnpackedinstall plus developer mode on for this to work there.)What changed
fingerprint.js(new): the watched files — every file Chrome loads for the extension:manifest.json,background.jsand what it imports,content.js,options.htmlandoptions.js— hashed (FNV-1a) and compared; a file that cannot be read fails the fingerprint rather than counting as a change, so a transient read failure (a file mid-write, a checkout mid-switch) cannot reload in a loop.background.js: the alarm now runs a beat — the file check, then the cycle. A beat landing on a running cycle does nothing; the next one checks again. The reload is noted as the last cycle's outcome (reloading the extension: content.js changed on disk).check.mjs: two cases, written red first and break-checked three ways (an always-empty comparison, an unreadable file counted as a change, a file dropped from the watched list — each turned the right case red): the watched list equals what the manifest,importScriptsand the options page load — nothing missing, nothing extra — and unchanged/edited/unreadable behave as above.fingerprint.SPEC.md(new),background.SPEC.md, the extensionSPEC.md,check.SPEC.md;FEATURES-SPEC.mdgets the feature; the README's "reload the extension on chrome://extensions" paragraph is rewritten.Not covered here: the user's own claude.ai tabs still run the old content script until they reload — as after a manual reload; only the Driver tab is the extension's to reload.
Dogfood: one last manual reload to get this worker running, then an edit to a comment in
content.jsand the Driver'spage loaded on /codeself-report arriving with no click — posted below when done.🤖 automated · Fable 5, effort high