Service worker and messaging

Add a keyboard command

Declare a browser command and handle it in the service worker to open or toggle the current page note.

A keyboard shortcut is a browser event. There’s no popup involved, so it can’t be handled in popup.js. It goes to the service worker.

Declaring the command

Commands live in the manifest under commands. Each one has a name and a suggested key:

{
  "commands": {
    "toggle-page-note": {
      "suggested_key": { "default": "Alt+Shift+N" },
      "description": "Show or hide the note for this page"
    }
  }
}

The name, toggle-page-note, is the contract. That’s the string your code receives. The key is only a suggestion.

Suggested means suggested

The browser or the operating system may already use that combination. Then your command has no key at all. Users can also change it, or remove it, from chrome://extensions/shortcuts.

So never tell users “press Alt+Shift+N” as a fact. Ask Chrome what the current binding is:

const commands = await chrome.commands.getAll()
const toggle = commands.find(c => c.name === 'toggle-page-note')
console.log(toggle.shortcut) // 'Alt+Shift+N' or ''

Show that value in the popup, and when it’s empty, show a link to the shortcuts page instead. A feature the user can’t find doesn’t exist.

Handling it in the worker

Listen with chrome.commands.onCommand. A command is a qualifying gesture for activeTab, so inside the listener we can query the active tab and inject or message it, the same way the popup does.

A toggle also needs to know whether the panel is open. We keep that per tab in chrome.storage.session, which survives a worker restart. A global variable wouldn’t.

chrome.commands.onCommand.addListener(async command => {
  if (command !== 'toggle-page-note') return

  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  if (!tab?.id || !tab.url?.startsWith('http')) return

  const stateKey = `open:${tab.id}`
  const { [stateKey]: isOpen } = await chrome.storage.session.get(stateKey)

  await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] })

  if (isOpen) {
    await chrome.tabs.sendMessage(tab.id, { type: 'HIDE_PAGE_NOTE' })
  } else {
    const key = keyForUrl(tab.url)
    const { [key]: note } = await chrome.storage.local.get(key)
    await chrome.tabs.sendMessage(tab.id, { type: 'SHOW_PAGE_NOTE', note: note ?? '' })
  }

  await chrome.storage.session.set({ [stateKey]: !isOpen })
})

The listener is registered at the top level, and everything it needs is read inside it. Remember, the worker may have just started for this one keypress. No popup ran before it, no global holds the right tab.

The same checks from the popup apply: reject a missing ID and restricted URLs. Injecting before sending covers the tab that still runs an old content script after a reload, because setup is idempotent. And the operation is safe to repeat: two keypresses show and then hide, nothing piles up.

Now test the full set: the assigned key, the command with its key removed from the shortcuts page, a chrome:// page, a command fired a minute after the last one so the worker is cold, and a tab that still runs a content script from before your last reload. Each case should either toggle the panel or fail quietly with a logged reason.

Lesson completed