~14 min · clipdeck, mode-toggle, badge, per-tab, storage
Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Track 5 ends with a small, honest power: the user decides when ClipDeck is paying attention. Pause it on the page where they're typing their tax return; un-pause it the moment they're back on the article. State lives in storage so every surface stays in sync."
The Why
Users do not always want ClipDeck active. Privacy reasons, surprise-clip avoidance, or just the comfort of knowing the extension is off while they fill out a sensitive form. The pause toggle is the bar of trust:
Per-tab scope so they don't have to remember to re-enable.
Persistent across SW evictions so the pause does not silently reset.
Visible in the badge so they can tell at a glance.
Reachable from popup, keyboard shortcut, and (optionally) the side panel.
The State Shape
A simple array in chrome.storage.local:
{ pausedTabs: number[] }
Tab IDs aren't durable across browser restarts (Chrome assigns new ones), but the pause is meaningful only while the tab still exists, so this is fine — when the tab closes, the id is gone and a fresh tab with the same id starts un-paused.
A daily-ish cleanup pass (or one on chrome.runtime.onStartup) removes stale ids by intersecting against chrome.tabs.query({}). Keeps the array from growing forever.
The Three Touch Points
Wherever the pause state matters, read the same storage value:
Popup button. Reads current tab's pause state, toggles, writes back, lets storage.onChanged update the badge and content script.
Keyboard shortcut. Same logic, triggered from chrome.commands.onCommand with command toggle-pause (we wired this in Lesson 4).
Content script gate. Before the capture flow, content script checks storage; if paused, it responds with a no-op and the SW records nothing.
The SW also writes to the badge each time pause state changes for the active tab — setBadgeText({ tabId, text: 'II' }) when paused, empty when un-paused.
The Race-Free Update
Reading then writing the array invites a race when two events fire close together (user hits hotkey while clicking the popup button). The defensive pattern is a tiny mutation helper that always re-reads inside the same transaction:
async function togglePauseForTab(tabId) {
const { pausedTabs = [] } = await chrome.storage.local.get('pausedTabs');
const next = pausedTabs.includes(tabId)
? pausedTabs.filter((t) => t !== tabId)
: [...pausedTabs, tabId];
await chrome.storage.local.set({ pausedTabs: next });
return next.includes(tabId);
}
This is not strictly atomic — between the get and the set, another event could insert — but for human-rate toggles it's effectively fine. If ClipDeck ever grows multi-window sync, swap to a proper locking pattern with a sequence id.
Visual Feedback
The badge is your honest signal. Three states:
Active + clips today → number ("3") on the brand color.
Active + no clips today → empty badge.
Paused → "II" or "PAUSE" on a grey background.
Switch between count-mode and pause-mode whenever the storage changes. The popup also flips its button label: "Pause on this site" ↔ "Resume on this site," and the side panel can show a banner if the user is browsing a paused tab ("Capture paused. Resume?").
Wrapping Track 5
With this lesson ClipDeck has the full action surface:
Icon with badge that doubles as today's-count + pause indicator.
Popup with stats, three primary actions, and a discoverable settings link.
Right-click context menu with the actual selected text in the label.
Four user-bindable keyboard shortcuts.
Omnibox search keyword clip.
Per-tab pause/resume reflected everywhere.
Track 6 takes the next sensible step — the permission model. Why ClipDeck asks for what it asks for, how to ask only at the moment you need it, and how to design for the user who says no.
Pause is the user's trust dial. Per-tab scope, persisted in storage, mirrored to the badge and popup. Three surfaces, one state, no surprises.
chrome.storage.session as an alternative. Chrome 102+ ships chrome.storage.session, an in-memory storage area that dies when the browser closes. For pause state that should reset across browser restarts (a user expectation in some cases), session is a clean fit. ClipDeck v1 uses local so pause survives restarts, but the choice is yours — neither is wrong, they encode different defaults.
Code
background.js — toggle + badge refresh + tab close cleanup·javascript
Add the first code block to clipdeck/background.js — the togglePauseForTab helper, per-tab badge refresh, command wiring, and tab-close cleanup. Add the second code block to clipdeck/popup.js — the Pause button handler and live label. Add the togglePause message router (the comment at the bottom of the second code block) to background.js. Replace your existing save-clip handler with the gated version from the third code block. Reload. Open the popup on any page — Pause on this tab. Click it — badge changes to II on grey, label flips to Resume on this tab. Press Ctrl+Shift+K — nothing saves. Click Resume on this tab — badge restores, save-clip works again. Switch tabs — the badge reflects the per-tab state of each one independently. Close the paused tab and reopen the same URL — state is fresh (un-paused), as designed.
Hint
If the badge does not change to II when you pause, you forgot to await chrome.action.setBadgeText — it's a Promise. If togglePause from the popup throws Receiving end does not exist, your SW listener for togglePause messages isn't registered (the commented router at the bottom of the second code block — add it). If save-clip still saves while paused, the SW-side gate is not in your handler — find the existing chrome.commands.onCommand listener for save-clip and add the pause check at the top of it.
Progress
Progress is local-only — sign in to sync across devices.