Skip to content

perf(agent-chat): stream chat events over per-thread Tauri Channels#88

Merged
Zeus-Deus merged 3 commits into
mainfrom
feature/75-perf-agent-chat-stream-agent-chat-events-over-a-tauri
Jun 9, 2026
Merged

perf(agent-chat): stream chat events over per-thread Tauri Channels#88
Zeus-Deus merged 3 commits into
mainfrom
feature/75-perf-agent-chat-stream-agent-chat-events-over-a-tauri

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Owner

Closes #75

Summary

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. This PR moves per-thread chat streaming onto tauri::ipc::Channel, mirroring attach_pty_output.

Backend

  • AgentChatChannelRegistry (Tauri-managed state): per-thread Channel<AgentChatEventPayload> entries, newest attach wins, generation-guarded detach so a stale unmount can never tear down a newer pane's channel.
  • New commands: attach_agent_chat_output(thread_id, channel) -> u64 generation, detach_agent_chat_output(thread_id, generation).
  • forward_event routes thread-scoped events to the matching thread's channel only; thread-less RuntimeWarnings keep the low-frequency global agent_chat_event bus.
  • Replay split (documented in docs/features/agent-chat.md): the channel carries live events only. DB transcript persistence is unchanged, so late-attaching / resumed panes hydrate from agent_chat_list_messages exactly as before.

Frontend

  • useAgentChatEvents registers a Channel per 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 global listen("agent_chat_event") stream.
  • onAgentChatEvent bus subscription removed; attachAgentChatOutput / detachAgentChatOutput wrappers added.

Dev mock

  • The plain-browser dev mock now simulates the channel path: 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 — same mechanism as real IPC.

Acceptance criteria

  • Streaming tokens arrive over a Channel, not the global event bus
  • No cross-thread leakage — a pane only receives its own thread's events
  • Late-attaching / resumed panes still render the full transcript (DB hydrate + live channel)
  • No regression in streaming smoothness or event ordering

Testing

  • Rust: registry unit tests + 16 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, and the full provider → spawn_event_bridge → channel pipeline with a mock provider and real tauri::ipc::Channel consumers.
  • Frontend: tsc clean; 1832/1832 vitest including new hook lifecycle coverage (attach, dispatch, wrong-thread filter, generation detach, no re-attach on handler identity change, thread switch).
  • E2E (mock IPC, browser): drove the chat pane in the browser; DOM text length grew monotonically during a turn (incremental token rendering), and a decoy channel on another thread received zero events while the target thread received its full ordered stream.
  • E2E (real IPC): ran the dev desktop 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 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 test all verified on the merged tree.

Zeus-Deus added 3 commits June 9, 2026 21:06
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

# Conflicts:
#	src/dev/mock-fixtures.ts
#	src/dev/tauri-mock.ts
@Zeus-Deus Zeus-Deus merged commit debeca9 into main Jun 9, 2026
4 checks passed
@Zeus-Deus Zeus-Deus deleted the feature/75-perf-agent-chat-stream-agent-chat-events-over-a-tauri branch June 9, 2026 19:57
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.

[perf][agent-chat] Stream agent-chat events over a Tauri Channel instead of the global event bus

1 participant