Embedded Chat in the Editor
Applies to: ChengOS v0.1.0+ | Last verified: 2026-08-12 | Source:
chengflow-ui/src/features/chat,chengflow-ui/src/features/chat/hooks/useConversationWebSocket.ts,chengflow-ui/src/features/chat/utils/buildChatContextPayload.ts,crates/cheng-api/src/rest/handlers/conversation
The chat panel in the editor is not a demo widget bolted onto the workflow engine. It is a first-class execution surface: a message you send starts a workflow run, and everything the run emits — streaming tokens, tool calls, agent iterations, approval requests — is rendered back into the conversation as it happens.
The surfaces
The same chat machinery is composed into four places:
| Surface | Where |
|---|---|
ChatPanel | The full chat page, with the conversation list |
ChatPreviewPanel | Beside the canvas, for testing the workflow you are editing |
ChatPreviewPage | The standalone preview |
CliChatPanel | The CLI session mirrored into the browser |
They differ in how they track workspace, conversation, and workflow ids — the preview panel
mints local placeholder conversation ids (local-…) before a real conversation exists — so the
shared rules live in one shared toolbar rather than being duplicated four times. A placeholder
id is treated as "no conversation", and no request is ever made against an id the backend has
never seen.
A turn, end to end
you type ──► context assembly ──► POST /conversations/:id/messages
│
workflow execution starts
│
WebSocket events ◄────────────┘
│
message_created · node_stream_* · tool_execution ·
agent_iteration_completed · agent_turn_* · approval_requested ·
message_completed
│
▼
the assistant bubble updates in place
The chat panel subscribes to the conversation's event stream over the shared global WebSocket.
Because a user can switch conversations mid-run, the subscription tracks an
expectedExecutionId and discards events belonging to a run the user has navigated away from —
without that, a slow execution would leak its tokens into whatever conversation is on screen.
If the browser reconnects mid-run, the panel does not simply wait for the next event: it fetches the execution trace snapshot and merges it, so a refresh in the middle of a long agent turn recovers the tool calls already made rather than showing a gap.
Context assembly
What you actually send is more than the text in the box. One assembler builds the payload for every surface, from two sources:
- Explicit references — documents, blocks, and table records you deliberately attached.
- Automatic context — the currently open document, its selected or focused blocks, and the active table.
Four rules govern it, and they exist because of bugs that the obvious implementation causes:
- Automatic context is derived on demand, never written into the chat store. Copying it in leaves stale data behind as soon as you switch tabs.
- Explicit references win over automatically selected or focused blocks.
- A
draft-document id is never sent to the backend as a persistentdocumentId. - The assembler builds the payload; it does not send it and does not clear references. Sending and cleanup belong to the caller.
The resulting payload carries finalContent, an optional documentId, focusedBlockIds (the
union of explicit and automatic block ids, deduplicated), and a tableContext. The context
indicator in the UI shows exactly what will be attached, so nothing travels invisibly.
Routing controls: shortcuts and presets
Above the input sit two selectors that change how the next turn is routed.
Shortcuts attach a structured routing value to the next turn instead of injecting raw JSON
into your message text. That distinction matters: chat history stays clean, and the model never
sees route-control JSON as if it were natural language. The value must be a JSON object,
because the primary consumer is ai/llm_branch, which routes on nested-path conditions —
action: { name: search } rather than a flat action.name key.
Two lifetimes:
route_mode | Behaviour |
|---|---|
once (default) | Applies to the next turn, then clears automatically |
always | Stays active across turns until an explicit close signal or you deselect it |
The toolbar watches for a turn finishing and invalidates the cached active shortcut when the
mode was once — the backend has consumed it, so the checkmark must go away.
Presets apply a named configuration patch to a node in the active workflow. Humans may apply any preset; an LLM-initiated switch is gated behind approval and limited to presets explicitly marked LLM-visible. See Presets & Shortcuts.
There is also a model selector and a workflow selector, so you can retarget a conversation without leaving it.
Rich message rendering
The assistant bubble is a projection over several event streams, not a single string:
- Streaming text arrives as
node_stream_*chunks and is appended live. - Tool calls render as their own cards, keyed so that a call and its later result merge into one entry instead of appearing twice.
- Provider reasoning is kept separate from the answer text.
- Agent iterations and turns are grouped, so a ten-step agent run reads as a sequence rather than a wall.
- Approvals appear as an inline approval card; agent reviews as a review card. Deciding in the card resumes the paused execution — see Approvals & Human Review.
- Attachments and artifacts are normalised into media attachments, so an image an agent produced displays inline.
Rewind: editing an earlier message
Selecting one of your earlier messages and editing it rewinds the conversation to that point.
The cutoff is the selected message's sequence_number, used inclusively — never
sequence_number - 1. Persisted conversations require a real numeric sequence number, so
optimistic client-side ids (temp-, streaming, local-msg-) are rejected as rewind targets
rather than being rolled back approximately.
Before rewinding, the panel estimates the impact and asks for confirmation when messages would be discarded, and warns when the turn contained attachments that cannot be restored.
Rewind is a rollback followed by a resend, and the transport deliberately does not swallow its own failures: "the rollback succeeded but the resend failed" is a state the UI has to be able to report, which is only possible if the send path propagates the error.
Conversations
Conversations belong to a workspace and are listed in the sidebar. The backend resolves or
creates them via POST /conversations/resolve for channel traffic, or directly for editor
traffic; messages are persisted, so a conversation survives a reload and can be resumed from any
surface, including the CLI.
Next steps
- Messaging Channels — the same conversations, reached from WhatsApp or Slack.
- Channel Routing & Session Mapping — how an external message finds its workflow.
- Interactive Testing & Debugging — the other way to run a workflow from the editor.