Skip to content

fix(switch): drop duplicate tab refs that accumulate across switches - #79

Merged
Matuyuhi merged 1 commit into
mainfrom
fix/space-switch-tab-duplication
May 13, 2026
Merged

Matuyuhi merged 1 commit into
mainfrom
fix/space-switch-tab-duplication

Conversation

@Matuyuhi

Copy link
Copy Markdown
Owner

Symptom

Switching between Spaces accumulates phantom rows in the side panel that all point at the same chrome tabId. Reported by the user:

  • 切り替えるたびに各 Space のタブ表示が増え続ける
  • 実際の Chrome タブは増えていない(タブが死んだ後の id を指す行が無限に積もる)
  • X で閉じても Space 上の行は残り、クリックで No tab with id: … が出る
  • サイドパネルを閉じて再度開くと消える(panel mount で reconcile が走るため)

Root cause

switchTo の empty-Space starter パスが chrome.tabs.create を呼ぶと、

  1. chrome.tabs.onCreated → handlers.onTabCreated → registerTab が並走
  2. switchTo は直前に activeSpaceByWindow = target を書いているため、registerTab も同じ Space を active と判定し root.items に push
  3. switchTo 自身の updateStore もガードなしで push

結果、starter 作成のたびに同じ tabId が 2 重に items に入り、reconcile されるまで残る。

Fixes

  1. switchTo starter push をガード — root.items.some(...) でチェックしてから push (空 Space → starter の race を解消)。
  2. pruneDeadTabs を dedup 対応 — dead tab の除去に加え、各フォルダの items で同 tabId が複数あれば 1 件に圧縮。過去ログから残った重複を reconcile sweep が自己修復する。
  3. reconcile の判定を拡張 — dead tab 0 件でも live tab に重複があれば pruneDeadTabs を走らせる(panel mount / visibility change で発火)。
  4. activateTab ハンドラに self-heal — chrome.tabs.update が No tab with id で落ちたら、その場で dropTab してから error を投げる(ゴースト行が次の refresh で消える)。

Test plan

  • bun run build 通過
  • bunx vitest run src/background/ 145 件全 pass
  • 追加した dedupe テスト (reconcile dedupes duplicate live tab refs without any tabs being dead) が pass
  • chrome://extensions で拡張 reload して以下が解消するか確認:
    • 2 つの空 Space を作って何度も切替 → 各 Space の items が 1 件で止まる
    • 1 つの実タブを共有しつつ Space を切替 → タブ行が 1 行のまま
    • 既に重複している状態でパネルを開く → reconcile dedup で自動消去
    • 死んだ tabId の行をクリック → エラー表示と同時に行が消える

🤖 Generated with Claude Code

Symptom: switching between Spaces accumulates phantom rows in the side
panel that all point at the same chrome tabId. Closing the tab leaves
the ghost rows behind; clicking them throws "No tab with id"; closing
and reopening the side panel clears them (because reconcile runs on
mount).

The race that seeds the duplicates: switchTo's empty-Space starter path
calls chrome.tabs.create, which fires chrome.tabs.onCreated →
registerTab in parallel with switchTo's own updateStore. Since the SW
just wrote activeSpaceByWindow = target, registerTab picks the same
Space and pushes the new tabId into root.items. switchTo's updateStore
then pushes again with no guard, leaving two entries — repeated for
every starter creation.

Defenses:
- switchTo: guard the starter push with a `.some(...)` check so a
  concurrent registerTab can't produce a duplicate.
- pruneDeadTabs: dedupe folder.items by tabId on top of the existing
  dead-tab cull, so any duplicates that slipped through earlier (or
  through a race we haven't yet identified) self-heal on the next
  reconcile.
- reconcile: also trigger when items have duplicate live tabIds (not
  only when refs are dead), so the dedup pass actually runs.
- activateTab handler: when chrome.tabs.update throws "No tab with id",
  drop the stale ref before surfacing the error so the ghost row goes
  away on the next refresh instead of sitting there until the user
  reopens the panel.

Test coverage: reconcile dedupes duplicate live tab refs without any
tabs being dead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Matuyuhi
Matuyuhi merged commit 01c662c into main May 13, 2026
1 check passed
@Matuyuhi
Matuyuhi deleted the fix/space-switch-tab-duplication branch May 13, 2026 03:45
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