Skip to content

Migrate to Manifest V3#10

Open
kmbys wants to merge 3 commits into
ento:masterfrom
kmbys:manifest-v3
Open

Migrate to Manifest V3#10
kmbys wants to merge 3 commits into
ento:masterfrom
kmbys:manifest-v3

Conversation

@kmbys

@kmbys kmbys commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Chrome has been phasing out Manifest V2 extensions, so this PR migrates both the premade IME and the build tooling to Manifest V3. All changes have been verified on a ChromeOS device (see Testing below).

Changes

Service worker migration

  • Replaced background.scripts with a single service worker (remapper/background.js) that loads the existing scripts via importScripts. The script files themselves needed no changes.
  • The script list the build tooling needs for stacking IMEs now lives in remapper/background_scripts.json, keeping manifest.json free of MV2-only keys (and free of the "'background.scripts' requires manifest version of 2 or lower" warning).

Surviving service worker termination

MV3 service workers are terminated after ~30s of inactivity, which broke remapping in two ways, each needing its own fix:

  • State loss: contextId and lastFocusedWindowUrl are now persisted to chrome.storage.session and restored on worker startup, since onFocus doesn't fire again when a worker is revived by a key event alone. This adds the storage permission.
  • Missed key events: ChromeOS doesn't wait for a terminated worker to wake up before dispatching a key event, so keys pressed while the worker was asleep fell through to the browser's default handling (state restoration alone can't help with this). The worker now keeps itself alive with a cheap extension API call every 20s while the IME is active — starting on onActivate (and on focus/key events as a fallback) and stopping on onDeactivated, so the worker can still be reclaimed while the IME isn't in use.

crosh blacklist fix

Found while testing this PR, though it's a pre-existing issue: ChromeOS moved crosh from the crosh extension to a System Web App at chrome-untrusted://crosh/, so the hardcoded extension URL no longer matched and keys were being remapped inside crosh. The blacklist now matches URL prefixes and includes the crosh SWA and the Terminal app (which wants raw keys for the same reason).

Build tooling

  • wscript now emits an MV3 manifest plus a generated top-level background.js that importScripts each stacked IME's scripts, remapper first.
  • An IME's scripts are discovered from background_scripts.json next to its manifest, from background.scripts (MV2 fallback IMEs, unchanged), or from background.service_worker (MV3 fallback IMEs).

Documentation

README notes the MV3 architecture and one new limitation: fallback IMEs whose background scripts rely on DOM APIs (window, document, …) won't work inside a service worker.

Testing

On a ChromeOS device, with the remapper directory loaded unpacked:

  • Extension loads without errors or manifest warnings.
  • All keybindings in keymap.js work in a regular text field; unmapped keys pass through.
  • With a text field focused and the IME active, the service worker stays alive past the 30s idle timeout and remapping keeps working (previously keys reverted to browser defaults, e.g. C-a became select-all).
  • After switching to another IME and letting the worker die, switching back revives it and remapping works.
  • In crosh, keys are no longer remapped (verified with M-b, whose shell behavior — backward-word — is distinguishable from its remapping), while remapping continues to work in regular text fields.

Build tooling was verified by building both a standalone remapper and a remapper combined with a fallback MV2 IME: generated manifests are valid MV3, the generated background.js loads scripts in the right order, and the chrome.input.imeRemapper.hijack rewrite still applies.

https://claude.ai/code/session_01J8HdQQm7honRLVYMNkeqiR

kmbys added 3 commits July 12, 2026 10:45
- Replace background.scripts with a service worker (background.js)
  that loads the existing scripts via importScripts. The script list
  the build tooling needs lives in a separate background_scripts.json,
  keeping the manifest free of MV2-only keys.
- Persist contextId and lastFocusedWindowUrl to chrome.storage.session
  so key events that revive a terminated service worker can still be
  remapped (onFocus does not fire again on revival). This adds the
  storage permission.
- Make wscript emit an MV3 manifest plus a generated background.js
  service worker for combined IMEs. An IME's scripts are found in
  background_scripts.json next to its manifest, under
  background.scripts (MV2), or as background.service_worker (MV3).
ChromeOS doesn't wait for a terminated MV3 service worker to wake up
before dispatching a key event: keys pressed while the worker is asleep
fall through to the browser's default handling, so restoring state from
chrome.storage.session on revival isn't enough on its own.

Prevent the worker from idling out while the IME is in use by calling a
cheap extension API every 20 seconds, which resets Chrome's 30-second
idle timer. The keepalive starts on onActivate (and on focus/key events,
the wake-up paths when the worker died mid-activation) and stops on
onDeactivated so the worker can still be reclaimed while the IME isn't
active.
ChromeOS moved crosh from the crosh extension to a System Web App at
chrome-untrusted://crosh/, so the hardcoded extension URL no longer
matches and keys were being remapped inside crosh. Match known URL
prefixes instead of exact URLs, and add the crosh SWA and the Terminal
app, which wants raw keys for the same reason.
@ento

ento commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Thaks for the PR. Just so I can decide how to answer questions I have about the changes: Are you familiar with Chrome extension development and just used Claude for convenience, or did you rely on Claude to come up with solutions?

@kmbys

kmbys commented Jul 22, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look, and fair question. To be upfront, I'm not so experienced in Chrome extension development and I relied on Claude to come up with most of the solutions.

That said, all the testing was done by me on an actual ChromeOS device, and it did shape the changes: the initial version only persisted state to chrome.storage.session, but my testing showed keys still fell through to the browser's default handling after the service worker idled out (e.g. C-a became select-all), which is what led to the keepalive approach. I also found during testing that the hardcoded crosh URL no longer matches now that crosh is a System Web App.

For any questions I can't answer shortly, I'll dig in and follow up, so replies might just take me a little longer. And if you'd rather take the changes in a different direction, I'm happy to iterate — and feel free to push changes directly if that's easier (I've enabled maintainer edits). If the PR feels too large, I can also split it up; the crosh URL fix in particular is really an independent fix and could go in its own PR.

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.

2 participants