Skip to content

feat: multi-session tab management with navigation source tracking#159

Closed
liuyixin-louis wants to merge 6 commits intoOpenLAIR:mainfrom
liuyixin-louis:feat/multi-session-tabs
Closed

feat: multi-session tab management with navigation source tracking#159
liuyixin-louis wants to merge 6 commits intoOpenLAIR:mainfrom
liuyixin-louis:feat/multi-session-tabs

Conversation

@liuyixin-louis
Copy link
Copy Markdown
Collaborator

Summary

Adds browser-style tab management to the chat area. Users can open multiple sessions as tabs, switch between them, and see processing indicators on background tabs.

Core innovation: Introduces sessionNavigationSource tracking ('user-sidebar' | 'user-new-session' | 'system') threaded from useProjectsState through AppContent to MainContent. This allows the tab sync logic to distinguish user-initiated navigation (sidebar click) from system-initiated session ID changes (session-created events), solving the cascade problem where agent responses triggered spurious tab creation.

New files

  • src/hooks/useChatTabs.ts — Tab state management hook
  • src/components/chat/view/ChatTabBar.tsx — Tab bar UI with processing indicators
  • src/components/main-content/view/chatTabSync.ts — Pure function deciding tab action based on navigation source
  • src/components/chat/hooks/chatSessionTransition.ts — Determines when to preserve optimistic messages during session ID promotion
  • Unit tests — 10 tests covering tab sync, session transition, and tab bar rendering

Modified files

  • useProjectsState.ts — Exposes sessionNavigationSource ref
  • AppContent.tsx — Threads sessionNavigationSource to MainContent
  • MainContent.tsx — Tab bar integration + sync effect using resolveChatTabSyncAction
  • useChatSessionState.ts — In-memory message cache (10 entries, LRU eviction) for instant tab switching
  • useChatRealtimeHandlers.ts — Cache invalidation on *-complete events

How tab sync works

resolveChatTabSyncAction({navigationSource, nextSessionId, activeChatTabSessionId, ...})
  → 'user-sidebar' + new session    → 'open-tab'
  → 'user-new-session'              → 'open-new-tab'  
  → 'system' (session-created)      → 'update-active-tab-session' (no UI disruption)
  → session already in active tab   → 'skip'

Test plan

  • Open project, click session A → single tab visible
  • Click session B in sidebar → second tab appears
  • Switch between tabs → correct conversation loads (cached = instant)
  • Send message in session → agent responds → stays in same tab (no jump)
  • Click "New Session" → new tab opens with provider picker
  • Close active tab → neighbor activates
  • Background tab shows blue pulse while session is processing
  • npx tsc --noEmit --skipLibCheck → 0 errors
  • Tab-related tests: 10/10 pass
  • Pre-existing tests: 49/49 pass (server tests have pre-existing failures unrelated to this PR)

🤖 Generated with Claude Code

liuyixin-louis and others added 4 commits April 10, 2026 14:45
Adds browser-style tab bar above chat interface, allowing users to open
multiple sessions simultaneously. Background tabs show a blue pulse
indicator when their session is still processing.

- useChatTabs hook: manages tab state, open/close/switch with
  navigation integration via existing onNavigateToSession flow
- ChatTabBar component: hidden for single session (zero visual change),
  appears when 2+ tabs open, horizontal scroll for overflow
- MainContent wiring: syncs selectedSession into tabs, renders tab bar
  above ChatInterface without modifying ChatInterface internals

ChatInterface, useChatRealtimeHandlers, and WebSocketContext are
completely unchanged. Tab switching reuses the existing session
navigation system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user clicks "New Session" in sidebar, selectedSession becomes null.
Previously this was ignored by the tab sync effect. Now it opens a new
tab when tabs already exist, so the previous session stays in its own tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes 3 issues from Codex code review:

P1: Active tab now drives ChatInterface session via effectiveSession.
    When active tab has sessionId=null (new chat), ChatInterface receives
    null and shows provider picker instead of stale conversation.
    Tab bar now visible with 1+ tabs (was 2+) so the [+] button is
    always accessible.

P2: Removed setTimeout in closeTab — onNavigateToSession called
    synchronously, eliminating the race where rapid close+switch
    could cause stale navigation.

P3: Close button is now a proper <button> element (was nested span
    inside button). Keyboard-focusable, has aria-label, not nested
    inside interactive content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds browser-style tabs for chat sessions with proper handling of
system vs user navigation to prevent session-created events from
disrupting active conversations.

Key changes:
- useChatTabs hook: tab state management with open/close/switch
- ChatTabBar component: tab UI with processing indicators
- chatTabSync.ts: pure function deciding tab action based on
  navigation source (user-sidebar, user-new-session, system)
- chatSessionTransition.ts: determines when to preserve optimistic
  messages during session ID promotion
- sessionNavigationSource: new prop threaded from useProjectsState
  through AppContent to MainContent, distinguishing user clicks
  from system session-created events
- Message cache with invalidation on session complete
- 10 new unit tests for tab sync, session transition, and tab bar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@Zhang-Henry Zhang-Henry left a comment

Choose a reason for hiding this comment

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

Code Review

Nice architecture — the pure resolveChatTabSyncAction function is cleanly isolated and well-tested. A few issues to address before merge:

Blockers

  1. Duplicate useChatTabs export in src/hooks/useChatTabs.ts — there appear to be two export function useChatTabs() declarations with different signatures. This will fail TS compilation; looks like an unresolved merge artifact. Keep only the version actually consumed by MainContent.

  2. sessionNavigationSource is never reset after consumption (useProjectsState.ts + MainContent.tsx sync effect). After the effect reads a 'system' source, nothing clears it, so a subsequent unrelated state change can misclassify as system-driven and suppress tab creation. Make it a one-shot ref that resets to 'user' immediately after resolveChatTabSyncAction is called.

  3. "LRU" cache in useChatSessionState.ts is actually FIFO. SESSION_MESSAGE_CACHE_MAX = 10 evicts via Map.keys().next() (insertion order), not recency. A frequently-used tab opened long ago will be evicted before an untouched recent one. Either implement real LRU (delete+set on read to bump recency) or rename and document the FIFO semantics.

  4. Incomplete cache invalidation in useChatRealtimeHandlers.ts — invalidation only fires on *-complete events. Missing: message edit / delete / stream-error events. Editing a message in the active tab and switching away/back will show the stale cached version.

Should-fix

  1. No keyboard navigation in ChatTabBar.tsx. role="tab"/aria-selected are present, but there are no key handlers for ArrowLeft/Right, Enter/Space, Ctrl+Tab, and no focus management after closing the active tab. Keyboard-only users can't operate the tab bar.

  2. docs/plans/2026-04-09-multi-session-tabs.md (454 lines) is a task-list spec, not a design doc — it bloats the PR and will rot. Move to an Issue / project tracker, or trim to a short architecture note.

  3. Hard-coded 'new-session-' prefix in chatTabSync.ts (isTemporarySessionId) is fragile. Expose the prefix as a shared constant from wherever temp session IDs are generated.

Nits

  • activeTabIdRef is declared in useChatTabs.ts but never used — dead code.
  • Cache value types in useChatSessionState.ts use any[] / any — tighten to ChatMessage[] and the actual token usage type.
  • Tab titles don't sync if a session is renamed server-side; document the limitation or subscribe to rename events.

Tests

The 10 unit tests cover the pure decision function well, but missing: race between session-created and user sidebar click, cache invalidation on edits/deletes, and keyboard navigation on ChatTabBar.

Overall the design is sound — mainly asking for the blockers above to be fixed before merge.

…ion, new tab flow

Fixes all blockers from Zhang-Henry's code review:

B2: sessionNavigationSource now reset to 'user' after resolveChatTabSyncAction
    consumes it, preventing stale 'system' classification on subsequent changes.

B3: Message cache upgraded from FIFO to LRU — getCachedSessionMessages now
    delete+set on read to bump recency in Map iteration order.

B4: Cache invalidation added to all error handlers (claude-error, cursor-error,
    codex-error, gemini-error, openrouter-error, localgpu-error), not just
    *-complete events.

New tab [+] button fix: now calls onNewSession(selectedProject) which triggers
the full session lifecycle reset (setSelectedSession(null), navigate('/'),
clear provider state) before creating the tab. This ensures ChatInterface
shows the provider picker instead of stale conversation content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@liuyixin-louis
Copy link
Copy Markdown
Collaborator Author

Review Fixes Applied

Pushed feeab4d addressing all blockers from @Zhang-Henry's review:

Blockers Fixed

  • B1 (duplicate export): Confirmed not present — only one useChatTabs export exists. Likely a diff rendering artifact.

  • B2 (sessionNavigationSource never reset): Added resetSessionNavigationSource callback from useProjectsState, threaded through AppContentMainContent. Called immediately after resolveChatTabSyncAction() consumes the value, preventing stale 'system' classification on subsequent state changes.

  • B3 (FIFO → LRU cache): getCachedSessionMessages() now does delete + set on cache hit to bump the entry to most-recent position in Map iteration order. Frequently-used tabs stay cached.

  • B4 (incomplete cache invalidation): Added invalidateSessionMessageCache() calls in all error handlers: claude-error, gemini-error, openrouter-error, localgpu-error, cursor-error, codex-error — same pattern as the *-complete handlers.

Additional Bug Fix

  • [+] New Tab button: Was showing stale conversation instead of provider picker. Root cause: openNewTab() only updated tab state but didn't trigger the session lifecycle reset. Fix: [+] now calls onNewSession(selectedProject) (the existing handleNewSession from useProjectsState) which properly clears selectedSession, resets provider state, and navigates to /. Tab creation happens after state is clean.

  • chatTabSync duplicate prevention: Added guard so when active tab already has sessionId === null, a nextSessionId: null event returns 'noop' instead of 'open-new-tab'.

Nits

  • activeTabIdRef dead code — already removed in prior commit
  • Should-fix items (keyboard nav, spec doc trim, shared constant) deferred to follow-up

Verification

  • npx tsc --noEmit --skipLibCheck → 0 errors
  • Component tests: 31/31 pass
  • Manual testing: tab switching, [+] new tab, sending messages, background tab indicators all working

- Remove unread pendingNavRef in useChatTabs.closeTab (dead code).
- Add type="button" to ChatTabBar buttons so they never submit an
  ancestor form, plus aria-label on the [+] new-tab button.
@Zhang-Henry
Copy link
Copy Markdown
Collaborator

Rebase needed before merge

fa69ddb 已推送,清掉了 review 里两处残余:

  • 删除 useChatTabs.closeTab 里未读取的 pendingNavRef(死代码)
  • ChatTabBar 的三个 <button> 补齐 type="button" + [+]aria-label

验证:npx tsc --noEmit --skipLibCheck → 0 errors,tab 相关 10/10 tests pass。

但目前合不了:本分支落后 main 37 个 commit,冲突集中在 session lifecycle 核心文件:

这些冲突不是机械性的,建议由熟悉本 PR 意图的作者来 rebase。

@liuyixin-louis 麻烦 rebase 到最新的 main 并解决冲突,然后 re-request @Zhang-Henry 复审。rebase 后若 tab sync 的行为有任何偏移,请同步更新 chatTabSync.test.ts / chatSessionTransition.test.ts

@Zhang-Henry Zhang-Henry self-requested a review April 13, 2026 18:48
@liuyixin-louis
Copy link
Copy Markdown
Collaborator Author

Superseded by #174 — clean rebase on latest main with all review fixes applied.

liuyixin-louis added a commit to liuyixin-louis/dr-claw-1 that referenced this pull request Apr 15, 2026
- Add keyboard navigation to ChatTabBar (ArrowLeft/Right, Home/End, roving tabindex)
- Add type="button" and aria-label to all tab bar buttons
- Extract TEMP_SESSION_PREFIX and isTemporarySessionId into shared constant
- Tighten session message cache types from any[] to ChatMessage[]
- Trim 454-line spec doc to architecture note

Addresses deferred items from Zhang-Henry's review on PR OpenLAIR#159.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants