studio: back the "reply in thread" indicator with a real, daemon-persisted side conversation
Problem
studio/ already has UI for Slack-style side threads, built as part of the Atrium vendor
drop (#618) but never wired to anything real:
ReplyIndicator renders
the avatar stack / "N replies" / "Last reply HH:MM" row — but it reads
message.replies, a field (AgentMessage.replies)
that nothing in the codebase ever populates. It never renders today.
ThreadPanel opens on
"Reply in thread" and lets you type — but the "assistant" reply is a hardcoded string
("Picking this up in the thread…"), not a real model turn, and the whole exchange lives
in local React state only. Reload the page, or open the session on another device, and
it's gone.
That's a direct contradiction of ADR 0229,
which the rest of studio/ follows strictly: the daemon's session store is the single
source of truth, and "a second browser must see nothing" is treated as the bug class this
ADR exists to close. This thread stub is prototype residue that slipped past the #618
import filtering.
Separately, real delegation activity (Subagent/Team/Parallel — see
use-agent-chat.ts) is turned into
inline badges live during a stream, but isn't reconstructed from GET /transcript on
reload, so it also disappears on refresh. Worth fixing alongside this, since it's the same
"activity that happened off the main line isn't durable" shape.
Proposal
Make "reply in thread" a real feature: a side conversation anchored to one message, with a
real model reply, that survives reload and is visible from any client — matching what
ReplyIndicator already implies it can do.
Recommended shape — a thread is a child session anchored to a message.
Reuse the existing session-store/resumability machinery instead of inventing a new engine
primitive:
- A thread is an ordinary daemon session, tagged with its parent
(sessionId, messageId).
Creating one seeds its history from the parent message (± surrounding context, mirroring
the existing Subagent fork: true seeding pattern) so the model has context without a
new prompt-assembly path.
- New wire surface: something like
POST /v1/sessions/{id}/messages/{turnId}/thread to
create/get a thread, and the thread session then behaves like any other session (real
POST /prompt turns, GET /transcript, listed in GET /v1/sessions).
- Thread sessions are filtered out of the flat top-level sidebar the same way subagent/team
member rows already are — via a decoder-level inspect_only_kind-style reason (see
sessions.ts) — not id-prefix guessing.
- The indicator's data comes almost for free: "N replies" = the thread session's own
messageCount, "Last reply" = its updatedAt, participants = the same role/avatar data
MessageBubble already renders for ordinary messages. No new engine-level "reply count"
or "avatar" concept needed.
- Thread creation is lazy — only on first actual send — so browsing a session with no
threads costs nothing extra.
Cheaper fallback, if backend scope is too big for v1 — drop the human-authoring half
and re-skin existing delegation participants (Subagent/Team/Parallel) as the "repliers" for
a turn, Studio-only, no engine change. This gets the visual pattern but not the "user
chooses to branch a side conversation" behavior implied by the mock, and still needs the
reload-persistence fix for delegation events called out above.
Open questions for whoever picks this up
- Does a thread reply actually re-invoke the full agent (tools, permissions) or a
lighter-weight text-only completion scoped to the parent message? The former is more
useful but reintroduces permission-ask UI inside a side panel.
- Should threads be creatable on any message (including a user's own), or only on
assistant turns, as the mock suggests?
- Given the size, this may want to go through
/to-acceptance-plan rather than being
implemented as one shot — flagging here rather than pre-deciding it.
Non-goals (v1)
- No change to Subagent/Team/Parallel delegation semantics themselves — this is additive,
human-initiated, and orthogonal to model-initiated delegation.
- No multi-level threads (a thread cannot itself have a side thread).
- No mecatui (TUI) equivalent in this issue — the TUI's fleet/roster overlays already cover
a related-but-distinct need there; worth its own issue if wanted.
Acceptance
ReplyIndicator renders only for a message with a real, persisted thread that has at
least one reply, sourced from daemon data — never from local-only state.
- Sending a message in
ThreadPanel produces a real model turn (not a canned string) and
survives a full page reload.
- Opening the same session from a second browser/tab shows the same thread state.
- A session's transcript, once reloaded, still shows delegation badges for turns that had
them (closing the related live-only gap).
- Thread sessions do not appear as extra rows in the top-level chat sidebar.
studio: back the "reply in thread" indicator with a real, daemon-persisted side conversation
Problem
studio/already has UI for Slack-style side threads, built as part of the Atrium vendordrop (#618) but never wired to anything real:
ReplyIndicatorrendersthe avatar stack / "N replies" / "Last reply HH:MM" row — but it reads
message.replies, a field (AgentMessage.replies)that nothing in the codebase ever populates. It never renders today.
ThreadPanelopens on"Reply in thread" and lets you type — but the "assistant" reply is a hardcoded string
("Picking this up in the thread…"), not a real model turn, and the whole exchange lives
in local React state only. Reload the page, or open the session on another device, and
it's gone.
That's a direct contradiction of ADR 0229,
which the rest of
studio/follows strictly: the daemon's session store is the singlesource of truth, and "a second browser must see nothing" is treated as the bug class this
ADR exists to close. This thread stub is prototype residue that slipped past the #618
import filtering.
Separately, real delegation activity (Subagent/Team/Parallel — see
use-agent-chat.ts) is turned intoinline badges live during a stream, but isn't reconstructed from
GET /transcriptonreload, so it also disappears on refresh. Worth fixing alongside this, since it's the same
"activity that happened off the main line isn't durable" shape.
Proposal
Make "reply in thread" a real feature: a side conversation anchored to one message, with a
real model reply, that survives reload and is visible from any client — matching what
ReplyIndicatoralready implies it can do.Recommended shape — a thread is a child session anchored to a message.
Reuse the existing session-store/resumability machinery instead of inventing a new engine
primitive:
(sessionId, messageId).Creating one seeds its history from the parent message (± surrounding context, mirroring
the existing Subagent
fork: trueseeding pattern) so the model has context without anew prompt-assembly path.
POST /v1/sessions/{id}/messages/{turnId}/threadtocreate/get a thread, and the thread session then behaves like any other session (real
POST /promptturns,GET /transcript, listed inGET /v1/sessions).member rows already are — via a decoder-level
inspect_only_kind-style reason (seesessions.ts) — not id-prefix guessing.messageCount, "Last reply" = itsupdatedAt, participants = the same role/avatar dataMessageBubblealready renders for ordinary messages. No new engine-level "reply count"or "avatar" concept needed.
threads costs nothing extra.
Cheaper fallback, if backend scope is too big for v1 — drop the human-authoring half
and re-skin existing delegation participants (Subagent/Team/Parallel) as the "repliers" for
a turn, Studio-only, no engine change. This gets the visual pattern but not the "user
chooses to branch a side conversation" behavior implied by the mock, and still needs the
reload-persistence fix for delegation events called out above.
Open questions for whoever picks this up
lighter-weight text-only completion scoped to the parent message? The former is more
useful but reintroduces permission-ask UI inside a side panel.
assistant turns, as the mock suggests?
/to-acceptance-planrather than beingimplemented as one shot — flagging here rather than pre-deciding it.
Non-goals (v1)
human-initiated, and orthogonal to model-initiated delegation.
a related-but-distinct need there; worth its own issue if wanted.
Acceptance
ReplyIndicatorrenders only for a message with a real, persisted thread that has atleast one reply, sourced from daemon data — never from local-only state.
ThreadPanelproduces a real model turn (not a canned string) andsurvives a full page reload.
them (closing the related live-only gap).