Skip to content

Migrate thread selection to URL-driven state with browser history support#883

Open
JSv4 wants to merge 3 commits intomainfrom
claude/fix-mobile-navigation-fSBZh
Open

Migrate thread selection to URL-driven state with browser history support#883
JSv4 wants to merge 3 commits intomainfrom
claude/fix-mobile-navigation-fSBZh

Conversation

@JSv4
Copy link
Collaborator

@JSv4 JSv4 commented Feb 11, 2026

Summary

This PR migrates thread selection in the Discussions tab from local atom state to URL-driven state via the ?thread= query parameter. This enables proper browser back/forward navigation between thread list and thread detail views, and aligns with the CentralRouteManager architecture for managing application state through URLs.

Key Changes

  • Thread Selection State Migration: Replaced inlineSelectedThreadIdAtom with URL-driven state synced through selectedThreadIdVar reactive var in the Apollo cache. The ?thread= query parameter is now the source of truth.

  • Navigation Utilities:

    • Added updateThreadParam() function to manage thread URL parameter updates with proper history handling
    • Updated updateTabParam() and updateDetailViewParam() to use navigate() instead of replace: true, enabling proper browser back/forward navigation
    • Both functions now clear related params when switching contexts to prevent stale state
  • CorpusDiscussionsView Refactoring:

    • Removed local atom state management for thread selection
    • Updated handleThreadClick() to call updateThreadParam() instead of setting atom state
    • Updated handleBackToList() to use navigate(-1) for proper history navigation
    • Removed cleanup effect that was resetting state on unmount
    • Now reads thread selection from selectedThreadIdVar reactive var
  • State Derivation in Parent Component:

    • Corpuses.tsx now derives discussionInThreadView from the URL-driven selectedThreadId reactive var instead of managing it as local state
    • Removed onViewModeChange callback from CorpusDiscussionsView since state is now URL-driven
  • Mobile UI Enhancements:

    • Added MobileMenuButton styled component for mobile navigation menu access
    • Updated CorpusLandingView and CorpusDetailsView to include mobile menu button in headers
    • Added onOpenMobileMenu callback prop to support mobile navigation
  • Cleanup: Removed inlineSelectedThreadIdAtom from threadAtoms.ts with explanatory comment about URL-driven approach

Implementation Details

The migration follows the CentralRouteManager Phase 2 pattern where:

  1. User interactions update the URL via updateThreadParam()
  2. CentralRouteManager reads the ?thread= param and syncs it to selectedThreadIdVar
  3. Components read from the reactive var and re-render when it changes
  4. Browser back/forward naturally works because we use navigate() instead of replace: true

This approach eliminates the need for callback-based state synchronization and provides a single source of truth (the URL) for thread selection state.

https://claude.ai/code/session_015gUfjhz6wHKpvJiEiXgBA7

…ead nav

Three issues addressed:

1. Mobile corpus home view had no way to access sidebar tabs. Added a
   MobileMenuButton (kebab icon) to both CorpusLandingView (in breadcrumbs)
   and CorpusDetailsView (in header) that opens the sidebar bottom sheet,
   consistent with how other tabs expose the MobileKebabButton.

2. Discussion thread navigation was not URL-driven, making browser
   back/forward unusable. Replaced the local inlineSelectedThreadIdAtom
   (Jotai) with the URL-synced selectedThreadId reactive var (?thread=
   param), already handled by CentralRouteManager. Added updateThreadParam
   utility that pushes history entries for proper back/forward.

3. Tab switching used replace instead of push, preventing browser
   back/forward between tabs. Changed updateTabParam and
   updateDetailViewParam to push new history entries. updateTabParam also
   now clears thread/message params when switching tabs.

https://claude.ai/code/session_015gUfjhz6wHKpvJiEiXgBA7
@claude
Copy link

claude bot commented Feb 11, 2026

Pull Request Review: URL-Driven Thread Selection Migration

This PR successfully migrates thread selection from local Jotai atom state to URL-driven state via the ?thread= query parameter. The implementation aligns well with the CentralRouteManager Phase 2 pattern and follows the project's routing conventions. Below is my detailed review:


✅ Strengths

Architecture Alignment

  • Correct pattern adherence: The migration properly follows the CentralRouteManager Phase 2 pattern where components update the URL via utility functions, and CentralRouteManager syncs the URL params to reactive vars
  • Unidirectional data flow: Component → URL → CentralRouteManager → Reactive Var → Component (documented in docs/frontend/routing_system.md:54-55)
  • Proper cleanup: Removed the deprecated inlineSelectedThreadIdAtom with a clear explanatory comment about the new URL-driven approach

Browser Navigation Support

  • History management: Changed from replace: true to push-based navigation in updateTabParam(), updateDetailViewParam(), and new updateThreadParam() functions, enabling proper browser back/forward navigation
  • Proper back navigation: handleBackToList() correctly uses navigate(-1) to pop the history entry created by handleThreadClick()
  • State cleanup: updateTabParam() now clears thread/message params when switching tabs to prevent stale state

Mobile UX Improvements

  • Consistent menu access: Added MobileMenuButton (kebab icon) to both CorpusLandingView and CorpusDetailsView, consistent with other tab headers
  • Proper visual styling: Uses existing design system constants and media queries for responsive behavior

Code Quality

  • Clear documentation: Comprehensive JSDoc comments explain the navigation flow and history behavior
  • Consistent naming: updateThreadParam() follows the same naming pattern as updateTabParam(), updateMessageParam(), etc.
  • Proper hooks: Dependencies correctly specified in useCallback hooks

⚠️ Issues & Recommendations

1. Missing Test Coverage for New Functionality (Medium Priority)

Issue: The new updateThreadParam() function has no unit tests, and the existing component test doesn't verify the URL-driven behavior.

Evidence:

  • frontend/src/utils/__tests__/navigationUtils.test.ts has no tests for updateThreadParam
  • frontend/tests/discussions/CorpusDiscussionsView.test.tsx uses MemoryRouter without query params, doesn't test thread selection or navigation

Recommendation: Add tests to verify:

// In navigationUtils.test.ts
describe('updateThreadParam()', () => {
  it('should set thread query param when threadId provided', () => { /* ... */ });
  it('should clear thread and message params when threadId is null', () => { /* ... */ });
  it('should push history entry, not replace', () => { /* ... */ });
});

// In CorpusDiscussionsView.test.tsx
test('should update URL when thread is clicked', async ({ mount }) => {
  // Verify updateThreadParam called with correct threadId
});
test('should navigate back when handleBackToList called', async ({ mount }) => {
  // Verify navigate(-1) called
});

Impact: Without tests, regressions in the new URL-driven thread navigation could go undetected. Per CLAUDE.md baseline commit rules, all affected tests should pass before committing.


2. Inconsistent Prop Removal (Low Priority)

Issue: onViewModeChange prop is removed from CorpusDiscussionsView invocation in Corpuses.tsx:2405, but the prop definition remains in the component interface and is still used internally (CorpusDiscussionsView.tsx:398).

Current state:

// Corpuses.tsx:2405 - prop not passed
<CorpusDiscussionsView corpusId={opened_corpus.id} hideHeader />

// CorpusDiscussionsView.tsx:34 - prop still defined
export interface CorpusDiscussionsViewProps {
  onViewModeChange?: (inThreadView: boolean) => void; // Still here
}

// CorpusDiscussionsView.tsx:398 - prop still called
useEffect(() => {
  onViewModeChange?.(inThreadView); // Optional chaining prevents errors
}, [inThreadView, onViewModeChange]);

Recommendation: Since discussionInThreadView is now derived directly from selectedThreadId in the parent component (Corpuses.tsx:1583-1584), the callback is no longer needed. Either:

  1. Clean removal: Remove the prop from the interface and the useEffect in CorpusDiscussionsView.tsx
  2. Keep for extensibility: Document why it's kept if future use cases exist

Impact: Minor - the optional chaining prevents runtime errors, but it leaves dead code that could confuse future maintainers.


3. Potential Mobile Menu Prop Drilling (Low Priority)

Issue: The onOpenMobileMenu prop is threaded through CorpusHomeCorpusLandingView / CorpusDetailsView but its source isn't visible in this PR.

Current propagation:

Corpuses.tsx (parent, not in diff) 
  → CorpusHome.tsx:53 (receives prop)
    → CorpusLandingView.tsx:88 / CorpusDetailsView.tsx:96 (uses prop)

Recommendation: Verify that:

  • The prop is actually provided by the parent component (check Corpuses.tsx full implementation)
  • The mobile menu state management aligns with project patterns (reactive vars vs local state)
  • Optional chaining is appropriate if the prop may be undefined in some contexts

Impact: If the prop isn't provided upstream, the buttons will render but do nothing on click (no-op due to onClick={onOpenMobileMenu} without optional chaining).


4. CHANGELOG.md Not Updated (Medium Priority)

Issue: Per CLAUDE.md guidelines (CLAUDE.md:322-348), significant changes should be documented in CHANGELOG.md.

This PR includes:

  • New feature: URL-driven thread navigation with browser history support
  • UX improvement: Mobile menu access in corpus home views
  • Breaking change: Thread selection state migration from atoms to URL params

Recommendation: Add an entry to CHANGELOG.md under ## [Unreleased]:

### Changed
- **Thread selection now URL-driven**: Discussions tab thread selection uses `?thread=` query parameter instead of local atom state, enabling proper browser back/forward navigation between thread list and detail views
  - Files: `frontend/src/components/discussions/CorpusDiscussionsView.tsx`, `frontend/src/utils/navigationUtils.ts`, `frontend/src/views/Corpuses.tsx`

### Added  
- **Mobile navigation menu access**: Added kebab menu buttons to corpus landing and details views for accessing sidebar navigation on mobile devices
  - Files: `frontend/src/components/corpuses/CorpusHome/CorpusLandingView.tsx`, `frontend/src/components/corpuses/CorpusHome/CorpusDetailsView.tsx`

🔍 Code Quality Assessment

Positive Patterns

✅ No direct manipulation of reactive vars (respects CentralRouteManager ownership)
✅ Proper use of useCallback with correct dependencies
✅ Consistent with existing navigation utility patterns
✅ Good inline documentation
✅ Mobile-first responsive design

Potential Concerns

⚠️ Tests don't verify new URL navigation behavior
⚠️ Dead prop interface definition (onViewModeChange)
⚠️ Missing CHANGELOG entry


🛡️ Security & Performance

Security: No concerns. Navigation utilities sanitize query params through URLSearchParams, preventing XSS.

Performance: No concerns. The change actually reduces state synchronization overhead by eliminating callback-based state propagation.


📋 Pre-Merge Checklist

Before merging, please ensure:

  • Tests added for updateThreadParam() in navigationUtils.test.ts
  • Component tests updated to verify URL navigation behavior
  • TypeScript compiles (cd frontend && yarn build)
  • Pre-commit hooks pass (pre-commit run --all-files)
  • Verify onOpenMobileMenu prop is wired in Corpuses.tsx
  • Remove or document onViewModeChange if truly dead code
  • Update CHANGELOG.md per project guidelines
  • Manual testing: Verify browser back/forward works in discussions tab

Summary

This is a well-architected PR that correctly implements the URL-driven state pattern. The navigation flow is sound, and the mobile UX improvements are a welcome addition. The main gaps are in test coverage and documentation. Once those are addressed, this will be a solid contribution to the routing system migration.

Overall Assessment: ✅ Approve with minor changes requested

Great work following the architectural patterns! 🎉

- Remove dead `onViewModeChange` prop and `useEffect` from
  CorpusDiscussionsView — parent now derives thread view state directly
  from the URL-synced selectedThreadId reactive var
- Remove unused `useEffect` import
- Update existing `updateTabParam()` tests to assert push semantics
  (no `{ replace: true }`) and add 3 new tests for thread/message
  param clearing on tab switch
- Add 5 new `updateThreadParam()` tests covering set, clear, push
  behavior, param preservation, and update
- Add CHANGELOG.md entry documenting mobile nav and URL-driven thread
  selection changes

https://claude.ai/code/session_015gUfjhz6wHKpvJiEiXgBA7
@claude
Copy link

claude bot commented Feb 11, 2026

Code Review - PR #883

I've completed a thorough review of this PR. Overall, this is a well-architected migration that successfully aligns thread selection with the CentralRouteManager pattern. The implementation is clean, well-tested, and follows the project's established conventions.


✅ Strengths

1. Excellent Architecture Alignment

  • Properly follows the CentralRouteManager Phase 2 pattern documented in docs/frontend/routing_system.md
  • URL is now the single source of truth for thread selection state
  • Eliminates callback-based state synchronization in favor of URL-driven reactive vars
  • Browser back/forward navigation works correctly with navigate() instead of replace: true

2. Comprehensive Test Coverage

The test updates in navigationUtils.test.ts are thorough:

  • updateTabParam() tests verify history pushing (not replacing) for proper back/forward navigation
  • updateThreadParam() tests confirm thread selection and clearing behavior
  • Tests verify tab-specific params (thread, message) are properly cleared when switching tabs
  • All edge cases are covered (null values, preserving unrelated params, etc.)

3. Clean Code Quality

  • Removed dead code (inlineSelectedThreadIdAtom, onViewModeChange callback)
  • Added explanatory comments about URL-driven approach
  • Navigation utilities are well-documented with JSDoc comments
  • Proper separation of concerns (URL management in utilities, UI in components)

4. Mobile UX Enhancement

The mobile menu button addition is a thoughtful improvement:

  • Gives mobile users access to navigation from corpus home views
  • Consistent placement across CorpusLandingView and CorpusDetailsView
  • Uses existing styled component pattern

5. Excellent Changelog Documentation

The CHANGELOG.md updates are detailed and follow the Keep a Changelog format properly, with clear file references and line numbers.


🔍 Code Quality Observations

navigationUtils.ts (Lines 806-857)

The new utility functions are well-designed:

Good:

  • updateTabParam() correctly pushes history for tab navigation (Line 831)
  • Properly clears tab-specific params when switching tabs (Lines 827-829)
  • updateThreadParam() enables browser back/forward with history push (Line 856)
  • Clears message param when clearing thread (Line 853)

Good Pattern: Both functions preserve unrelated params while updating/clearing specific ones

CorpusDiscussionsView.tsx (Lines 385-458)

State management migration is clean:

Good:

  • Reads from URL-driven reactive var instead of local atom (Line 387)
  • handleThreadClick properly updates URL param (Lines 447-452)
  • handleBackToList uses navigate(-1) for proper history navigation (Lines 456-458)
  • Removed cleanup effect that was resetting state on unmount (good - URL state persists across navigation)

Corpuses.tsx (Lines 1582-1584)

Parent component correctly derives state:

Good:

  • Derives discussionInThreadView from URL-driven reactive var
  • No more local state management for this concern
  • Cleaner component logic

⚠️ Potential Issues & Recommendations

1. Minor: Typo in Corpuses.tsx (Line 1586)

/ Debug: Log state changes  // Missing second slash

Fix: Change to // for proper comment syntax

2. Performance: Consider Debouncing Thread Selection (Optional)

If users rapidly click through threads, each click pushes a new history entry. This could make the back button feel "sluggish" if many entries accumulate.

Recommendation: Consider debouncing handleThreadClick or using replace: true for rapid successive clicks within a short time window (e.g., 500ms). This is a minor optimization and not critical.

3. Test Coverage: Component Tests (Not Blocking)

While the unit tests for navigationUtils.ts are excellent, I don't see corresponding component tests for:

  • CorpusDiscussionsView thread selection behavior
  • Browser back/forward navigation in discussions tab

Recommendation: Add Playwright component tests following the pattern in .cursor/rules/test-document-knowledge-base.mdc:

// Pseudo-code example
test('should navigate to thread detail when thread clicked', async ({ mount }) => {
  const component = await mount(<DiscussionsTestWrapper />);
  await component.getByTestId('thread-item-123').click();
  expect(window.location.search).toContain('thread=123');
});

test('should return to list when back button clicked', async ({ mount, page }) => {
  // ... navigate to thread, then click back
  await page.goBack();
  expect(window.location.search).not.toContain('thread=');
});

4. Documentation: Update Architecture Docs (Minor)

The PR mentions following the CentralRouteManager pattern, but doesn't update docs/frontend/routing_system.md to document this specific thread selection migration.

Recommendation: Add a brief section to the routing docs showing thread selection as an example of Phase 2 URL-driven state.


🔒 Security Review

No security concerns identified

  • URL params are properly sanitized through URLSearchParams
  • No user input is directly injected into URLs
  • Reactive vars prevent XSS through proper React rendering

🎯 Performance Considerations

Good:

  • Removed unnecessary state synchronization callbacks
  • URL-driven state eliminates potential desync issues
  • Reactive vars provide efficient re-rendering

⚠️ Minor: As mentioned above, rapid thread clicks create many history entries. Not a significant performance issue, just a UX consideration.


📋 Best Practices Adherence

Follows CLAUDE.md conventions:

  • ✅ No magic numbers (uses proper constants)
  • ✅ DRY principles (utility functions are reusable)
  • ✅ Single responsibility (each function has one purpose)
  • ✅ Proper test coverage for utilities
  • ✅ Clean changelog documentation
  • ✅ No dead code left behind

Follows project's routing architecture:

  • ✅ URL as single source of truth
  • ✅ Components read from reactive vars
  • ✅ Navigation utilities handle URL updates
  • ✅ CentralRouteManager pattern correctly implemented

🚦 Recommendation

APPROVE with minor suggestions

This PR is ready to merge. The suggested improvements are optional enhancements that don't block merging:

Before Merge (Critical):

  • Fix typo in Corpuses.tsx Line 1586 (///)

Future Enhancements (Non-blocking):

  • Consider debouncing rapid thread clicks (performance optimization)
  • Add component tests for thread navigation (test coverage)
  • Update routing architecture docs with thread example (documentation)

📊 Summary

Code Quality: ⭐⭐⭐⭐⭐ (5/5)
Test Coverage: ⭐⭐⭐⭐ (4/5) - Unit tests excellent, component tests would make it perfect
Documentation: ⭐⭐⭐⭐ (4/5) - Excellent changelog, could add architecture doc update
Architecture: ⭐⭐⭐⭐⭐ (5/5) - Perfectly aligns with CentralRouteManager pattern

Overall: Excellent work! This migration is well-thought-out and properly executed. 🎉

- Remove console.log debug statements left in Corpuses.tsx
- Replace navigate(-1) with updateThreadParam(null) in handleBackToList
  so back-to-list always explicitly clears the ?thread= param via URL,
  letting CentralRouteManager Phase 2 handle state — consistent with
  the routing mantra and safe for deep-linked/bookmarked URLs
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