Add per-conversation URLs with client-side routing #36

Merged
ryan merged 1 commits from conversation-routing into main 2026-09-09 22:12:07 -04:00
Owner

Give each conversation a deep-linkable URL (/c/:conversationId) so threads survive refresh, support browser back/forward, and are shareable within a session. The app was already an SPA but had no router.

Changes

  • Add react-router-dom; wrap the app in BrowserRouter and define routes (/ for the empty/new state, /c/:conversationId for a thread)
  • Make the URL the source of truth for selection: selectedConversation is derived from the :conversationId param; create/delete navigate instead of mutating local selection state
  • Load a thread's messages in a URL-keyed effect that clears immediately for an instant switch, with a stale-response guard
  • Preserve the optimistic message + in-flight stream when the first message lazily creates a conversation and navigates into it (skip-ref keyed on the new id, guarded against the null/home-route collision)
  • Side effect: "New thread" now navigates to a blank / and lets the first message lazily create the conversation, so it no longer litters empty conversations

No backend changes: app.py already serves index.html for unmatched paths, and the service worker is network-first for navigations, so deep links resolve on hard refresh.

Verification

Drove the real built bundle in headless Chrome against a mock backend (Docker/OIDC unavailable locally):

  • Select thread → /c/:id + messages; switch clears old thread instantly
  • Hard refresh on /c/:id loads the same conversation
  • Browser back/forward navigate between threads
  • New thread → /; first message creates conv, navigates to /c/:newId, stream not wiped
  • Delete active conversation → redirects home
  • Probes: unknown route redirects to /; non-existent conversation id 404s gracefully (no crash)

Verification caught and fixed a bug where the skip-ref null sentinel collided with the home route, leaving stale messages on "New thread". tsc clean, yarn build clean, backend pytest 74 passed (unaffected).

🤖 Generated with Claude Code

Give each conversation a deep-linkable URL (`/c/:conversationId`) so threads survive refresh, support browser back/forward, and are shareable within a session. The app was already an SPA but had no router. ## Changes - Add `react-router-dom`; wrap the app in `BrowserRouter` and define routes (`/` for the empty/new state, `/c/:conversationId` for a thread) - Make the URL the source of truth for selection: `selectedConversation` is derived from the `:conversationId` param; create/delete navigate instead of mutating local selection state - Load a thread's messages in a URL-keyed effect that clears immediately for an **instant switch**, with a stale-response guard - Preserve the optimistic message + in-flight stream when the first message lazily creates a conversation and navigates into it (skip-ref keyed on the new id, guarded against the null/home-route collision) - Side effect: "New thread" now navigates to a blank `/` and lets the first message lazily create the conversation, so it no longer litters empty conversations No backend changes: `app.py` already serves `index.html` for unmatched paths, and the service worker is network-first for navigations, so deep links resolve on hard refresh. ## Verification Drove the real built bundle in headless Chrome against a mock backend (Docker/OIDC unavailable locally): - Select thread → `/c/:id` + messages; switch clears old thread instantly - Hard refresh on `/c/:id` loads the same conversation - Browser back/forward navigate between threads - New thread → `/`; first message creates conv, navigates to `/c/:newId`, stream not wiped - Delete active conversation → redirects home - Probes: unknown route redirects to `/`; non-existent conversation id 404s gracefully (no crash) Verification caught and fixed a bug where the skip-ref `null` sentinel collided with the home route, leaving stale messages on "New thread". `tsc` clean, `yarn build` clean, backend `pytest` 74 passed (unaffected). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ryan added 1 commit 2026-09-09 22:11:07 -04:00
Give each conversation a deep-linkable URL (/c/:conversationId) so threads
survive refresh, support browser back/forward, and are shareable within a
session. The app was already an SPA but had no router.

- Add react-router-dom; wrap the app in BrowserRouter and define routes
  ("/" for the empty/new state, "/c/:conversationId" for a thread)
- Make the URL the source of truth for selection: selectedConversation is
  derived from the :conversationId param; create/delete navigate instead of
  mutating local selection state
- Load a thread's messages in a URL-keyed effect that clears immediately for
  an instant switch, with a stale-response guard
- Preserve the optimistic message + in-flight stream when the first message
  lazily creates a conversation and navigates into it (skip-ref keyed on the
  new id, guarded against the null/home-route collision)

No backend changes: app.py already serves index.html for unmatched paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ryan merged commit bde4a994b7 into main 2026-09-09 22:12:07 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ryan/simbarag#36