Skip to content

feat(tab-awareness): detect active tab URL and gate live data in side panel [LCO-38] - #32

Merged
DevanshuNEU merged 3 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/lco-38-tab-awareness-gate
Apr 11, 2026
Merged

feat(tab-awareness): detect active tab URL and gate live data in side panel [LCO-38]#32
DevanshuNEU merged 3 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/lco-38-tab-awareness-gate

Conversation

@DevanshuNEU

@DevanshuNEU DevanshuNEU commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Problem

The side panel is window-scoped. When the user switches from a claude.ai tab to Gmail, GitHub, or any other page, the Usage Budget card continues showing the last-seen session percentages with no visual indicator that the data is stale. Two gaps:

  1. No URL gate: budget was never cleared on tab switch.
  2. No centralized check: every future live-data feature would need to independently handle the "not on Claude" case.

Solution

isTabOnClaude(tabId) -- the single gate

Exported from useDashboardData.ts. Takes a tab ID, calls chrome.tabs.get, checks the URL against CLAUDE_DOMAIN. Returns false on any error (tab closed, no permission, undefined URL). Every future live-data feature calls this before loading.

isClaudeTab: boolean in DashboardData

Added to the hook's return type with a JSDoc comment explaining the intended usage pattern for future features (pre-submit estimates, delta tracking, etc.).

Budget cleared on non-Claude tab

  • init(): gates loadBudget() call on onClaude
  • onTabActivated: clears budget immediately when switching to a non-Claude tab; reloads when switching back
  • onStorageChanged: guards budget reload with isClaudeTabRef -- prevents background alarms from silently re-populating the card while the user is on a non-Claude tab
  • onTabUpdated: new listener for URL changes within the same tab (navigating away without switching tabs -- onTabActivated does not fire for this)
  • onTabRemoved: clears isClaudeTab and budget when the tracked tab is closed

Historical data untouched

today and conversations are org-scoped and always visible, on Claude and non-Claude tabs alike.

Stale async resolution guard

onTabActivated is async (needs to await isTabOnClaude()). If the user switches tabs faster than the API resolves, the stale result is discarded: if (tabIdRef.current !== info.tabId) return.

App.tsx

Subtle banner between Today and Usage Budget sections when !isClaudeTab:

"Open a Claude conversation to see live usage data"

Usage Budget card receives budget={null} (already handled by the hook) and renders its existing empty state.

CSS

.lco-dash-not-claude-banner: muted, italic, no border, no icon. Informs without alarming. Respects prefers-reduced-motion.

Files changed

File Change
entrypoints/sidepanel/hooks/useDashboardData.ts isTabOnClaude(), isClaudeTab state + ref, gate in all loaders, onTabUpdated listener
entrypoints/sidepanel/App.tsx Destructure isClaudeTab, render not-Claude banner
entrypoints/sidepanel/dashboard.css .lco-dash-not-claude-banner styles
tests/unit/tab-awareness.test.ts 15 new tests covering all URL and error paths

Test results

26 test files, 598 tests -- all pass
bun run compile -- clean
bun run build -- clean

Acceptance criteria

  • Switching to a non-claude.ai tab: Usage Budget shows empty state, banner appears
  • Switching back to claude.ai tab: Usage Budget reloads with fresh data, banner disappears
  • Today and History remain visible on both claude.ai and non-claude tabs
  • Active Conversation shows "No active conversation" on non-claude tabs (existing behavior)
  • isTabOnClaude() is the single gate -- no inline URL checks scattered across loaders
  • All existing tests pass; 15 new tests for the URL gate
  • bun run compile + bun run build clean

Summary by CodeRabbit

  • New Features

    • Shows an informational banner when the active tab is not claude.ai.
    • Live usage budget is gated to the claude.ai tab and will clear when you switch away; the usage card now handles empty/null states.
  • Style

    • Banner styling added with reduced-motion respect for accessibility.
  • Tests

    • Added unit tests validating precise tab-detection for claude.ai and non-claude pages.

… panel [LCO-38]

- Export isTabOnClaude() from useDashboardData -- the single URL gate for all
  live-data loaders; takes a tabId, calls chrome.tabs.get, returns false on
  any error or non-Claude URL
- Add isClaudeTab: boolean to DashboardData; defaults false until init resolves
- Clear budget on non-Claude tab; keep today and conversations (historical)
- Guard onStorageChanged budget reload with isClaudeTabRef so background alarms
  cannot re-populate the budget card while the user is on Gmail
- Add onTabUpdated listener for URL changes within the same tab (navigate away
  from claude.ai without switching tabs -- onTabActivated never fires for this)
- Stale-check in onTabActivated: discard async resolution if user switched again
- Clear isClaudeTab and budget on tab close
- App.tsx: render not-Claude banner between Today and Usage Budget when !isClaudeTab
- dashboard.css: add .lco-dash-not-claude-banner (muted, italic, no border)
- 15 new tests in tests/unit/tab-awareness.test.ts covering all URL and error paths
@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6b60e9f-253c-4c83-9e0a-a57194f5d11a

📥 Commits

Reviewing files that changed from the base of the PR and between 549ecec and 7d88292.

📒 Files selected for processing (1)
  • entrypoints/sidepanel/hooks/useDashboardData.ts

📝 Walkthrough

Walkthrough

The PR adds tab-awareness to the sidepanel dashboard: it detects whether the active Chrome tab is on claude.ai, gates live usage budget loading/subscriptions to Claude tabs, clears budget when off-Claude, and shows a conditional "not-Claude" banner when the active tab is not claude.ai.

Changes

Cohort / File(s) Summary
Tab-awareness Logic
entrypoints/sidepanel/hooks/useDashboardData.ts
Adds exported isTabOnClaude(tabId) and isClaudeTab in DashboardData. Loads/clears budget based on tab host, guards storage-change reloads with isClaudeTabRef, adds async chrome.tabs.onActivated with staleness checks, and chrome.tabs.onUpdated handling.
Conditional Banner UI
entrypoints/sidepanel/App.tsx
Destructures isClaudeTab from useDashboardData() and conditionally renders a non-Claude informational banner when !isClaudeTab; UsageBudgetCard remains present but may receive null budget.
Banner Styling
entrypoints/sidepanel/dashboard.css
Adds .lco-dash-not-claude-banner styling (muted/italic/centered with transition) and prefers-reduced-motion override.
Tab-awareness Tests
tests/unit/tab-awareness.test.ts
New Vitest suite validating isTabOnClaude() for exact claude.ai host matches (various paths), non-matches/subdomains/substring traps, undefined URLs, chrome.tabs.get rejection cases, and correct tabId passthrough (including 0).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ChromeAPI as Chrome Tabs API
    participant Hook as useDashboardData
    participant App as App Component

    User->>ChromeAPI: Activate or navigate tab
    ChromeAPI->>Hook: onActivated / onUpdated event (tabId)
    Hook->>ChromeAPI: chrome.tabs.get(tabId)
    ChromeAPI-->>Hook: { url: "...", id: tabId }
    Hook->>Hook: isTabOnClaude(tabId) -> true/false
    alt isClaudeTab == true
        Hook->>Hook: loadBudget() (subscribe/storage reads)
        Hook->>App: emit { isClaudeTab: true, budget: {...} }
        App->>App: render Usage Budget card
    else isClaudeTab == false
        Hook->>Hook: setBudget(null)
        Hook->>App: emit { isClaudeTab: false, budget: null }
        App->>App: render not-Claude banner
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A tab hops fresh across the stream,
Claude's the meadow where budgets gleam.
If not on Claude, the numbers sleep,
A gentle banner wakes—soft and neat.
Hoppity hops, the dashboard keeps time.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: tab-awareness detection and gating live data in the side panel, directly matching the core functionality across all modified files.
Description check ✅ Passed The description is comprehensive and well-structured, covering problem statement, solution architecture, file changes, test results, and acceptance criteria that align with the template sections.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
entrypoints/sidepanel/hooks/useDashboardData.ts (1)

57-65: URL matching uses substring check.

The includes(CLAUDE_DOMAIN) approach works for the current use case but would also match subdomains like api.claude.ai and theoretically crafted URLs like https://not-claude.ai.example.com. For a side panel gate this is low risk, but if you want stricter matching, consider parsing the URL and checking the hostname.

Optional: stricter hostname check
 export async function isTabOnClaude(tabId: number): Promise<boolean> {
     try {
         const tab = await chrome.tabs.get(tabId);
-        return tab.url?.includes(CLAUDE_DOMAIN) ?? false;
+        if (!tab.url) return false;
+        try {
+            const { hostname } = new URL(tab.url);
+            return hostname === CLAUDE_DOMAIN || hostname.endsWith('.' + CLAUDE_DOMAIN);
+        } catch {
+            return false;
+        }
     } catch {
-        // Tab closed, extension lacks permission for that URL, or API unavailable.
         return false;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts` around lines 57 - 65,
isTabOnClaude currently uses a substring check
(tab.url?.includes(CLAUDE_DOMAIN)) which can false‑positive on crafted
subdomains; update isTabOnClaude to parse tab.url with the URL constructor and
compare the parsed hostname to CLAUDE_DOMAIN (or optionally allow subdomain
suffixes via hostname === CLAUDE_DOMAIN ||
hostname.endsWith(`.${CLAUDE_DOMAIN}`)), handle missing/invalid tab.url before
parsing, and keep the existing try/catch so failures still return false; locate
this logic in the isTabOnClaude function and replace the includes-based check
with the hostname comparison.
tests/unit/tab-awareness.test.ts (1)

116-145: Consider adding subdomain test coverage.

The edge case tests are thorough. One optional enhancement: a test for subdomains like api.claude.ai or console.claude.ai would document the expected behavior since isTabOnClaude uses includes(CLAUDE_DOMAIN) which would match these. If subdomains should be treated differently, this test would catch a regression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/tab-awareness.test.ts` around lines 116 - 145, Add a unit test in
tests/unit/tab-awareness.test.ts that covers subdomains so behavior is explicit:
call mockTabsGet.mockResolvedValueOnce(mockTab('https://api.claude.ai/path'))
(or 'https://console.claude.ai') and assert await isTabOnClaude(tabId) returns
true (since implementation uses includes(CLAUDE_DOMAIN)); if subdomains should
instead be excluded, update isTabOnClaude to perform exact host matching against
CLAUDE_DOMAIN and write the test to expect false accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 57-65: isTabOnClaude currently uses a substring check
(tab.url?.includes(CLAUDE_DOMAIN)) which can false‑positive on crafted
subdomains; update isTabOnClaude to parse tab.url with the URL constructor and
compare the parsed hostname to CLAUDE_DOMAIN (or optionally allow subdomain
suffixes via hostname === CLAUDE_DOMAIN ||
hostname.endsWith(`.${CLAUDE_DOMAIN}`)), handle missing/invalid tab.url before
parsing, and keep the existing try/catch so failures still return false; locate
this logic in the isTabOnClaude function and replace the includes-based check
with the hostname comparison.

In `@tests/unit/tab-awareness.test.ts`:
- Around line 116-145: Add a unit test in tests/unit/tab-awareness.test.ts that
covers subdomains so behavior is explicit: call
mockTabsGet.mockResolvedValueOnce(mockTab('https://api.claude.ai/path')) (or
'https://console.claude.ai') and assert await isTabOnClaude(tabId) returns true
(since implementation uses includes(CLAUDE_DOMAIN)); if subdomains should
instead be excluded, update isTabOnClaude to perform exact host matching against
CLAUDE_DOMAIN and write the test to expect false accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5164e9d1-0399-4186-abe6-b3c45134d7b3

📥 Commits

Reviewing files that changed from the base of the PR and between ae20bdc and fb417c5.

📒 Files selected for processing (4)
  • entrypoints/sidepanel/App.tsx
  • entrypoints/sidepanel/dashboard.css
  • entrypoints/sidepanel/hooks/useDashboardData.ts
  • tests/unit/tab-awareness.test.ts

…L gate [LCO-38]

includes('claude.ai') false-positives on notclaude.ai and on URLs that contain
claude.ai as a query parameter. Extract isClaudeUrl() helper that uses the URL
constructor and compares the parsed hostname directly to CLAUDE_DOMAIN.

Apply the same fix to the onTabUpdated inline check which had the identical bug.

Add two tests that document the corrected behavior:
- notclaude.ai now returns false (was true with includes)
- api.claude.ai returns false (exact hostname only; no subdomain allowance)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
entrypoints/sidepanel/hooks/useDashboardData.ts (1)

215-228: ⚠️ Potential issue | 🟠 Major

Guard loadBudget() against stale completions.

This function always commits its result after await getUsageLimits(...). If the user leaves Claude while that await is in flight, Line 329 or Line 355 clears the budget, but the older request can still resolve later and repopulate live data on a non-Claude tab.

Proposed fix
-    const loadBudget = useCallback(async () => {
+    const loadBudget = useCallback(async (expectedTabId = tabIdRef.current) => {
         try {
             const orgId = orgIdRef.current;
-            if (!orgId) return;
+            if (!orgId || expectedTabId === null || !isClaudeTabRef.current) return;
             const limits = await getUsageLimits(orgId);
+            if (
+                !isClaudeTabRef.current ||
+                tabIdRef.current !== expectedTabId ||
+                orgIdRef.current !== orgId
+            ) {
+                return;
+            }
             if (!limits) {
                 setBudget(null);
                 return;
             }
             setBudget(computeUsageBudget(limits, Date.now()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts` around lines 215 - 228,
loadBudget currently always applies results after await and can overwrite newer
state from other tabs; capture orgId into a local const (already done) and after
each await (both when limits is falsy and before calling
setBudget(computeUsageBudget(...))) verify that orgIdRef.current === orgId (and
return early if not) so stale responses don't repopulate budget; apply this
check around both the limits falsy branch and the setBudget call (functions to
edit: loadBudget, using orgIdRef, getUsageLimits, setBudget,
computeUsageBudget).
🧹 Nitpick comments (1)
entrypoints/sidepanel/hooks/useDashboardData.ts (1)

61-63: Trim the future-feature references from these docs.

These JSDoc blocks describe patterns for features that do not exist in this file yet. The current contract is clear without the forward-looking guidance.

As per coding guidelines, "No speculative code, stubs, placeholders, or 'future-proofing' for features that do not exist yet".

Also applies to: 94-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts` around lines 61 - 63, The
JSDoc for the useDashboardData hook contains speculative "future-feature"
wording (e.g., mentions of per-tab features like pre-submit estimates or delta
tracking); remove those forward-looking sentences and trim the block to only
describe the current contract: that useDashboardData is the single gate for
loading live data such as Usage Budget. Update both JSDoc occurrences (the block
around the top comment and the other block referenced nearby) to be concise and
concrete, leaving no references to features that don't exist.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 233-247: The init() function currently uses a truthy check on
tab.id which skips valid tabId 0; update the conditional to explicitly test for
null/undefined (e.g., if (tab?.id !== undefined && tab?.id !== null) or if
(typeof tab.id === 'number')) so tabIdRef.current is set for id 0,
isTabOnClaude(tab.id) and subsequent calls to applyIsClaudeTab,
loadActiveConversation and setBudget run correctly; adjust references in init()
where tab?.id is used (tabIdRef.current, isTabOnClaude, loadActiveConversation)
to rely on the explicit null/undefined check instead of a truthy check.

---

Outside diff comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 215-228: loadBudget currently always applies results after await
and can overwrite newer state from other tabs; capture orgId into a local const
(already done) and after each await (both when limits is falsy and before
calling setBudget(computeUsageBudget(...))) verify that orgIdRef.current ===
orgId (and return early if not) so stale responses don't repopulate budget;
apply this check around both the limits falsy branch and the setBudget call
(functions to edit: loadBudget, using orgIdRef, getUsageLimits, setBudget,
computeUsageBudget).

---

Nitpick comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 61-63: The JSDoc for the useDashboardData hook contains
speculative "future-feature" wording (e.g., mentions of per-tab features like
pre-submit estimates or delta tracking); remove those forward-looking sentences
and trim the block to only describe the current contract: that useDashboardData
is the single gate for loading live data such as Usage Budget. Update both JSDoc
occurrences (the block around the top comment and the other block referenced
nearby) to be concise and concrete, leaving no references to features that don't
exist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10a1b88a-5c1d-4d92-b31a-c1d9e7ba36fb

📥 Commits

Reviewing files that changed from the base of the PR and between fb417c5 and 549ecec.

📒 Files selected for processing (2)
  • entrypoints/sidepanel/hooks/useDashboardData.ts
  • tests/unit/tab-awareness.test.ts

Comment thread entrypoints/sidepanel/hooks/useDashboardData.ts
…m JSDoc [LCO-38]

loadBudget: add orgIdRef.current === orgId check after getUsageLimits resolves;
without this, a slow storage read from the previous org would overwrite the
newly-cleared or newly-loaded budget state after an account switch or tab change.

init(): replace truthy if (tab?.id) with typeof tab?.id === 'number' so tabId 0
is not silently skipped; Chrome assigns IDs from 1 in practice but the guard
was logically wrong for a number field.

JSDoc: remove speculative references to pre-submit estimates, delta tracking,
and efficiency score -- features that do not exist yet; per CLAUDE.md no
forward-looking wording in production code.
@DevanshuNEU
DevanshuNEU merged commit 1fae644 into OpenCodeIntel:main Apr 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant