@kognitivedev/ui
v0.2.28
Published
Provider-agnostic React components for building agentic chat UIs with Kognitive
Downloads
1,037
Maintainers
Readme
@kognitivedev/ui
React providers, primitives, components, and hooks for building provider-agnostic chat UIs with Kognitive.
Installation
bun add @kognitivedev/uiPeer dependencies: react, zod
Quick Start
The simplest setup is to point the UI at browser-reachable app routes:
"use client";
import { KognitiveUI } from "@kognitivedev/ui";
export default function ChatPage() {
return (
<KognitiveUI serverUrl="http://localhost:3000" agentName="assistant">
<KognitiveUI.Thread />
</KognitiveUI>
);
}The UI sends and receives Kognitive-native message and stream events. It does not depend on ai or @ai-sdk/react.
Product Shells
@kognitivedev/ui is organized in three practical layers:
KognitiveUIas the runtime/provider boundary- default components such as
Thread,ThreadList,Composer, andMessage - primitives such as
ThreadPrimitive,ThreadListPrimitive,ComposerPrimitive, andMessagePrimitivefor custom shells
The new create-kognitive starter uses the package in this product-shell mode:
"use client";
import {
ComposerPrimitive,
KognitiveUI,
MessagePrimitive,
ThreadListPrimitive,
ThreadPrimitive,
} from "@kognitivedev/ui";
export default function ChatPage() {
return (
<KognitiveUI
api="/api/kognitive/agents/assistant/stream"
agentName="assistant"
threads={{ apiBase: "/api/kognitive/threads/agents/assistant" }}
>
<div className="app-shell">
<ThreadListPrimitive.Root>
<ThreadListPrimitive.New>New chat</ThreadListPrimitive.New>
<ThreadListPrimitive.Items>
{(thread, isActive) => (
<ThreadListPrimitive.Item key={thread.sessionId} thread={thread} isActive={isActive}>
<span>{thread.title}</span>
</ThreadListPrimitive.Item>
)}
</ThreadListPrimitive.Items>
</ThreadListPrimitive.Root>
<ThreadPrimitive.Root>
<ThreadPrimitive.Messages>
{(message, index) => (
<MessagePrimitive.Root key={message.id} message={message} index={index}>
<MessagePrimitive.Content />
</MessagePrimitive.Root>
)}
</ThreadPrimitive.Messages>
<ComposerPrimitive.Root>
<ComposerPrimitive.Input placeholder="Ask anything" />
<ComposerPrimitive.Send>Send</ComposerPrimitive.Send>
</ComposerPrimitive.Root>
</ThreadPrimitive.Root>
</div>
</KognitiveUI>
);
}Features
- Drop-in
<KognitiveUI.Thread /> - Headless primitives for custom product shells and layouts
useKognitiveChat()for hook-level control- Tool call rendering with
makeToolUI()andtoolkit() - File attachments
- Message editing and branch management
- Thread management and runtime event hooks
Custom Tool UIs
import { makeToolUI } from "@kognitivedev/ui";
import { weatherTool } from "./tools";
const WeatherUI = makeToolUI({
tool: weatherTool,
render: ({ input, output, state }) => (
<div>{input.city}: {state === "output-available" ? output?.temperature : "Loading..."}</div>
),
});Tool rendering lifecycle
Tool UIs are rendered once per toolCallId. The stream may emit a tool-call and then a tool-result,
but <KognitiveUI /> groups those into one invocation before rendering.
input is pulled from the tool-call and output is filled from the tool-result as soon as it arrives.
The state field advances with the tool lifecycle (input-available, approval-requested, output-available, etc.),
so each renderer can animate progress without creating a second widget.
This means toolkit({ ... }) and makeToolUI({ ... }) usage does not change; the rendering behavior is
handled inside MessageContent.
Standalone Hook
import { useKognitiveChat } from "@kognitivedev/ui";
function MyChat() {
const { messages, send, status, stop } = useKognitiveChat({
serverUrl: "http://localhost:3000",
agentName: "assistant",
});
return (
<div>
{messages.map((message) => (
<div key={message.id}>{message.role}</div>
))}
<button onClick={() => send("Hello")}>Send</button>
<button onClick={stop} disabled={status !== "streaming"}>Stop</button>
</div>
);
}Auto-submitting after completed tool calls
useKognitiveChat and <KognitiveUI /> support an optional sendAutomaticallyWhen callback.
If provided, it is evaluated when stream processing finishes (status becomes ready) and can
re-submit the current message history automatically.
No tool output callback is required, and auto-submit is opt-in (sendAutomaticallyWhen is undefined by default).
Predicates can return boolean or PromiseLike<boolean>.
import {
KognitiveUI,
lastAssistantMessageIsCompleteWithToolCalls,
} from "@kognitivedev/ui";
function DemoChat() {
return (
<KognitiveUI
serverUrl="http://localhost:3000"
agentName="assistant"
sendAutomaticallyWhen={lastAssistantMessageIsCompleteWithToolCalls}
>
<KognitiveUI.Thread />
</KognitiveUI>
);
}You can also provide a custom predicate that inspects the latest messages and decides when to re-submit:
import { useKognitiveChat } from "@kognitivedev/ui";
function MyChat() {
const { messages, send, status } = useKognitiveChat({
serverUrl: "http://localhost:3000",
agentName: "assistant",
sendAutomaticallyWhen: ({ messages }) => {
const last = messages.at(-1);
if (!last || last.role !== "assistant") return false;
return last.parts.some((part) => part.type === "tool-call");
},
});
return <></>;
}The predicate receives full UI messages with existing tool-call and tool-result parts and runs only after the assistant stream is ready.
Routing
serverUrlpoints the browser at your app server.apilets you override the chat endpoint when needed.threads={{ apiBase }}lets you override the thread endpoint base when your product uses custom thread routes.
Server-side Kognitive.baseUrl in @kognitivedev/core is different. It points to the Kognitive backend/cloud and should not be passed to browser UI components.
Bring Your Own API
Use api when your app needs a business-specific backend contract:
<KognitiveUI
api="/api/chat"
agentName="assistant"
resourceId={{ userId: "user-1" }}
threads={{ apiBase: "/api/chat/threads" }}
/>The chat endpoint should accept:
{
"messages": [],
"sessionId": "session_123",
"resourceId": { "userId": "user-1" },
"agentName": "assistant"
}sessionId is top-level. resourceId should carry user and tenant context, not duplicate the public session identifier.
If you enable built-in threads, the thread API base should expose:
GET /threadsPOST /threadsGET /threads/:sessionIdPATCH /threads/:sessionIdDELETE /threads/:sessionId
Each endpoint should use the same public sessionId the chat API sees.
Automatic thread titles are generated from the stream execution flow and persisted by the backend after conversation snapshots are stored.
Stable Primitive Attributes
The primitive roots expose stable data attributes so custom shells can style and test against runtime state without depending on private internals:
ComposerPrimitive.Root:data-disabled,data-has-input,data-has-files,data-drag-overThreadPrimitive.Root:data-status,data-streaming,data-emptyMessagePrimitive.Root:data-role,data-editing,data-branch-count,data-branch-indexThreadListPrimitive.Root:data-empty,data-has-active-threadThreadListPrimitive.Item:data-active,data-status,data-session-id
