garrytan / garrytan/gstack

Sidebar tab awareness: tabs.json has empty url/title on non-localhost sites since v1.14 (manifest permission gap)

Open Beginner friendly
#1,256 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
133k
Forks
19.9k
Avg merge
18h 46m
Merged PRs (30d)
26

Description

## Summary

Since `v1.14.0.0` (`ed1e4be2`), the live tab awareness feature ships with a manifest permission gap: the extension can't read `tab.url` / `tab.title` for any site that isn't `127.0.0.1`, so `tabs.json` is written with empty strings and `active-tab.json` stops updating after the first navigation away from `/welcome`.

Net effect: the sidebar Claude Code REPL can't see what tab the user is actually on, and falls back to stale state from initial launch.

## Repro

1. `gstack browse connect` — Chromium opens at `127.0.0.1:34567/welcome`
2. Manually navigate (URL bar or click a link) to any non-localhost page, e.g. `https://news.ycombinator.com`
3. Read state files:
```bash
cat /.gstack/tabs.json
cat /.gstack/active-tab.json
```

### Actual

`tabs.json` updates timestamp + `tabId`, but `url` and `title` are empty:

```json
{
"updatedAt": "2026-04-28T06:59:36.945Z",
"reason": "updated",
"tabs": [{
"tabId": 1927305561,
"url": "",
"title": "",
"active": true,
"windowId": 1927305560,
...
}]
}
```

`active-tab.json` is stale, frozen at the welcome page:

```json
{"tabId":1927305561,"url":"http://127.0.0.1:34567/welcome","title":"GStack Browser"}
```

The Side Panel REPL then reports the user is still on the welcome page, even though the visible Chromium window is on a different site.

### Expected

`tabs.json` and `active-tab.json` should reflect the current real URL / title.

(`browse status` from CLI works fine — the daemon sees the live URL — so this is isolated to the extension → state-file pipeline.)

## Root cause

Permissions, not code. The code path is correct end-to-end:

- `extension/background.js:521` `snapshotTabs()` calls `chrome.tabs.query({})` and uses `t.url || ''` as a fallback.
- `extension/background.js:571` `chrome.tabs.onUpdated` listener fires `pushTabState('updated')`.
- `browse/src/terminal-agent.ts:441` writes `tabs.json` atomically.
- `browse/src/terminal-agent.ts:457` writes `active-tab.json` only if `active.url` is truthy and not `chrome://` / `chrome-extension://`.

The break is at the Chrome boundary: with the current `manifest.json`,

```json
\"permissions\": [\"sidePanel\", \"storage\", \"activeTab\", \"scripting\"],
\"host_permissions\": [\"http://127.0.0.1:*/\", \"ws://127.0.0.1:*/\"]
```

Chrome MV3 returns `tab.url` / `tab.title` only when one of:
- `\"tabs\"` permission is granted (broad), or
- `host_permissions` matches the tab's URL, or
- `activeTab` is granted **and** the user just clicked the extension action on that tab (does not apply to background polling).

For any site outside `127.0.0.1`, none of these hold, so Chrome returns `undefined` for both fields. `snapshotTabs()`'s `|| ''` fallback then writes empty strings, and the active-tab gate (`if (active && active.url && ...)`) silently skips the write.

This means **the feature has never worked outside localhost** since it shipped — not a regression from a later version.

## Git timeline

- `7665adf4` (v0.12.0) — extension introduced; only listened to extension's own messaging, didn't read arbitrary tab URLs.
- `dc0bae82` (v0.12.6.0) — \"sidebar agent uses real tab URL instead of stale Playwright URL\" (related but different code path).
- `ed1e4be2` (v1.14.0.0) — added the `chrome.tabs.onUpdated` listener + `tabs.json` writer for live tab awareness, but the manifest diff only added `\"ws://127.0.0.1:*/\"` for the new PTY channel; tab-related permissions weren't broadened.

## Proposed fix

One line in `extension/manifest.json`. Two reasonable options:

**Option A (minimal scope, recommended)** — add `\"tabs\"` permission:

\`\`\`diff
- \"permissions\": [\"sidePanel\", \"storage\", \"activeTab\", \"scripting\"],
+ \"permissions\": [\"sidePanel\", \"storage\", \"activeTab\", \"scripting\", \"tabs\"],
\`\`\`

The `\"tabs\"` permission is a no-warning install-time grant in MV3 (it's a non-host permission). It exposes `tab.url` / `tab.title` / `tab.favIconUrl` to `chrome.tabs.query()` etc. across all tabs, but does **not** grant content-script injection or cross-origin fetch — so it's a tight scope match for what `snapshotTabs()` actually needs.

**Option B** — broaden host permissions:

\`\`\`diff
- \"host_permissions\": [\"http://127.0.0.1:*/\", \"ws://127.0.0.1:*/\"],
+ \"host_permissions\": [\"http://127.0.0.1:*/\", \"ws://127.0.0.1:*/\", \"\"],
\`\`\`

This works but triggers the install-time \"Read your browsing history on all websites\" warning. Heavier than necessary.

Recommend Option A.

## Test idea

Add an e2e test in the headed-mode harness that:
1. Navigates the Playwright-controlled Chromium to a non-localhost URL (e.g., `https://example.com`).
2. Reads `tabs.json` after a 1s settle.
3. Asserts `tabs[0].url === 'https://example.com/'` and `tabs[0].title` is non-empty.

Without the manifest fix, this test fails with empty strings — guards against the same regression on future MV3 changes.

## Environment

- gstack `v1.15.0.0` (also reproduces by inspection of v1.14.x manifest)
- macOS 25.4.0 (Darwin)
- Chromium launched via `gstack browse connect` (Playwright-managed, headed)

---

Found by reading state files + `extension/manifest.json` + `background.js` + `terminal-agent.ts` after a Side Panel session reported \"can't see current tab.\" Happy to PR if useful.

Contributor guide

Open the contributing guide

Research direction

Start with extension/manifest.json and the snapshotTabs() and chrome.tabs.onUpdated paths in extension/background.js; review how browse/src/terminal-agent.ts writes tabs.json and active-tab.json. Verify non-localhost navigation produces the real URL and title, then run or add the headed-mode e2e check described in the issue and confirm both state files update correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
devtools
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.