Support shared conversations in the V2 interface - #1394
Merged
Paul Lizer (paullizer) merged 3 commits intoSep 3, 2026
Merged
Conversation
Opening a shared conversation in V2 showed an empty thread. V2 loaded every
thread with GET /api/get_messages, which reads only the personal messages
container: for a conversation that is not in it, _authorize_personal_conversation_read
raises LookupError and the route converts that into {'messages': []} with a 200.
So a shared conversation did not fail to open -- it opened successfully, with
nothing in it, and remained the target of the next message sent.
Shared conversations live in their own Cosmos containers behind the existing
/api/collaboration/* API, which the classic interface already drives from
chat-collaboration.js. This routes V2 through it. Front-end only: no Flask
route, schema or server behaviour is changed.
Routing is decided by conversation_kind, which the feed already returns on every
row, rather than by trying one endpoint and falling back -- the personal endpoint
does not fail for a shared conversation.
What V2 gains:
- Reading a shared thread, with sender attribution, the `file` display role, and
reply quoting. Another participant's message sits on the left rather than the
reader's own side.
- Live updates over the conversation's event stream: other people's messages,
deletions, masks, membership changes and typing.
- Sending, under the classic rule: the assistant answers only when the message
@-mentions a model or agent, or an assistant-implying option is set. Otherwise
the message goes to the participants and the model never sees it.
- An @ mention menu offering participants, then AI targets, then people who could
be invited -- and inviting them when chosen.
- A participants panel behind a People button and a Share item in the rail,
covering invitations, roles, removal, and leaving versus deleting.
- Pending generated-file approvals.
Retry, edit, attempt navigation and fork are hidden in a shared conversation.
Those endpoints read the personal messages container and have no collaboration
counterpart, so hiding them is the parity rather than a reduction. Stream
recovery is likewise off: /api/chat/stream/reattach is keyed on a hidden source
conversation whose id the browser is never given.
Three things worth knowing, each of which is easy to get wrong and was:
- Event payloads carry the permissions of whoever *triggered* the event, not the
reader's, because every publishing route serializes the conversation for the
acting user and broadcasts that one document. Applying it verbatim disabled
every other participant's composer when somebody left, and offered every member
a "Delete for everyone" button when an owner acted. conversationFactsOnly
strips the viewer-scoped fields; a membership change is a reason to re-read.
- The event stream replays its whole history on every attach, and EventSource
reconnects by itself, so events are both replay-guarded and de-duplicated.
- Capability flags are deny-by-default while unknown, and the collaboration store
refuses a membership write for any conversation that is not the open one.
Mentions now consume their matched span, so writing "@ada Lovelace" no longer
also notifies somebody called "Ada".
Tests: test_v2_shared_conversations.py asserts the wiring, the gating and the
hidden operations; test_v2_shared_conversation_logic.mjs executes the send rule,
the mention grammar and the event handling against the real modules.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Conflicts, and how each was resolved: - config.py / release_notes.md: the base renumbered to 0.261.036 while this branch was on 0.261.034, so the shared-conversation entries move to a new 0.261.037 section above it and the base's 0.261.034 agent-picker entry is kept as it stands. - chatStore.ts: the base replaced the separate model / agent / reasoning assignments with buildSelectionFields(), which enforces that an agent wins because a model identity sent alongside agent_info reads to the server as an override of it. The @model and @agent tag overrides added here now feed that function rather than bypassing it. A tagged model additionally clears the agent selection: without that, the exclusivity rule would let a picked agent silently discard the model the reader had just named. - Composer.tsx: both branches added an import; kept both. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Conflicts, and how each was resolved: - config.py / release_notes.md: the base took 0.261.037 while this branch was on it, so the shared-conversation entries move to 0.261.038 above the base's diagram section, which is kept as it stands. - MessageList.tsx: the base memoised the message bubble, renaming MessageBubble to MessageBubbleInner. Kept that rename alongside the ReplyQuote and FileMessage components added here; both are used from inside the bubble, so neither is affected by the memo boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Sep 3, 2026
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.
The bug
Opening a shared conversation in the V2 interface showed an empty thread.
V2 loaded every thread with
GET /api/get_messages, which reads only the personal messages container. For a conversation that is not in it,_authorize_personal_conversation_readraisesLookupErrorand the route converts that into{'messages': []}with a 200:So a shared conversation did not fail to open — it opened successfully, with nothing in it, and remained the target of the next message sent.
Shared conversations live in
cosmos_collaboration_conversations_container/cosmos_collaboration_messages_container, behind the existing/api/collaboration/*API that the classic interface already drives fromchat-collaboration.js. The conversation feed already merged them, which is why the rows appeared in the rail at all — only the per-conversation operations were missing.Front-end only. No Flask route, Cosmos container or server behaviour is changed. Every endpoint used already existed.
What V2 gains
Routing is decided by
conversation_kind, which the feed already returns on every row — never by trying one endpoint and falling back, because the personal endpoint does not fail for a shared conversation.filedisplay role, and reply quoting. Another participant's message sits on the left rather than the reader's own side.@-mentions a model or agent, or an assistant-implying option is set (agent, document search, web search, image generation, deep research, URL access, or a saved prompt). Otherwise the message goes to the participants and the model never sees it. Taken frombuildCollaborativeInvocationTargetso the two interfaces cannot disagree about what a given message does.@mention menu offering participants, then AI targets, then people who could be invited — and inviting them when chosen.Retry, edit, attempt navigation and fork are hidden in a shared conversation. Those endpoints read the personal messages container and have no collaboration counterpart, so hiding them is the parity rather than a reduction. Stream recovery is likewise off —
/api/chat/stream/reattachis keyed on a hidden source conversation whose id the browser is never given.Gated on
enable_collaborative_conversations; with it off, no sharing controls appear at all.Three things that were easy to get wrong, and were
Caught in review and fixed:
conversationFactsOnlynow strips the viewer-scoped fields, and a membership change is treated as a reason to re-read rather than a payload to trust.Also fixed along the way: mentions now consume their matched span, so writing
@Ada Lovelaceno longer also notifies somebody calledAda.Tests
test_v2_shared_conversations.pytest_v2_shared_conversation_logic.mjstsc -b --noEmitandnpm run buildcleantest_v2_shared_conversations.py14/14,test_v2_shared_conversation_logic.mjs45/45test_v2_*.py, docs coverage, docs quality and version-guardrail suites passactiveConversationIdwrite is mirrored" assertion actually fails when a mirror call is removedThree existing tests were updated where they pinned source deliberately changed here; their intent is preserved and, in two cases, strengthened.
Merges with the base
The base moved twice while this was in progress, and both merges are in the history with their resolutions explained:
buildSelectionFields()now enforces that an agent wins over a model identity. The@model/@agenttag overrides feed that function rather than bypassing it, and a tagged model additionally clears the agent selection — without that, the exclusivity rule would silently discard the model the reader had just named.MessageBubblewas memoised and renamed toMessageBubbleInner. The rename is kept;ReplyQuoteandFileMessageare used from inside the bubble, so neither is affected by the memo boundary.Renumbered to 0.261.038.
Known limitation
Deep research, URL access and source review make a message a request to the assistant, but
_build_collaboration_stream_request_payloaddoes not forwarddeep_research_enabled,source_review_enabledorurl_access_enabledto the chat stream it bridges to. That is existing server behaviour affecting the classic interface identically, so it is documented rather than changed here.Documentation
docs/explanation/features/V2_SHARED_CONVERSATIONS.mddocs/guides/collaborate-in-a-conversation.md