fix(extension): add "tabs" permission so live tab awareness works on non-localhost sites#1257
fix(extension): add "tabs" permission so live tab awareness works on non-localhost sites#1257fredchu wants to merge 1 commit intogarrytan:mainfrom
Conversation
…non-localhost sites Without "tabs", chrome.tabs.query() returns tab objects with undefined url/title for any site outside host_permissions (everything except 127.0.0.1). snapshotTabs() in background.js then writes empty strings into tabs.json, and the active-tab.json gate (`if (active && active.url && ...)`) silently skips the write. The sidebar agent loses track of what page the user is actually on. The "tabs" permission is a no-warning install-time grant in MV3 (not a host permission). It exposes tab.url / tab.title / tab.favIconUrl across all tabs, but does not grant content-script injection or cross-origin fetch — a tight scope match for what snapshotTabs() actually needs. activeTab is too narrow (only after a user gesture on the extension action) for background polling. Reproduces on every gstack version since v1.14.0.0 (ed1e4be), where the chrome.tabs.onUpdated listener was added but the manifest only gained `ws://127.0.0.1:*/` for the new PTY channel — tab-related permissions were never broadened. Adds a manifest source-level test in sidebar-tabs.test.ts that asserts `permissions` contains "tabs", to guard against future regressions. Closes garrytan#1256
End-to-end verification on a real installVerified the fix on a v1.15.0.0 install (live extension at Red phase (manifest reverted to upstream/main)``` (fail) manifest: live tab awareness needs "tabs" permission > permissions includes "tabs" 27 pass Green phase (fix restored)``` 28 pass Live browser e2e (the part that matters)Before the fix, with browser on ```json // .gstack/active-tab.json — stale, never updated past welcome After applying the manifest fix +
// .gstack/active-tab.json — gate now passes because url is truthy
Both the initial-state path and the live Note for reviewerState files are written only when Side Panel + Terminal tab are open — the relay chain is |
Summary
\"tabs\"permission toextension/manifest.jsonsochrome.tabs.query()returns realurl/titlefor tabs outside127.0.0.1.browse/test/sidebar-tabs.test.ts.Closes #1256.
Why
Since
v1.14.0.0(ed1e4be2introduced live tab awareness),tabs.jsonhas been written with emptyurl/titlestrings for any user-visited site outside localhost, andactive-tab.jsonhas stopped updating after the first non-localhost navigation. The sidebar Claude Code REPL can't see what tab the user is actually on, and falls back to stale state.Repro:
gstack browse connecthttps://news.ycombinator.comcat <repo>/.gstack/tabs.json—url/titleare emptycat <repo>/.gstack/active-tab.json— still pinned to/welcome(
browse statusfrom the CLI works fine — the daemon sees the live URL — so the break is isolated to the extension → state-file pipeline.)Root cause
The code path is correct end-to-end. The break is at the Chrome boundary.
extension/background.js:snapshotTabs()(L521) — callschrome.tabs.query({})and tolerates missing fields witht.url || ''/t.title || ''.chrome.tabs.onUpdatedlistener (L571) — firespushTabState('updated')on URL/title changes.browse/src/terminal-agent.ts:tabs.jsonwrite.active-tab.jsonwrite only ifactive.urlis truthy and notchrome:///chrome-extension://.In Manifest V3,
chrome.tabs.query()returnstab.url/tab.titleonly when one of:\"tabs\"permission is granted, orhost_permissionsmatches the tab's URL, oractiveTabis granted and the user just clicked the extension action on that tab (does not apply to background polling).Current
manifest.json:```json
"permissions": ["sidePanel", "storage", "activeTab", "scripting"],
"host_permissions": ["http://127.0.0.1:/", "ws://127.0.0.1:/"]
```
For any site outside
127.0.0.1, none of these hold → Chrome returnsundefinedfor both fields →snapshotTabs()writes empty strings →active-tab.jsongate skips silently.Why
\"tabs\"and not\"<all_urls>\"host permissionBoth fix the symptom, but
\"tabs\"is the tighter scope match:\"tabs\"permission\"<all_urls>\"host permissiontab.url/tab.titletochrome.tabs.*snapshotTabs()only needs the metadata, not injection / fetch —\"tabs\"is the minimum sufficient grant. No new install warnings, no cross-origin attack surface.Tests
```
$ bun test browse/test/sidebar-tabs.test.ts
28 pass
0 fail
```
Without the manifest fix, the new test fails:
```
expect(received).toContain(expected)
Expected to contain: "tabs"
Received: ["sidePanel", "storage", "activeTab", "scripting"]
```
Test plan
browse/test/sidebar-tabs.test.tssuite still passes (28/28).main(without the permission), passes on this branch.gstack browse connect, navigate to a non-localhost URL, confirmtabs.jsonshows the real URL andactive-tab.jsonupdates.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmithwith what you need.