Skip to content

refactor(examples-chat): URL is the sole source of truth for active thread#518

Merged
blove merged 3 commits into
mainfrom
claude/url-as-truth
May 21, 2026
Merged

refactor(examples-chat): URL is the sole source of truth for active thread#518
blove merged 3 commits into
mainfrom
claude/url-as-truth

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 21, 2026

Summary

Finishes PR #500's original intent and unwinds PR #514's localStorage walkback. The URL is now the sole source of truth for the active thread — no localStorage fallback for threadId.

Spec doc (2026-05-20-url-thread-routing-design.md) rewritten to reflect the simplified architecture; was describing the pre-#504 6-route world.

Test plan

  • examples-chat-angular unit tests pass (4 stay green, 3 new)
  • examples/chat — e2e matrix green on CI
  • Manual: paste /embed/<good-id> → loads that thread
  • Manual: paste /embed/<bogus-id> → silently redirects to /embed
  • Manual: bare /embed with legacy localStorage threadId → welcome state (no teleport)

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview, Comment May 21, 2026 9:20pm

Request Review

blove and others added 2 commits May 21, 2026 14:07
…hread

PR #500 introduced URL-based thread routing with the intent that the URL
would replace localStorage as the persistence layer. PR #514 partly
walked that back by re-introducing a localStorage `threadId` fallback to
fix mode-switch sync — but that fallback conflates URL state with
browser-local state and silently teleports users to old threads when
they navigate to bare-mode URLs (paste link, back button).

This finishes the URL-as-truth migration:

- Drops `threadId` from `PaletteState`.
- Removes the persistence write effect + persistence-read fallback in
  the URL→signal sync and `threadIdSignal` initialiser.
- Removes the persistence clear in `validateUrlThreadId`'s 404 handler.
- Keeps PR #514's `untracked` guard on the URL→signal effect — that
  guard prevents the stamp-in-progress signal from being cleared during
  the async URL navigation gap. It works without the persistence layer.
- Keeps PR #504's `UrlMatcher` collapse (the stream-survival fix).
- Keeps PR #500's `getThread()` validator + 404 redirect.

Mode-switch UI continues to preserve the active thread across mode
boundaries via `onModeChange` (URL navigation to `/<next-mode>/<id>`),
which was the bug PR #514 was trying to fix. That path didn't need
localStorage — it just needed the URL navigation to carry the id.

Tests:
- "does not write the active thread id to localStorage (URL is the
  source of truth)" — new
- "ignores any legacy persisted threadId — bare mode URLs start fresh"
  — new (covers users who upgrade with legacy localStorage state)
- "hydrates the active thread id from /<mode>/<threadId> URLs" — new
- "does not clear an agent-created thread id while URL navigation is
  still pending" — retained from PR #514

Spec at `docs/superpowers/specs/2026-05-20-url-thread-routing-design.md`
rewritten to match the simplified architecture; was describing the
pre-#504 6-route world and the pre-#514 sync flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four specs were reading the active thread id from
\`localStorage['ngaf-chat-demo:palette'].threadId\` — which no longer
exists after the persistence layer was dropped. One spec asserted
cross-mode persistence via bare /<mode> navigation, which now lands
on the welcome state by design (URL is the sole source of truth).

Changes:
- New helper \`activeThreadIdFromUrl(page)\` in test-helpers.ts —
  parses \`/<mode>/<threadId>\` URL shape.
- lifecycle.spec.ts:27 — "New chat (sidenav)…" now asserts URL flips
  to bare /embed on welcome state, then sends again to verify a
  fresh thread id replaces the prior one (reads from URL, not
  localStorage).
- mode-routing.spec.ts:39 — "cross-mode persistence…" captures the
  thread id after first send, then navigates to /<other-mode>/<id>
  explicitly. Bare /<mode> would clear the thread by design.
- model-picker.spec.ts:12 — reads threadId from URL via the helper.
- regenerate.spec.ts:5 — same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidenav 'New chat' button calls \`onNewThread\` which creates a
new thread server-side and sets \`threadIdSignal\` to the new id —
the signal→URL effect then navigates to /embed/<new-thread-id>. The
URL does NOT go back to bare /embed; the welcome state renders
because the new thread is empty, not because the URL is bare.

Drops the incorrect \`expect(page).toHaveURL(/\\/embed\$/)\` assertion
and removes the redundant second send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@blove blove merged commit 0a6bd08 into main May 21, 2026
25 checks passed
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