Skip to content

[codex] fix examples chat thread sync#514

Merged
blove merged 1 commit into
mainfrom
codex/fix-examples-chat-thread-sync
May 21, 2026
Merged

[codex] fix examples chat thread sync#514
blove merged 1 commit into
mainfrom
codex/fix-examples-chat-thread-sync

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 21, 2026

Summary

  • Fix the canonical chat demo thread sync so an agent-created thread cannot be cleared by stale bare URL state before router navigation catches up.
  • Restore persisted active-thread fallback for bare mode routes so /embed, /popup, and /sidebar keep the same conversation when no explicit URL thread id is present.
  • Bound the examples/chat e2e CI job and upload the actual Playwright test-results/ trace path on failure.

Root Cause

The shell's URL-to-signal synchronization tracked threadIdSignal() while comparing it to urlThreadId(). When the agent created a thread, the signal update could retrigger the URL sync effect while the URL was still /embed, clearing the thread back to null. The backend run completed, but the UI returned to the welcome state, causing Playwright to wait until timeout for missing chat messages.

Test Plan

  • npx nx test examples-chat-angular -- --runInBand
  • focused Playwright rerun for lifecycle, mode routing, model picker, and regenerate specs
  • CI=1 npx nx e2e examples-chat-angular --skip-nx-cache
  • node --test scripts/ci-workflow.spec.mjs

@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 6:43pm

Request Review

@blove blove enabled auto-merge (squash) May 21, 2026 18:41
@blove blove merged commit f55a3ba into main May 21, 2026
37 of 38 checks passed
blove added a commit that referenced this pull request May 21, 2026
…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>
blove added a commit that referenced this pull request May 21, 2026
…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>
blove added a commit that referenced this pull request May 21, 2026
…hread (#518)

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

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>

* test(examples-chat): update e2e specs for URL-as-truth

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>

* test(examples-chat): correct lifecycle 'New chat' URL expectation

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>

---------

Co-authored-by: Claude Opus 4.7 (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.

1 participant