feat: multi-session tab management with navigation source tracking#159
feat: multi-session tab management with navigation source tracking#159liuyixin-louis wants to merge 6 commits intoOpenLAIR:mainfrom
Conversation
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>
Zhang-Henry
left a comment
There was a problem hiding this comment.
Code Review
Nice architecture — the pure resolveChatTabSyncAction function is cleanly isolated and well-tested. A few issues to address before merge:
Blockers
-
Duplicate
useChatTabsexport insrc/hooks/useChatTabs.ts— there appear to be twoexport function useChatTabs()declarations with different signatures. This will fail TS compilation; looks like an unresolved merge artifact. Keep only the version actually consumed byMainContent. -
sessionNavigationSourceis never reset after consumption (useProjectsState.ts+MainContent.tsxsync 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 afterresolveChatTabSyncActionis called. -
"LRU" cache in
useChatSessionState.tsis actually FIFO.SESSION_MESSAGE_CACHE_MAX = 10evicts viaMap.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+seton read to bump recency) or rename and document the FIFO semantics. -
Incomplete cache invalidation in
useChatRealtimeHandlers.ts— invalidation only fires on*-completeevents. 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
-
No keyboard navigation in
ChatTabBar.tsx.role="tab"/aria-selectedare 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. -
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. -
Hard-coded
'new-session-'prefix inchatTabSync.ts(isTemporarySessionId) is fragile. Expose the prefix as a shared constant from wherever temp session IDs are generated.
Nits
activeTabIdRefis declared inuseChatTabs.tsbut never used — dead code.- Cache value types in
useChatSessionState.tsuseany[]/any— tighten toChatMessage[]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>
Review Fixes AppliedPushed Blockers Fixed
Additional Bug Fix
Nits
Verification
|
- 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.
Rebase needed before merge
验证: 但目前合不了:本分支落后
这些冲突不是机械性的,建议由熟悉本 PR 意图的作者来 rebase。 @liuyixin-louis 麻烦 rebase 到最新的 |
|
Superseded by #174 — clean rebase on latest main with all review fixes applied. |
- 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>
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
sessionNavigationSourcetracking ('user-sidebar' | 'user-new-session' | 'system') threaded fromuseProjectsStatethroughAppContenttoMainContent. This allows the tab sync logic to distinguish user-initiated navigation (sidebar click) from system-initiated session ID changes (session-createdevents), solving the cascade problem where agent responses triggered spurious tab creation.New files
src/hooks/useChatTabs.ts— Tab state management hooksrc/components/chat/view/ChatTabBar.tsx— Tab bar UI with processing indicatorssrc/components/main-content/view/chatTabSync.ts— Pure function deciding tab action based on navigation sourcesrc/components/chat/hooks/chatSessionTransition.ts— Determines when to preserve optimistic messages during session ID promotionModified files
useProjectsState.ts— ExposessessionNavigationSourcerefAppContent.tsx— ThreadssessionNavigationSourceto MainContentMainContent.tsx— Tab bar integration + sync effect usingresolveChatTabSyncActionuseChatSessionState.ts— In-memory message cache (10 entries, LRU eviction) for instant tab switchinguseChatRealtimeHandlers.ts— Cache invalidation on*-completeeventsHow tab sync works
Test plan
npx tsc --noEmit --skipLibCheck→ 0 errors🤖 Generated with Claude Code