feat(chrome-extension): open the recorder in Chrome's side panel on non-injectable pages#2027
Conversation
… is not injectable
…eardown, and single-recorder rule
…ailures fall back to the side panel
…abs instead of deferring to a dead flag
…the recorder doc can idle-close
| if (standalonePanelOpen) { | ||
| closeStandalonePanel(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
When Chrome restarts the MV3 service worker while the side panel remains open, standalonePanelOpen resets to false and the surviving panel does not report itself again, so the next icon click calls sidePanel.open() instead of closing the visible recorder.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/background/service-worker.ts
Line: 1903-1906
Comment:
**Side-panel state is lost**
When Chrome restarts the MV3 service worker while the side panel remains open, `standalonePanelOpen` resets to `false` and the surviving panel does not report itself again, so the next icon click calls `sidePanel.open()` instead of closing the visible recorder.
How can I resolve this? If you propose a fix, please make it concise.| const closeStandalonePanel = () => { | ||
| if (!standalonePanelOpen) return; | ||
| standalonePanelOpen = false; | ||
| chrome.runtime.sendMessage( | ||
| { target: "standalone-panel", type: "close" }, | ||
| () => { | ||
| void chrome.runtime.lastError; | ||
| }, | ||
| ); |
There was a problem hiding this comment.
If the service worker gets restarted while the side panel is open, standalonePanelOpen can desync and this early return prevents teardown/toggle from reaching the panel. Might be safer to always send the close message and just keep the flag best-effort.
| const closeStandalonePanel = () => { | |
| if (!standalonePanelOpen) return; | |
| standalonePanelOpen = false; | |
| chrome.runtime.sendMessage( | |
| { target: "standalone-panel", type: "close" }, | |
| () => { | |
| void chrome.runtime.lastError; | |
| }, | |
| ); | |
| const closeStandalonePanel = () => { | |
| standalonePanelOpen = false; | |
| chrome.runtime.sendMessage( | |
| { target: "standalone-panel", type: "close" }, | |
| () => { | |
| void chrome.runtime.lastError; | |
| }, | |
| ); | |
| }; |
| if (windowId !== undefined && chrome.sidePanel) { | ||
| await chrome.sidePanel.open({ windowId }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Minor robustness: if the side panel opens successfully but the standalone page never manages to ping standalone-panel-opened (race/suspension), standalonePanelOpen stays false and toggle/teardown can miss it. Consider setting the flag on successful open() as well.
| if (windowId !== undefined && chrome.sidePanel) { | |
| await chrome.sidePanel.open({ windowId }); | |
| return; | |
| } | |
| if (windowId !== undefined && chrome.sidePanel) { | |
| await chrome.sidePanel.open({ windowId }); | |
| standalonePanelOpen = true; | |
| return; | |
| } |
| chrome.sidePanel.open({ windowId: tab.windowId }).then( | ||
| () => undefined, | ||
| (error) => { | ||
| console.warn("sidePanel.open failed, using popup window", error); | ||
| return openRecorderPanel(tab); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Same idea in the click-path: setting the flag on success makes the toggle behavior less dependent on the standalone page sending its lifecycle message.
| chrome.sidePanel.open({ windowId: tab.windowId }).then( | |
| () => undefined, | |
| (error) => { | |
| console.warn("sidePanel.open failed, using popup window", error); | |
| return openRecorderPanel(tab); | |
| }, | |
| ); | |
| chrome.sidePanel.open({ windowId: tab.windowId }).then( | |
| () => { | |
| standalonePanelOpen = true; | |
| }, | |
| (error) => { | |
| console.warn("sidePanel.open failed, using popup window", error); | |
| return openRecorderPanel(tab); | |
| }, | |
| ); |
| if (standalonePanelOpen) { | ||
| closeStandalonePanel(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
One edge case: on a cold-started MV3 worker, refreshStandalonePanelFlag() runs via callback, so this click can hit the standalonePanelOpen check before the flag is rebuilt. That makes the first click after SW restart re-open/focus the already-open panel instead of toggling it closed.
| } | |
| if (!standalonePanelOpen) { | |
| try { | |
| standalonePanelOpen = await new Promise<boolean>((resolve) => { | |
| chrome.runtime.getContexts( | |
| { | |
| contextTypes: [ | |
| "SIDE_PANEL", | |
| "TAB", | |
| ] as chrome.runtime.ContextType[], | |
| documentUrls: [chrome.runtime.getURL(POPUP_URL)], | |
| }, | |
| (contexts) => { | |
| if (chrome.runtime.lastError) return resolve(false); | |
| resolve((contexts ?? []).length > 0); | |
| }, | |
| ); | |
| }); | |
| } catch { | |
| // best-effort | |
| } | |
| } | |
| if (standalonePanelOpen) { | |
| closeStandalonePanel(); | |
| return; | |
| } |
Follow-up to the "recorder opens in a separate window" confusion from the support thread.
On pages the extension can't inject into (new tab, chrome://, Web Store), the Cap icon now docks the recorder in Chrome's side panel instead of a floating popup window. It survives tab switches, the icon toggles it, and normal websites keep the in-page panel exactly as shipped. The popup window stays as a logged last resort.
Implementation notes: sidePanel.open() must ride the click gesture (any timer voids it), so the decision happens synchronously in the action handler and the fallback delivery path avoids timed retries. The standalone panel reports open/closed to the service worker so teardown reaches it and only one recorder shows at a time.
Also includes two bug fixes found while testing, both reproducible on main (can split into a separate PR):
Tested manually on Chrome/macOS (side panel open/toggle/persistence, normal sites unchanged incl. the cap.so dashboard flow, recording from the side panel, orphan recovery after reload). Typecheck, unit tests, and builds pass. sidePanel needs Chrome 116 = existing minimum_chrome_version. Firefox has no sidePanel API; the Firefox port handles this separately.
Greptile Summary
This PR moves the standalone Chrome-extension recorder into the side panel and adds recovery for stale content-script and offscreen recording state.
Confidence Score: 4/5
This PR should not merge until side-panel toggling remains correct across routine MV3 service-worker suspension.
The visible side panel can outlive the service worker that owns its unpersisted open flag, causing the next icon click to reopen rather than close the recorder.
apps/chrome-extension/src/background/service-worker.ts and apps/chrome-extension/src/popup/main.tsx
Important Files Changed
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(chrome-extension): sweep stale overl..." | Re-trigger Greptile