AI data and commands
Keep observable message state separate from application decisions and side effects.
Messages and parts
ChatMessage requires id, actor (user, assistant, system) and ordered parts. Optional status is queued, streaming, complete, stopped or error. createdAt accepts Date or string; serialize dates explicitly for storage. metadata carries application records. Ghost does not validate your persisted database schema.
ChatPart discriminates text, file, source, citation anchor, tool, reasoning summary, activity, input request, custom data, error and unknown content. Preserve meaningful IDs for tools, files, sources and requests; assign stable IDs to changing text/custom parts too. Array order records transcript order. The default Message composition can group user attachments separately. Do not regroup all tools or sources ahead of interleaved text unless your product explicitly calls for that composition.
Adapters retain message IDs and merge tool results at the matching call's position. Anonymous text parts receive distinct IDs by their text-part order within the message; streaming text growth and insertion of other part types preserve those IDs. Supply explicit IDs if text parts can be inserted or reordered. They cannot invent a missing tool identity safely: malformed identities/states become errors or unknown/read-only content. Preserve the original SDK record in your own store when you need fields outside the normalized contract; the normalized view model is not a lossless SDK backup.
Approval is not execution
ToolPart.state describes input, approval, running, output, error or cancellation. Its approval has a separate ID and pending/approved/denied state. An approved request does not mean a tool ran or succeeded. Use the output/error state reported by the executing application. Never run a tool because a component became visible or because an approval callback exists.
bindApproval sends a decision through the supplied SDK command. Pass getCurrent so an obsolete or automatic request cannot be approved from a stale view. The application must still enforce authorization, idempotency and persistence at the execution boundary. Clarification uses a request ID plus nonnegative revision; its binding prevents duplicate answers for that revision and leaves acknowledgement to the application.
The shared runtime adds current-scope guards and explicit command capability descriptors around this existing controller; its optional React binding leaves SDK/store state with the caller.
Cancellation and capability
ChatController requires a send(ComposerSubmission) command and a status. stop, retry, editAndResubmit and respondToToolApproval are optional. Only offer those controls when the application can perform them. A stop request can abort transport without cancelling a remote tool; show stopped/cancelled only when the corresponding owner confirms it. Ignore late results from superseded runs using application identity/revision checks.
Adapter capability flags describe native normalization/command support. activity: false means no automatic SDK activity stream: caller-provided activityEvents still render through the same canonical Activity view. attachments: false means outbound attachment conversion is application-owned; normalized inbound files can still render. Both default send bindings reject nonempty attachments. nativeReasoning: false does not authorize exposing private reasoning.
Extend the same view
Use mapData for application data parts, mapInputRequest for explicit clarification, and mapAuthor/mapDelivery for identity/receipt metadata. classifyReasoning must explicitly identify a provider or application summary; reasoning is dropped by default. Do not classify a private trace as a summary to make it visible.
MessageParts.renderers replaces rendering by part type and receives the ordered-part context. Dispatch a data part by its name in your renderer; keep application-specific schemas outside the core union. Preserve the existing view parts and user-controlled disclosure when replacing a snapshot. Use lower-level Message, Tool, Activity or Citation parts when the default renderer does not fit your layout.
Reference and troubleshooting
Import contracts from @ghost-ui/core/chat-model; import MessagePartsProps and PartRenderer from the message-parts component subpath. SDK integrations describes adapters. A hidden reasoning summary usually needs explicit classification. An unknown tool result usually lacks a matching earlier call. Missing actions usually mean an unsupported or absent command; do not manufacture an action to hide that gap.