—kit

AI SDK adapter

useKitTurn maps useChat messages onto Thinking and Tool call. It imports nothing from the SDK.

# Preview

Tool part stateToolCall status
input-streamingPENDING
input-availableRUNNING
approval-requestedAWAITING
approval-responded (approved)RUNNING
approval-responded (denied)DENIED
output-available (preliminary)RUNNING
output-availableDONE
output-errorERROR
output-deniedDENIED

Steps, in part order: one REASONING per reasoning part, one TOOL NAME per tool part, one WRITING for the first text part. A data-kit-step part with { label } names its own step.

Timings are client-observed. AI SDK parts carry no timestamps, so the adapter stamps the clock when it first sees a part and when its state changes. They include network latency. A message loaded from history was never seen live, so it gets no durations at all: the readouts simply do not render.

Tokens come only from message metadata at finish. There is no live token counter, because the client never receives one.

server
// app/api/chat/route.ts: send usage so TOKENS can render
const result = streamText({ model, messages: convertToModelMessages(messages), tools });
return result.toUIMessageStreamResponse({
  messageMetadata: ({ part }) =>
    part.type === "finish" ? { usage: part.totalUsage } : undefined,
});

# Install

shell
npx shadcn@latest add https://kit.ulasalyesil.com/r/ai-sdk.json

Add @import "../styles/hud.css"; to your global CSS, after @import "tailwindcss". Zero dependencies beyond React.

# Usage

tsx
import { useKitTurn } from "@/lib/ai-sdk";

const { messages, status } = useChat();
const last = messages.findLast((m) => m.role === "assistant");
const turn = useKitTurn({ message: last, status });

{turn.thinking && <Thinking {...turn.thinking} />}
{turn.tools.map((t) => <ToolCall key={t.toolCallId} {...t} />)}

# Props

PropTypeDefaultDescription
messageUIMessage | undefinedUsually the last assistant message. Structural type, AI SDK v5 to v7.
status"submitted" | "streaming" | "ready" | "error"useChat's status.
clock() => numberperformance.nowWhen each part is first observed and when it changes state.
labelFor(part) => string | undefinedOverride step labels. A data-kit-step part names its own step.

# Presets

[ TOOL: SEARCH ][PENDING]
search pending
pendingstatus="pending"
[ TOOL: SEARCH ][RUNNING]
search running
runningstatus="running"
[ TOOL: SEARCH ][AWAITING]
search awaiting approval
awaitingstatus="awaiting"
[ TOOL: SEARCH ][DONE]
search done
donestatus="done"
[ TOOL: SEARCH ][ERROR]

Request timed out

search failed
errorstatus="error"
[ TOOL: SEARCH ][DENIED]
search denied
deniedstatus="denied"

# Agent prompt

Paste into Claude Code, Cursor or any coding agent to wire it in.

prompt
Add the —kit AI SDK adapter adapter to this project.
1. Run: npx shadcn@latest add https://kit.ulasalyesil.com/r/ai-sdk.json
2. Make sure the global CSS imports styles/hud.css after tailwindcss.
3. Import: import { useKitTurn } from "@/lib/ai-sdk";
4. Props:
   - message: UIMessage | undefined. Usually the last assistant message. Structural type, AI SDK v5 to v7.
   - status: "submitted" | "streaming" | "ready" | "error". useChat's status.
   - clock: () => number. When each part is first observed and when it changes state.
   - labelFor: (part) => string | undefined. Override step labels. A data-kit-step part names its own step.
5. Truth rule: only pass values you actually have. Leave a prop out rather than
   passing a placeholder; its readout will not render.
6. With the Vercel AI SDK: npx shadcn@latest add https://kit.ulasalyesil.com/r/ai-sdk.json
   then: const turn = useKitTurn({ message: lastAssistant, status })