Skip to content

Global chat: receive more streaming statuses#4969

Open
hanna-paasivirta wants to merge 2 commits into
mainfrom
global-assistant-multiple-text-events
Open

Global chat: receive more streaming statuses#4969
hanna-paasivirta wants to merge 2 commits into
mainfrom
global-assistant-multiple-text-events

Conversation

@hanna-paasivirta

@hanna-paasivirta hanna-paasivirta commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Apollo will start streaming the global chat response much earlier, so the user waits less before seeing output. To support this, it sends several types of streaming events, including status updates. This PR receives them in Lightning.

Apollo changes: OpenFn/apollo#573

nice and as the last step can you add a

How it works

There are two kinds of status:

  • Transient "thinking" statuses. These replace each other and disappear. They use the existing scalar streamingStatus mechanism (dots + italic text).
  • Persistent "action done" statuses (e.g. "Edited workflow structure"). These are new. They stay in the message, woven between text blocks in the order they arrived.

To make persistent statuses survive a reload, assistant messages get a new response_segments field: a timeline of {"type": "text" | "status", "content": string} entries. The final Apollo response payload still contains exactly what it did before, including the full final message in response, so history building and everything downstream of content is unchanged. Segments are display-only.

Closes OpenFn/apollo#572

Backend

  • A new status SSE event from Apollo is broadcast to the session channel as streaming_segment. The payload is byte-identical to a response_segments entry, so live and reloaded timelines render through one code path.
  • New nullable response_segments column on ai_chat_messages (migration included, additive, backward compatible). The changeset validates the segment shape; AiAssistant.normalize_response_segments/1 filters invalid entries and falls back to nil, which means flat legacy rendering.
  • The channel now serializes response_segments on messages.
  • MessageProcessor handles {:error, %Ecto.Changeset{}} from save, marking the message as errored instead of crashing.
  • Small fix along the way: streaming requests carry a lazy stream as the response body, so error bodies can no longer be indexed like maps. error_message_from_body/1 guards this and falls back to a generic HTTP error message.

Frontend

  • Store: new streamingSegments timeline. Text chunks grow the trailing text segment, or open a new one after a status. Reset alongside the other streaming fields.
  • AIChannelRegistry: statuses arriving over the wire are pinned to their position in the typewriter char buffer, and only enter the timeline once the drain reaches that position. This keeps the visible order identical to wire order despite the drain delay. A persistent status also clears any active thinking status immediately.
  • MessageList: renders the woven timeline when segments exist (streaming, gated to the global assistant; or persisted response_segments), otherwise falls back to the flat content path. Non-global chats are unchanged.
  • Streamed workflow YAML now applies regardless of which page the panel is on, because it writes to the shared Y.Doc and the diagram must be current when the user navigates there. Job code previews stay page-gated. The __streaming__ message id is trusted as global, since a workflow YAML streamed while a job is open can only come from the global assistant.
  • Note on how applied changes surface to the user: changes apply automatically to steps the user is not currently viewing. If the user is already on a step whose code changed, they see a diff instead. How we present these diffs needs more work; that is out of scope for these initial changes and will come in a follow-up.

Validation steps

  1. Check out the corresponding changes in Apollo Global chat: Add answer streaming to planner apollo#573
  2. Test generating job code and new steps from different views (canvas, inside the step being edited, inside a different step)

Additional notes for the reviewer

Blast radius: which chats does this touch?

This is a global chat feature, but much of the code it changes is shared with job chat and workflow chat. Global chat is experimental; job and workflow chat are live services, so the shared paths deserve the careful review.

Global chat only (lighter review):

  • Persisting response_segments on save (build_global_message).
  • The woven streaming timeline in the UI (gated on the global assistant being active; persisted segments only exist on global messages).

Shared with job and workflow chat (careful review):

  • The SSE pipeline (process_stream / handle_sse_event) is one code path for all three streaming endpoints. The new status handler lives there. Apollo only sends status events on the global endpoint today, but if it ever sent them for job or workflow chat they would flow through.
  • The streaming error-body fix (error_message_from_body), the ChatMessage schema/changeset, the MessageProcessor changeset-error handling, and channel message serialization.
  • The MessageList render refactor: the flat rendering path all chats use was restructured into a conditional. Non-global chats must render exactly as before.
  • The AIChannelRegistry char-drain loop now flushes status markers on every tick, for all chats' streams.
  • The streamed-YAML apply change is a behavior change for workflow chat, not just global. Previously streamed YAML only applied when the panel was on the workflow_template page. Now it applies from any page, and the __streaming__ message id bypasses the workflow-template guard in useAIWorkflowApplications for all streams, including non-global workflow chat. This is intentional (workflow YAML writes to the shared Y.Doc), but it is the one place where live-service behavior changes.

What to review

  1. Ordering logic in AIChannelRegistry.ts. This is the trickiest part. pendingStatusMarkers must flush exactly when the drain passes their buffer position, including the final flush inside drainThenRun before the message finalizes. Check the reset path (stopDraining) clears markers so nothing leaks into the next stream.
  2. The page-independence change for streamed YAML in AIAssistantPanelWrapper.tsx and useAIWorkflowApplications.ts (see blast radius above). The claim is that a YAML in streamingChanges while a job is open can only come from the global assistant. Non-global workflow chat also streams YAML, so please sanity-check whether a workflow chat stream can be active while the user has a job open, and whether applying in that case is acceptable. Note the applied-ref is now only set when a change is actually handled, so a change that could not apply stays eligible if the user switches pages mid-stream.
  3. Backend segment validation. Unknown segment types are silently dropped and an all-invalid list becomes nil (flat message). Confirm silent degradation is the behavior we want, versus rejecting.
  4. Rendering fallbacks. Old messages, non-global messages, and error paths must all still render flat content. The tests cover this, but it is the main regression surface.

AI Usage

Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):

  • I have used Claude Code
  • I have used another model
  • I have not used AI

You can read more details in our
Responsible AI Policy

Pre-submission checklist

  • I have performed an AI review of my code (we recommend using /review
    with Claude Code)
  • I have implemented and tested all related authorization policies.
    (e.g., :owner, :admin, :editor, :viewer)
  • I have updated the changelog.
  • I have ticked a box in "AI usage" in this PR

@hanna-paasivirta hanna-paasivirta marked this pull request as ready for review July 14, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New Issues

Development

Successfully merging this pull request may close these issues.

Global chat: Fix planner streaming

1 participant