perf(agent-chat): stream chat events over per-thread Tauri Channels#88
Merged
Zeus-Deus merged 3 commits intoJun 9, 2026
Merged
Conversation
Agent-chat runtime events — including the high-frequency streaming
content_delta tokens — were broadcast to every listener via the global
Tauri event bus (app.emit). Tauri's event system is not designed for
high-throughput streaming; Channels are the documented mechanism for
it, and PTY output in this repo already streams that way.
Backend:
- Add AgentChatChannelRegistry (Tauri-managed state): per-thread
tauri::ipc::Channel<AgentChatEventPayload> entries, newest attach
wins, generation-guarded detach so a stale unmount can never tear
down a newer pane's channel (mirrors the PTY output_channel pattern).
- New commands attach_agent_chat_output(thread_id, channel) -> u64
generation and detach_agent_chat_output(thread_id, generation).
- forward_event now routes thread-scoped events to the matching
thread's channel only; thread-less RuntimeWarnings keep the global
agent_chat_event bus. DB transcript persistence is unchanged, so the
replay split is: channel = live events only, late-attaching/resumed
panes hydrate from agent_chat_list_messages as before.
Frontend:
- useAgentChatEvents now registers a Channel per mounted thread
(attach on mount / generation-guarded detach on unmount, handler on
a ref so re-renders never re-attach) instead of filtering a global
listen("agent_chat_event") stream.
- Remove the onAgentChatEvent bus subscription; add command wrappers.
Dev mock:
- Simulate the channel path in plain-browser dev: a seeded
chat-streaming workspace boots an agent_chat pane, and send_turn
streams token-by-token content_delta frames through the registered
@tauri-apps/api Channel dispatcher with ordered {index, message}
frames, exactly like the real IPC layer.
Tested:
- Rust: registry unit tests + integration tests (per-thread routing,
no cross-thread leakage, ordering across the async bridge,
generation-guarded detach, threadless-warning bus fallback,
persistence without a subscriber, full provider->bridge->channel
pipeline with a mock provider).
- Frontend: vitest coverage for the hook lifecycle (attach, dispatch,
filter, generation detach, no re-attach on handler change).
- E2E (mock IPC): drove the chat pane in the browser; DOM grew
monotonically during a turn (incremental token rendering) and a
decoy thread channel received zero events.
- E2E (real IPC): ran the dev app against an isolated HOME with a
real Claude session; the reply streamed as content_delta frames over
the thread's channel in order (session_configured -> deltas ->
item_completed -> turn_completed), nothing on the event bus, and a
decoy channel on another thread received zero events.
Closes #75
…-chat-stream-agent-chat-events-over-a-tauri
…-chat-stream-agent-chat-events-over-a-tauri # Conflicts: # src/dev/mock-fixtures.ts # src/dev/tauri-mock.ts
This was referenced Jun 10, 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.
Closes #75
Summary
Agent-chat runtime events — including the high-frequency streaming
content_deltatokens — were broadcast to every listener via the global Tauri event bus (app.emit). Tauri's event system is not designed for high-throughput streaming; Channels are the documented mechanism for it, and PTY output in this repo already streams that way. This PR moves per-thread chat streaming ontotauri::ipc::Channel, mirroringattach_pty_output.Backend
AgentChatChannelRegistry(Tauri-managed state): per-threadChannel<AgentChatEventPayload>entries, newest attach wins, generation-guarded detach so a stale unmount can never tear down a newer pane's channel.attach_agent_chat_output(thread_id, channel) -> u64generation,detach_agent_chat_output(thread_id, generation).forward_eventroutes thread-scoped events to the matching thread's channel only; thread-lessRuntimeWarnings keep the low-frequency globalagent_chat_eventbus.docs/features/agent-chat.md): the channel carries live events only. DB transcript persistence is unchanged, so late-attaching / resumed panes hydrate fromagent_chat_list_messagesexactly as before.Frontend
useAgentChatEventsregisters aChannelper mounted thread (attach on mount, generation-guarded detach on unmount, handler kept on a ref so re-renders never re-attach) instead of filtering a globallisten("agent_chat_event")stream.onAgentChatEventbus subscription removed;attachAgentChatOutput/detachAgentChatOutputwrappers added.Dev mock
chat-streamingworkspace boots anagent_chatpane andsend_turnstreams token-by-tokencontent_deltaframes through the registered@tauri-apps/apiChannel dispatcher with ordered{index, message}frames — same mechanism as real IPC.Acceptance criteria
Testing
spawn_event_bridge→ channel pipeline with a mock provider and realtauri::ipc::Channelconsumers.tscclean; 1832/1832 vitest including new hook lifecycle coverage (attach, dispatch, wrong-thread filter, generation detach, no re-attach on handler identity change, thread switch).HOMEwith a real Claude session; the reply streamed ascontent_deltaframes over the thread's channel in order (session_configured → deltas → item_completed → turn_completed), nothing thread-scoped appeared on the event bus, and a decoy channel on a second thread received zero events. Session stop + detach were clean.cargo check,cargo test(only 2 pre-existing machine-local failures unrelated to this change — both reproduce identically at clean HEAD),npm run check,npm run testall verified on the merged tree.