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:

SurfaceWhere
ChatPanelThe full chat page, with the conversation list
ChatPreviewPanelBeside the canvas, for testing the workflow you are editing
ChatPreviewPageThe standalone preview
CliChatPanelThe 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:

Four rules govern it, and they exist because of bugs that the obvious implementation causes:

  1. 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.
  2. Explicit references win over automatically selected or focused blocks.
  3. A draft- document id is never sent to the backend as a persistent documentId.
  4. 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_modeBehaviour
once (default)Applies to the next turn, then clears automatically
alwaysStays 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:

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