@k2b/cloud
Cloud assistant chat
Cloud AI messages and composer adapters bound to Cloud sessions, turns, tools, and attachments.
The generic timeline owns chat presentation. Cloud projects its persisted messages and active turn into that contract.
TSX
Copyconst items = createAiChatTimeline({ messages: () => [], activeTurn: () => null }); <AiChatActionsProvider> <Chat.Timeline items={items()} emptyTitle="Start a conversation" /></AiChatActionsProvider>The generic composer owns interaction and accessibility. Cloud only adapts model profiles and outgoing payloads.
TSX
Copy<Chat.Composer value={draft()} onValueChange={setDraft} models={aiChatModelOptions(models)} selectedModelId={selectedModelId()} onModelChange={setSelectedModelId} commands={commands} contextUsage={{ usage: latestUsage, loopUsage: latestLoopUsage, contextWindow: 262_000, modelLabel: "vLLM Qwen 3.6", }} onSubmit={(input) => sendMessage(aiComposerSendInput(input))}/>The Cloud adapter connects AI sessions, turns, tools, attachments, retries, forks, and usage snapshots to Chat from @k2b/ui.
Use Cloud assistant chat
Use the Cloud adapter only for the platform AI protocol. Standalone Solid applications and other backends use Chat directly.
Import
import {
AiChatActionsProvider,
aiChatAttachments,
aiChatModelOptions,
aiComposerSendInput,
createAiChatTimeline,
} from "@k2b/cloud/ai/ui";
import { Chat } from "@k2b/ui";Cloud ownership
createAiChatTimelinereactively maps persisted messages and the active turn toChatTimelineItem[]. Each assistant turn shows one work line, its results, its newest text, and its actions; while the turn runs, a newest text that a step follows reads as a quiet status, and only a text that may be the answer keeps answer typography; the live turn and its history share item ids, so finishing a turn moves nothing. Passreconnectingfrom the controller's stream status: while the stream reconnects or a model call waits for its retry, the work line says so in place and its clock stands. PasstimeZone, such as the page'sgetTimeZone(c), so receipt times and dates in server-rendered history match the browser.AiChatActionsProviderbinds approval, outcome-check, frontend-tool, retry, fork, message-feedback, and file behavior to rich Cloud blocks. Every Action question and its outcome render as card v4 fromChat.Decision,Chat.Preview, andChat.Outcome; passonCheckActionso an unknown outcome can be checked again without repeating the Action, andonContinueTurnso an expired question can be asked again through a visible message.aiChatModelOptionsandaiChatAttachmentsmap Cloud records into portable values.aiComposerSendInputmapsChatSubmitInputback to the Cloud controller input.
The application still owns the current controller and mutation callbacks. Cloud persistence, streaming, tools, approvals, and files remain outside @k2b/ui.
The controller keeps a submitted message visible while attachments upload and the server accepts the draft. This local send is separate from server snapshots: a reconnect cannot remove it, and the consumed draft revision identifies its confirmation without rendering a duplicate message.
Accessibility
Chat owns common keyboard, focus, status, and accessible-name behavior. Cloud renderers preserve visible status and accessible names for domain-specific tool blocks.
Runtime
Persisted messages, live turns, attachments, retries, tools, feedback, and mutations require the Cloud AI controller and authenticated platform routes. Message feedback belongs to the private conversation owner and never enters model context.
Example
const Conversation = () => {
const items = createAiChatTimeline({
messages: chat.messages,
activeTurn: chat.activeTurn,
reconnecting: () => chat.streamStatus() === "reconnecting",
});
return (
<Chat>
<Chat.Timeline
items={items()}
loading={chat.loadingConversation()}
hasMore={chat.hasMoreHistory()}
onLoadOlder={chat.loadOlderMessages}
/>
<Chat.Composer
value={draft()}
onValueChange={setDraft}
state={chat.runStatus() === "stopping" ? "stopping" : chat.running() ? "running" : "idle"}
models={aiChatModelOptions(availableModels())}
selectedModelId={selectedModelId()}
onModelChange={setSelectedModelId}
attachments={aiChatAttachments(attachments())}
onSubmit={(input) =>
input.intent === "steer"
? chat.steer(input.text)
: chat.send(aiComposerSendInput(input))
}
onStop={chat.abort}
contextUsage={{
usage: latestUsage(),
loopUsage: latestLoopUsage(),
contextWindow: selectedContextWindow(),
modelLabel: selectedModelLabel(),
}}
/>
</Chat>
);
};
<AiChatActionsProvider actions={messageActions}>
<Conversation />
</AiChatActionsProvider>;