Skip to content

Chat Events (chat:*)

The chat:* namespace handles all communication between the Python ChatManager and the chat frontend. Events flow in both directions: user messages travel JS → Python, while assistant responses, artifacts, and state updates travel Python → JS.

Availability

Chat events are only active when content is rendered via app.show_chat() or the ChatManager component. They require the chat frontend assets.

User Messages (JS → Python)

Event Payload Description
chat:user-message {messageId, text, threadId, timestamp, attachments?} User sends a message. messageId (e.g. "msg_abc12345") is generated by the frontend and stored on both sides so the same message can be addressed later for editing or resending.
chat:stop-generation {threadId, messageId} User clicks stop button to cancel in-progress generation. Sets cooperative cancel event.
chat:edit-message {messageId, threadId, text} User submits edited text for a prior user message. Backend replaces the message, drops everything after it, asks the provider to forget its own state for that thread, and re-runs generation under the same messageId.
chat:resend-from {messageId, threadId} User re-runs generation starting at a specific user message. The target user message and everything after it are dropped, then regeneration runs.
chat:slash-command {command, args, threadId} User submits a /command from the input bar (e.g., /clear, /export).
chat:input-response {text, requestId, threadId} User responds to a free-form chat:input-required prompt mid-stream (text / buttons / radio). Permission decisions use chat:permission-response instead.
chat:request-state {} Frontend requests full state snapshot on initialization.
chat:request-history {threadId, limit} Frontend requests message history for a thread.

chat:user-message attachment structure:

{
    "type": "file" | "widget",
    "name": str,
    "path": str,             # Desktop only (filesystem path)
    "content": str,          # Browser/inline (file content)
    "widgetId": str,         # For widget attachments
    "componentId": str
}

Message identifiers

Every message the chat manager stores carries an id field alongside role and text:

{"id": "msg_abc12345", "role": "user", "text": "..."}
{"id": "msg_a1b2c3d4", "role": "assistant", "text": "..."}

User-message ids are generated on the frontend and passed through chat:user-message; assistant-message ids are generated by the manager and used as messageId on every stream chunk. chat:edit-message and chat:resend-from address messages by this id — the UI's edit/resend buttons send the id from the data-msg-id attribute on the bubble.

Thread Management (JS → Python)

Event Payload Description
chat:thread-create {title} Create a new conversation thread.
chat:thread-switch {threadId} Switch active thread and replay its message history.
chat:thread-delete {threadId} Delete a thread and switch to the next available one.
chat:thread-rename {threadId, title} Rename a thread.

Settings & Todos (JS → Python)

Event Payload Description
chat:settings-change {key, value} User changed a settings menu item (e.g., temperature slider, model select).
chat:todo-clear {} User dismissed the todo list above the input bar.
chat:permission-response {toolCallId, optionId, threadId} User clicked one of the options offered by a chat:permission-request. Resumes the paused generation with the chosen option id.

Assistant Responses (Python → JS)

Event Payload Description
chat:assistant-message {messageId, text, threadId, role?, stopped?} Complete (non-streamed) assistant message. Also used to replay history on thread switch.
chat:stream-chunk {messageId, chunk, done, stopped?} Incremental text chunk during streaming. Flushed every 30 ms or 300 characters.
chat:typing-indicator {typing, threadId?} Show or hide the typing indicator before/after streaming.
chat:generation-stopped {messageId, partialContent} Generation was cancelled or stopped by the user or system.
chat:messages-deleted {threadId, messageIds: [str], editedMessageId?, editedText?} The backend truncated the thread — either because the user hit Edit or Resend on a prior message. The frontend drops every bubble whose id is in messageIds, removes any thinking/tool blocks tied to those message ids, and — if editedMessageId/editedText are present — re-renders the edited user message in place with the new text.

Reasoning & Status (Python → JS)

Event Payload Description
chat:thinking-chunk {messageId, text, threadId} Incremental reasoning/thinking text (rendered in a collapsible block).
chat:thinking-done {messageId, threadId} Thinking stream complete — collapses the thinking block and shows character count.
chat:status-update {messageId, text, threadId} Transient status message (e.g., "Searching..."). Shown inline, not stored in history. The manager emits this with text: "" at the end of every stream to clear any persistent banner left behind by provider updates (e.g. LangGraph's "Thinking..." status).

Tool Use (Python → JS)

Event Payload Description
chat:tool-call {messageId, toolId, toolCallId, title, name, kind, status, threadId} Announces a tool invocation. Rendered as a collapsible <details> card with a spinner. Emitted when ToolCallUpdate(status="in_progress") is yielded.
chat:tool-result {messageId, toolId, toolCallId, name, kind, status, result, isError, threadId} Final result of the invocation. Frontend looks up the existing tool-call card by toolId (data-tool-id attribute), removes the spinner, updates the status icon, and appends the result text inside the card. Emitted when ToolCallUpdate(status="completed") or ToolCallUpdate(status="failed") is yielded — the manager routes by status so a second "tool-call" event is never created for the same run.

The toolId and toolCallId fields carry the same value; both are emitted so handlers written against older PyWry releases (which used toolCallId only) keep working while the newer data-tool-id DOM attribute also gets populated.

Interactive Input (Python → JS)

Event Payload Description
chat:input-required {messageId, threadId, requestId, prompt, placeholder?, inputType?, options?} Pause streaming to request free-form user input (inputType: "text" / "buttons" / "radio"). Typically emitted by callback / custom providers. Response arrives on chat:input-response.
chat:permission-request {toolCallId, title, options: [{id, label, kind?}], requestId, threadId} Pause streaming to ask the user to approve / deny a tool invocation. Payload mirrors the ACP PermissionRequestUpdate fields (no prompt / placeholder / description). Response arrives on chat:permission-response.

Handler pattern for permission requests:

from pywry.chat.updates import PermissionRequestUpdate
from pywry.chat.session import PermissionOption

def my_handler(messages, ctx):
    yield PermissionRequestUpdate(
        toolCallId="call_1",
        title="Execute deployment script",
        options=[
            PermissionOption(id="allow_once", label="Allow"),
            PermissionOption(id="reject_once", label="Reject"),
        ],
    )

Rich Content (Python → JS)

Event Payload Description
chat:artifact {messageId, artifactType, title, threadId, ...} Rich content artifact (code, chart, table, image, etc.).
chat:citation {messageId, url, title, snippet, threadId} Source citation/reference link.
chat:todo-update {items} Push a todo list above the input bar. Not stored in history.
chat:plan-update {entries: [{content, priority, status}]} Push a structured plan (to-do card) above the input bar. Emitted by agent providers that expose an explicit planning phase (e.g. Deep Agents write_todos).

Artifact types and type-specific fields:

artifactType Additional Fields
code content, language
markdown content
html content
table rowData, columns, columnTypes, columnDefs?, gridOptions?, height
plotly figure, height
image url, alt
json data

Todo item structure:

{
    "id": int | str,
    "title": str,
    "status": "not-started" | "in-progress" | "completed"
}

State & Configuration (Python → JS)

Event Payload Description
chat:state-response {threads, activeThreadId, messages, settingsItems, contextSources} Full state snapshot in response to chat:request-state.
chat:clear {threadId?} Clear all messages from the chat display.
chat:update-thread-list {threads} Refresh the sidebar thread list after create/delete/rename.
chat:switch-thread {threadId} Tell the frontend to switch the active thread.
chat:load-assets {scripts, styles} Lazy-inject AG Grid or Plotly libraries on first artifact of that type.
chat:register-command {name, description} Register a slash command in the input autocomplete palette.
chat:register-settings-item {id, label, type, value, options?, min?, max?, step?} Register a settings menu item in the gear dropdown.
chat:context-sources {sources} List of dashboard components available as @-mentionable context sources.
chat:update-settings {key: value, ...} Push updated settings values to the frontend menu.
chat:config-update {options: [{id, label, description?}]} Update the registered command palette when a provider adjusts its slash-command surface mid-session.
chat:mode-update {currentModeId, availableModes: [ModeInfo]} Broadcast a change in the active agent mode (e.g. planner / builder / chat). The mode picker in the chat header rerenders from this payload.
chat:commands-update {commands: [{name, description}]} Replace the full slash-command palette. Used when the provider resets or switches its command set.

Settings item types: action, toggle, select, range, separator