Skip to content

The extension reloads itself when its files on disk change (#1711) - #1712

Merged
suleimansh merged 1 commit into
mainfrom
1711-extension-self-reload
Aug 26, 2026
Merged

The extension reloads itself when its files on disk change (#1711)#1712
suleimansh merged 1 commit into
mainfrom
1711-extension-self-reload

Conversation

@suleimansh

Copy link
Copy Markdown
Contributor

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).
  • One thing found on the way: with developer mode switched off on chrome://extensions, Chrome (137+) disables an unpacked extension on reload — 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-extension command-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 CDP Extensions.loadUnpacked install 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.js and what it imports, content.js, options.html and options.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, importScripts and the options page load — nothing missing, nothing extra — and unchanged/edited/unreadable behave as above.
  • Specs: fingerprint.SPEC.md (new), background.SPEC.md, the extension SPEC.md, check.SPEC.md; FEATURES-SPEC.md gets 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.js and the Driver's page loaded on /code self-report arriving with no click — posted below when done.

🤖 automated · Fable 5, effort high

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>
@suleimansh

Copy link
Copy Markdown
Contributor Author

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 (onBridgeStatus), UTC; nobody touched chrome://extensions after the reload.

Edit 1 — a comment in content.js, 02:16:09. The next beat saw it and reloaded the extension:

  • 02:16:32 /_bridge/hellorows 0, no transcript rows found: a fresh content script in the Driver tab, loaded by the new worker's first cycle (its page refresh is due at once after a worker start)
  • 02:16:36 /_bridge/hellorows 10 (human,marker,assistant), turns 6, sent 6 of 6 turn(s): the fresh script re-mirrors the whole session (the old one was sending increments, sent 1 of 1)
  • 02:17:58 /_bridge/hellopage loaded on /code

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: page loaded on ${location.pathname}page loaded on ${location.pathname} [self-reloaded #1711], on disk only.

  • 02:19:31 / 02:19:35 — same fresh-script pair as above (the reload)
  • 02:20:29 /_bridge/hellopage loaded on /code [self-reloaded #1711]

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

@suleimansh
suleimansh merged commit 09b5c57 into main Aug 26, 2026
2 checks passed
@suleimansh
suleimansh deleted the 1711-extension-self-reload branch August 26, 2026 02:40
suleimansh added a commit that referenced this pull request Aug 26, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The extension could reload itself when its files change

1 participant