V2: put the open conversation back in the address bar - #1384
Merged
Paul Lizer (paullizer) merged 3 commits intoSep 2, 2026
Merged
Conversation
The classic interface has supported linking to a conversation since v0.237.001:
chat-onload.js reads ?conversationId= (or the older ?conversation_id=) on load, and
chat-conversations.js writes the open conversation back with replaceState. The V2 SPA
had neither half, so /v2/chat never named what was open, copying the URL shared
nothing, and a refresh landed in an empty chat.
The read and the write are deliberately asymmetric: the URL is read once, during the
chat page's first render, and written on every change of the open conversation after
that. The incoming id is captured in a lazy useState initialiser because the write
effect also runs on mount and would otherwise strip the parameter before the read
effect ever saw it, and the write is held until the open settles so it cannot observe
the moment before the conversation exists.
Both parameter spellings are accepted, since the server emits both, and only the
canonical one is written. A conversation older than the first feed page, or hidden,
gets a list row built from the metadata already being fetched, so the rail highlights
it instead of the header reading "New chat".
Existence is checked against the metadata endpoint rather than inferred from the
message load. /api/get_messages is not an existence check: it turns a not-found
conversation into {'messages': []} with a 200, so a deleted conversation would have
opened as an empty chat, kept its id in the URL, and stayed the target of the next
message sent.
Back to classic UI now carries the conversation across.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The base moved on twice while this was open: PR #1383 (web search and AI notices) and PR #1386 (inline image proposals). Both conflicts were version bookkeeping rather than code -- chatStore.ts auto-merged, and the two branches touch different parts of it. VERSION goes to 0.261.030. 0.261.028 was taken by the notices work and 0.261.029 by the image proposals, so this claims the next free number rather than reusing one; the mermaid branch is taking 0.261.031. The release note entry moves to a new 0.261.030 section above both of theirs, which are left intact, and the feature doc keeps both its testing-table rows. Verified after resolving: no leftover markers, the feature doc differs from the base only by additions, the full V2 suite (24 Python files plus the inline image proposal runtime checks) passes, both docs tests pass, and the bundle builds against the merged dependencies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
PR #1385 (Mermaid diagram rendering) landed first and took 0.261.031, so this branch's 0.261.030 was no longer ahead of the base and both sides claimed a changelog section at the top. The version moves to 0.261.032 in all four places that carry it -- config.py, the release note section heading, the test's docstring header and its assert_app_version_at_least floor -- rather than adopting the base's 031 and leaving this feature documented under a number that never shipped containing it. The base's VERSION must strictly increase on every merge into it; keeping 031 would have left two different base commits indistinguishable by version, which is the one thing a running deployment can report about itself. The gap where 030 was costs nothing. Note that 032 is on neither side of the conflict: this is not a pick-one resolution, which is the shape most easily resolved by reflex. Changelog reads 032 -> 031 -> 029 -> 028, with the diagram sections intact and this branch's delta being additions only. Verified: no leftover markers, no lingering 030 reference anywhere, the V2 suite (24 Python files plus the inline image proposal runtime checks) passes, the two diagram tests arriving with the base pass, both docs tests pass, and the bundle builds. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds conversation deep linking to the V2 interface, so a link opens the conversation it names.
Reported against
https://…/chats?conversationId=67553a4a-…— that link works in the classic interface but had no V2 equivalent.The gap
The classic interface has supported this since v0.237.001:
chat-onload.jsreads?conversationId=(or the older?conversation_id=) on load, andchat-conversations.jswrites the open conversation back withhistory.replaceState.The V2 SPA had neither half.
useSearchParamsappeared exactly once in all of V2, inSettingsPage.tsxfor?tab=. So/v2/chatnever named what was open, copying the address bar shared nothing, and a refresh landed in an empty chat.Approach
The read and the write are deliberately asymmetric, and that is the whole design: the URL is read once, during the chat page's first render, and written on every change of the open conversation after that.
Two ordering hazards are handled explicitly, because both fail silently:
useStateinitialiser, which runs during the first render. The write effect also runs on mount and would otherwise strip the parameter before the read effect ever saw it.replacerather than a push, matching classic'sreplaceState: opening ten conversations should not put ten entries behind the back button.Changes
lib/conversationUrl.ts(new)pages/ChatPage.tsxuseConversationUrlSync()stores/chatStore.tsopenLinkedConversation(), plus a conversation-list backfillcomponents/layout/Sidebar.tsxBoth spellings are accepted on arrival because the server emits both —
conversationIdfromfunctions_notifications.pyandfunctions_workflow_runner.py,conversation_idfromroute_frontend_chats.pyandfunctions_documents.py. An incoming legacy link is rewritten to the canonical spelling, so a URL never carries both.First send, fork, New Chat and delete all came free, since they all end at
activeConversationId.A conversation reached by link need not be in the loaded list — it can be older than the first feed page (
FEED_PAGE_SIZE = 30) or hidden (include_hiddendefaults false). Its row is built from the metadata the chat page already fetches, so the rail highlights it rather than the header reading "New chat" for a thread that is plainly open.Worth a reviewer's attention
The first cut detected a bad link by checking
messagesErrorafter loading. That misses deleted conversations:/api/get_messagescatches the not-foundLookupErrorand answers{'messages': []}with a 200 (route_backend_conversations.py:1068). A deleted conversation would have opened as an empty chat, kept its id in the address bar, and stayed the target of the next message sent.Existence is now checked against
/api/conversations/<id>/metadata, which answers 404 when the conversation is gone and 403 when it is someone else's — the question actually being asked. That costs one extra request on a path that runs once per page load.Scope
V2 only, as agreed. Server-generated links (notifications, workflow runs, document sources) still point at the classic
/chatsand are not made interface-aware.Validation
npm run typecheckandnpm run buildcleanfunctional_tests/test_v2_conversation_deep_link.py: 8/8test_docs_app_surface_coverage.pyandtest_docs_site_quality.py: 0 failuresAlso in this change
config.py→0.261.028, a "Linking to a conversation" section inREACT_V2_UI.mdwith a row in its testing table, and a release-notes entry.No
docs/_data/app_surface.ymlregeneration needed:scripts/build_docs_inventory.pyscansapplication/single_apponly, and this adds noenable_*key, admin tab, action plugin or chat control.