# Two weekends into a Chrome side panel: the four state bugs that took longer than the UI

> Source: <https://dev.to/cwsaibuddy/two-weekends-into-a-chrome-side-panel-the-four-state-bugs-that-took-longer-than-the-ui-3m1j>
> Published: 2026-07-11 03:17:47+00:00

I shipped the first public build of a Chrome extension two weekends ago. The marketing-ready UI took me about six hours. The four state bugs below took me the rest of those two weekends, plus parts of the following week.

I am writing this down because every reviewer of "I built an X in Y hours" posts seems to skip the state-model half, and the state-model half is where the actual time goes.

A sidebar that lives in Chrome's side panel API. You highlight text or screenshot a region on any page, the sidebar lets you pick a destination AI tab (ChatGPT / Claude / Gemini / a custom one) and forwards the content with a small wrapper prompt.

That is the whole product description. The interesting part is what happens when a user does it twice.

First failure I caught: user has two ChatGPT tabs open, one workspace, one personal. The extension forwards to whichever tab was last focused. The user sees the message arrive in the workspace, replies there, then realizes the context they wanted to capture is on the personal tab.

Fix: every AI destination registers a stable tab id at extension boot, not at click time. The forwarding logic walks the registry, not the focused window. Took a morning to redesign, an afternoon to migrate existing flows.

Lesson: tab identity is not the same as window focus. Chrome's `chrome.tabs.query({active: true})`

returns the active tab. The active tab is not necessarily the destination the user has in their head.

User takes a screenshot of a code block, opens the sidebar, hits "annotate", drags a red box around lines 12-15, hits send. The annotation worked. But the underlying screenshot bytes were captured at the moment the toolbar first appeared, before the user could draw the box.

Fix: the sidebar cannot trust that the screenshot in memory is the screenshot the user is looking at. Either re-capture on every annotation event, or maintain a single source-of-truth region object that the canvas and the network layer both read from.

This is the kind of bug that does not crash, does not log anything, and silently degrades the experience. It took me three separate user reports to isolate it because the screenshots "looked right" on visual inspection.

I added a feature where every successful forward gets logged to a "prompt library" the user can re-run later. The library entry stored the prompt text the user typed in the sidebar. But what actually got sent to the AI was that text plus the wrapped source content plus the auto-injected screenshot.

When the user re-ran the library entry three days later, they got a completely different response because the page content had changed.

Fix: the library entry has to store either (a) the exact network payload that was sent, or (b) the resolved source content at the moment of send. Storing the user's pre-wrap text is a lie.

I went with (b). The user can still edit the wrapper prompt, but the source content is fixed at the moment they hit send. This is closer to how browser history works than how chat history works.

Chrome's side panel survives the user closing it as a popup. The user closes it. Comes back tomorrow. The state is still there, except the destination tab from yesterday is gone (user closed that AI tab too).

Fix: every state object needs a "still valid" check at panel open, not just at click time. If a registered AI tab id no longer exists, drop it from the registry and show a "this destination is no longer connected" placeholder.

The temptation is to write a tab-existence check once and never revisit it. The actual lesson is that you have to write it again every time you add a new state field.

If you read the four bugs as a list, they share a pattern: state lives in more places than the user thinks it does, and the gap between "where the user expects it to live" and "where the code actually stores it" is where the bugs hide.

For me the meta-fix was a rule: every state field has exactly one writer and one reader. If two components both read the same field, that is a sign one of them should be reading through the other. This rule did not stop me from violating it, but it shortened the time between "I introduced a bug" and "I see which rule I broke".

If you are building a browser extension that talks to AI tabs, the UI is the easy part. The state model is the part.

*Built with the Chrome Side Panel API, plain DOM, and a lot of console.log in content scripts.*
