Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@timbal-ai/timbal-react

React components and runtime for building Timbal chat UIs. Drop in a single component to get a fully-featured streaming chat interface connected to a Timbal workforce agent.

Installation

npm install @timbal-ai/timbal-react
# or
bun add @timbal-ai/timbal-react

Peer dependencies:

npm install react react-dom @assistant-ui/react @timbal-ai/timbal-sdk

Tailwind setup

The package ships pre-built Tailwind class names plus a complete light + dark token set (bg-background, text-foreground, bg-card, bg-bubble-user, from-elevated-from, bg-playground-from/via/to, etc.). Your app CSS only needs three lines:

/* src/index.css */
@import "tailwindcss";
@import "@timbal-ai/timbal-react/styles.css";
@source "../node_modules/@timbal-ai/timbal-react/dist";

That's it — no @theme, :root, or .dark blocks of your own. Toggling dark mode is a single document.documentElement.classList.toggle("dark") (or next-themes attribute="class").

Adjust the @source path if your CSS file lives at a different depth relative to node_modules.

Overriding the palette

Every token has a CSS-variable indirection in styles.css. Override individual variables to rebrand without forking:

:root {
  --primary: oklch(0.5 0.12 265);
  --playground-from: oklch(0.95 0.04 265 / 0.6);
}

.dark {
  --primary: oklch(0.72 0.14 265);
  --playground-from: oklch(0.27 0.04 265);
}

Both light AND dark blocks must be defined for every overridden token — otherwise toggling dark mode produces an inconsistent UI. The library prints a one-time dev-only console warning when it detects a mismatch.

CSS imports

Import these stylesheets once in your app entry:

// src/main.tsx
import "@assistant-ui/react-markdown/styles/dot.css";
import "katex/dist/katex.min.css";

Quick start

Basic usage

TimbalChat is a single component that handles everything — runtime, streaming, messages, and the composer:

import { TimbalChat } from "@timbal-ai/timbal-react";

export default function App() {
  return (
    <div style={{ height: "100vh" }}>
      <TimbalChat workforceId="your-workforce-id" />
    </div>
  );
}

TimbalChat requires a fixed height parent. Use height: "100vh" or flex-1 min-h-0 depending on your layout.

Welcome screen and suggestions

<TimbalChat
  workforceId="your-workforce-id"
  welcome={{
    heading: "Hi, I'm your assistant",
    subheading: "Ask me anything about your data.",
  }}
  suggestions={[
    { title: "Summarize this week", description: "Get a quick overview of recent activity" },
    { title: "What can you help with?" },
    { title: "Show me the latest report" },
  ]}
/>

Suggestions also accept a function (sync or async) for per-user or server-driven chips:

<TimbalChat
  workforceId="your-workforce-id"
  suggestions={async () => {
    const res = await authFetch("/api/suggestions");
    return res.json(); // ThreadSuggestion[]
  }}
/>

Each chip supports icon, description, and prompt (sent instead of title when clicked).

Placeholder and width

<TimbalChat
  workforceId="your-workforce-id"
  composerPlaceholder="Type a question..."
  maxWidth="60rem"
  className="my-custom-class"
/>

Switching agents dynamically

Pass key to fully reset the chat when the workforce changes:

const [workforceId, setWorkforceId] = useState("agent-a");

<select onChange={(e) => setWorkforceId(e.target.value)}>
  <option value="agent-a">Agent A</option>
  <option value="agent-b">Agent B</option>
</select>

<TimbalChat workforceId={workforceId} key={workforceId} />

Studio shell (sidebar + header + chat)

TimbalStudioShell is the most opinionated layout — a floating workforce sidebar, a top bar for actions (mode toggle, account), and a full-height TimbalChat. Works as a one-line app:

import {
  TimbalStudioShell,
  ModeToggle,
  TimbalMark,
} from "@timbal-ai/timbal-react";
import { useTheme } from "next-themes";

export default function App() {
  const { resolvedTheme, setTheme } = useTheme();

  return (
    <TimbalStudioShell
      brand={<TimbalMark size={32} />}
      welcome={{ heading: "How can I help you today?" }}
      headerActions={
        <ModeToggle theme={resolvedTheme} setTheme={setTheme} />
      }
      suggestions={[{ title: "Get started" }]}
      attachments
    />
  );
}

When workforceId is omitted, the shell fetches the workforce list and lets the sidebar drive selection. Pass workforceId to pin a single agent and hide the picker UI.

Apps that need finer control can compose the public building blocks (StudioSidebar + TimbalChat) directly:

import { StudioSidebar, TimbalChat } from "@timbal-ai/timbal-react";

function MyShell() {
  const [agent, setAgent] = useState("agent-a");
  return (
    <div className="relative h-dvh bg-background">
      <StudioSidebar selectedId={agent} onSelect={setAgent} />
      <main className="h-full pl-[var(--studio-inset-left)]">
        <TimbalChat workforceId={agent} key={agent} />
      </main>
    </div>
  );
}

The --studio-inset-left CSS variable is automatically set on the document by StudioSidebar, so a normal Tailwind pl-[var(--studio-inset-left)] call resolves to the correct offset.

Drop-in shell (header + agent picker)

TimbalChatShell wraps the common blueprint layout: brand area, workforce selector, optional header actions, and a full-height chat. When workforceId is omitted, it fetches {baseUrl}/workforce and selects the first agent automatically:

import { TimbalChatShell, Button, useSession } from "@timbal-ai/timbal-react";

export default function App() {
  const { logout, isAuthenticated } = useSession();

  return (
    <TimbalChatShell
      brand={<span className="font-semibold">Acme AI</span>}
      headerActions={
        isAuthenticated ? (
          <Button variant="ghost" size="sm" onClick={logout}>
            Log out
          </Button>
        ) : null
      }
      welcome={{ heading: "How can I help you today?" }}
      suggestions={[{ title: "Get started" }]}
    />
  );
}

Pass workforceId to lock the agent and hide the built-in selector. Use hideWorkforceSelector when you render your own picker.

Workforce list hook

For custom layouts (sidebar tree, command palette), use useWorkforces with the optional WorkforceSelector:

import {
  TimbalChat,
  useWorkforces,
  WorkforceSelector,
} from "@timbal-ai/timbal-react";

function ChatWithPicker() {
  const { workforces, selectedId, setSelectedId, isLoading } = useWorkforces();

  if (isLoading) return <div>Loading agents…</div>;

  return (
    <div className="flex h-screen flex-col">
      <WorkforceSelector
        workforces={workforces}
        value={selectedId}
        onChange={setSelectedId}
      />
      <TimbalChat workforceId={selectedId} key={selectedId} className="min-h-0 flex-1" />
    </div>
  );
}

useWorkforces accepts baseUrl, fetch, and pickInitial (custom resolver for the default selection). It returns selected, error, and refresh() as well.


Splitting the runtime and UI

TimbalChat is a convenience wrapper around TimbalRuntimeProvider + Thread. Use them separately when you need to place the runtime above the chat — for example, to build a custom header that reads or controls chat state:

import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";

export default function App() {
  return (
    <TimbalRuntimeProvider workforceId="your-workforce-id">
      <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
        <header>My App</header>
        <Thread
          composerPlaceholder="Ask anything..."
          className="flex-1 min-h-0"
        />
      </div>
    </TimbalRuntimeProvider>
  );
}

Custom API base URL

Useful when your API is mounted at a subpath (e.g. behind a reverse proxy):

<TimbalRuntimeProvider workforceId="your-workforce-id" baseUrl="/api">
  <Thread />
</TimbalRuntimeProvider>

Attachments

Attachments are opt-in. Pass attachments to enable the composer + button, drag-and-drop, and multimodal prompts:

<TimbalChat workforceId="your-workforce-id" attachments />

When enabled, each file is uploaded via POST to ${baseUrl}/files/upload (multipart file field). The response must include { url } (or { signed_url } / { id }). That URL is sent to the workforce as { type: "file", file: "<url>" } alongside { type: "text", text: "..." } when the user typed a message.

Your API must expose that upload route (the Timbal blueprint API includes it). authFetch is used by default and must not force a Content-Type header on FormData uploads.

Variants

// Default upload adapter
<TimbalChat workforceId="..." attachments />

// Custom endpoint or MIME whitelist
<TimbalChat
  workforceId="..."
  attachments={{ uploadUrl: "/api/uploads", accept: "image/*,application/pdf" }}
/>

// Fully custom adapter (e.g. presigned S3)
<TimbalChat workforceId="..." attachments={myAdapter} />

// Explicitly off (default when prop is omitted)
<TimbalChat workforceId="..." attachments={null} />

Power-user exports

import {
  createDefaultAttachmentAdapter,
  createUploadAttachmentAdapter,
  resolveAttachmentAdapter,
  parseSSELine,
  AssistantRuntimeProvider,
  useTimbalStream,
} from "@timbal-ai/timbal-react";

parseSSELine and AssistantRuntimeProvider are re-exported so custom runtimes do not need a second @assistant-ui/react import for those symbols. useTimbalStream exposes the same SSE reducer and send / reload / cancel API without mounting <Thread>.

Custom fetch function

Pass your own fetch to add headers, inject tokens, or proxy requests:

const myFetch: typeof fetch = (url, options) => {
  return fetch(url, {
    ...options,
    headers: { ...options?.headers, "X-My-Header": "value" },
  });
};

<TimbalRuntimeProvider workforceId="your-workforce-id" fetch={myFetch}>
  <Thread />
</TimbalRuntimeProvider>

Customizing the UI

Use the components prop on TimbalChat or Thread to replace any part of the interface while keeping everything else as the default.

Available slots

Slot Props forwarded Default
UserMessage none built-in user bubble
AssistantMessage none built-in assistant bubble
EditComposer none built-in inline edit composer
Composer placeholder (+ full ComposerProps) built-in composer bar
Welcome config, suggestions, Suggestions built-in welcome screen
Suggestions suggestions built-in suggestion chips
ScrollToBottom none built-in scroll button

Custom slot components read their data via hooks — no props are passed automatically except where noted above.

Custom user message

import { TimbalChat, MessagePrimitive } from "@timbal-ai/timbal-react";

const CompactUserMessage = () => (
  <MessagePrimitive.Root className="flex justify-end px-4 py-2">
    <div className="bg-primary text-primary-foreground rounded-2xl px-4 py-2 text-sm max-w-[75%]">
      <MessagePrimitive.Parts />
    </div>
  </MessagePrimitive.Root>
);

<TimbalChat workforceId="..." components={{ UserMessage: CompactUserMessage }} />

Custom composer

The Composer slot receives placeholder from the composerPlaceholder prop:

import { TimbalChat, ComposerPrimitive } from "@timbal-ai/timbal-react";

const MinimalComposer = ({ placeholder }: { placeholder?: string }) => (
  <ComposerPrimitive.Root className="flex items-center gap-2 border rounded-full px-4 py-2">
    <ComposerPrimitive.Input
      placeholder={placeholder ?? "Type here..."}
      className="flex-1 bg-transparent text-sm outline-none"
      rows={1}
    />
    <ComposerPrimitive.Send className="text-primary font-medium text-sm">
      Send
    </ComposerPrimitive.Send>
  </ComposerPrimitive.Root>
);

<TimbalChat workforceId="..." components={{ Composer: MinimalComposer }} />

Custom welcome screen

The Welcome slot is always mounted and controls its own visibility. Use useThread to replicate the default "show only when the thread is empty" behaviour:

import { TimbalChat, useThread, useThreadRuntime, type ThreadWelcomeProps } from "@timbal-ai/timbal-react";

const BrandedWelcome = ({ suggestions }: ThreadWelcomeProps) => {
  const isEmpty = useThread((s) => s.isEmpty);
  const runtime = useThreadRuntime();
  if (!isEmpty) return null;
  return (
    <div className="flex flex-col items-center justify-center h-full gap-4">
      <img src="/logo.svg" className="h-12" />
      <h2 className="text-xl font-semibold">Welcome to Acme AI</h2>
      <div className="flex gap-2 flex-wrap justify-center">
        {suggestions?.map((s) => (
          <button
            key={s.title}
            onClick={() => runtime.append({ role: "user", content: [{ type: "text", text: s.title }] })}
            className="border rounded-full px-4 py-1.5 text-sm hover:bg-muted"
          >
            {s.title}
          </button>
        ))}
      </div>
    </div>
  );
};

<TimbalChat
  workforceId="..."
  suggestions={[{ title: "Get started" }, { title: "Show me an example" }]}
  components={{ Welcome: BrandedWelcome }}
/>

Mixing slots

Override any combination — slots are independent of each other:

<TimbalChat
  workforceId="..."
  components={{
    UserMessage: CompactUserMessage,
    Composer: MinimalComposer,
  }}
/>

Hooks and primitives

These are re-exported from @assistant-ui/react for use inside custom slot components:

Export Use inside
ThreadPrimitive Any slot
MessagePrimitive UserMessage, AssistantMessage, EditComposer
ComposerPrimitive Composer, EditComposer
ActionBarPrimitive UserMessage, AssistantMessage
useThread Any slot — subscribe to thread state (e.g. isRunning, isEmpty)
useThreadRuntime Any slot — call actions (e.g. runtime.append(...))
useMessageRuntime UserMessage, AssistantMessage — edit, reload, branch
useComposerRuntime Composer, EditComposer — access composer state

Artifacts

Agents can return structured JSON artifacts — charts, tables, choice widgets, and interactive UI — instead of plain text. The chat UI renders them automatically from tool results or inline ```timbal-artifact fences.

Tell the agent about the schema

Import the ready-made instruction block and append it to your workforce system prompt (or blueprint tool-result docs):

import { ARTIFACT_AGENT_INSTRUCTIONS } from "@timbal-ai/timbal-react";

const systemPrompt = `${basePrompt}\n\n${ARTIFACT_AGENT_INSTRUCTIONS}`;

ARTIFACT_AGENT_INSTRUCTIONS documents every built-in type (chart, table, question, html, json, ui) and the full interactive ui node palette (hover tooltips, buttons, toggles, sliders, drag).

Subscribe to interactive events

ui artifacts can fire { kind: "emit" } actions (e.g. after a slider commit or drag). Handle them with onArtifactEvent on Thread or TimbalChat:

<TimbalChat
  workforceId="your-workforce-id"
  onArtifactEvent={(event) => {
    console.log(event.name, event.payload);
    // e.g. refetch data, update local UI, call your API
  }}
/>

When using Thread directly:

<TimbalRuntimeProvider workforceId="your-workforce-id">
  <Thread
    onArtifactEvent={(event) => console.log(event.name, event.payload)}
  />
</TimbalRuntimeProvider>

Built-in { kind: "message" } actions already append a user message — you only need onArtifactEvent for host-side logic beyond that.

Custom artifact renderers

Register extra type values or override defaults:

<TimbalChat
  workforceId="..."
  artifacts={{
    renderers: {
      "my:widget": MyWidgetRenderer,
    },
  }}
/>

Extend the interactive palette with host-registered custom nodes:

import {
  UiCustomNodeRegistryProvider,
  TimbalRuntimeProvider,
  Thread,
} from "@timbal-ai/timbal-react";

<UiCustomNodeRegistryProvider renderers={{ "price-card": PriceCard }}>
  <TimbalRuntimeProvider workforceId="...">
    <Thread />
  </TimbalRuntimeProvider>
</UiCustomNodeRegistryProvider>

API reference

TimbalChat props

TimbalChat accepts all TimbalRuntimeProvider props plus all Thread props.

Prop Type Default Description
workforceId string required ID of the workforce to stream from
baseUrl string "/api" Base URL for API calls. Posts to {baseUrl}/workforce/{workforceId}/stream
fetch (url, options?) => Promise<Response> authFetch Custom fetch. Defaults to the built-in auth-aware fetch (Bearer token + auto-refresh)
attachments boolean | { uploadUrl?, accept? } | AttachmentAdapter | null off true or a config object enables the built-in upload adapter; null disables; omitted = off
attachmentsUploadUrl string Shorthand: enables the default adapter with a custom upload URL
attachmentsAccept string Shorthand: MIME accept for the default adapter
debug boolean false Log every parsed SSE event to the console with a [timbal] prefix
welcome.heading string "How can I help you today?" Welcome screen heading
welcome.subheading string "Send a message to start a conversation." Welcome screen subheading
suggestions { title: string; description?: string }[] Suggestion chips on the welcome screen
composerPlaceholder string "Send a message..." Composer input placeholder
components ThreadComponents Override individual UI slots
onArtifactEvent (event: UiEventEnvelope) => void Called when a ui artifact fires an emit action
maxWidth string "44rem" Max width of the message column
className string Extra classes on the root element

Thread props

Same as TimbalChat minus workforceId, baseUrl, and fetch (those live on TimbalRuntimeProvider).

TimbalRuntimeProvider props

Prop Type Default Description
workforceId string required ID of the workforce to stream from
baseUrl string "/api" Base URL for API calls
fetch (url, options?) => Promise<Response> authFetch Custom fetch function
attachments same as TimbalChat off Enable uploads on the runtime (usually set on TimbalChat instead)
attachmentsUploadUrl string Shorthand upload URL for the default adapter
attachmentsAccept string Shorthand MIME accept for the default adapter
debug boolean false SSE debug logging (see above)

TimbalChatShell props

Extends all TimbalChat props except workforceId is optional.

Prop Type Default Description
workforceId string auto from API When set, skips fetching and hides the built-in selector
brand ReactNode Logo or title at the start of the header
headerActions ReactNode Trailing header content (logout, theme toggle, etc.)
hideWorkforceSelector boolean false Hide the built-in <select> even when multiple agents exist
className string Classes on the outer h-screen flex container
headerClassName string Classes on the header bar

Auth

The package includes an optional session/auth system backed by localStorage tokens. The API is expected to expose /api/auth/login, /api/auth/logout, and /api/auth/refresh.

Auth is opt-in — it only activates when VITE_TIMBAL_PROJECT_ID is set in your environment.

Setup

Wrap your app with SessionProvider and protect routes with AuthGuard:

// src/App.tsx
import { SessionProvider, AuthGuard, TooltipProvider } from "@timbal-ai/timbal-react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";

const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;

export default function App() {
  return (
    <SessionProvider enabled={isAuthEnabled}>
      <TooltipProvider>
        <BrowserRouter>
          <AuthGuard requireAuth enabled={isAuthEnabled}>
            <Routes>
              <Route path="/" element={<Home />} />
            </Routes>
          </AuthGuard>
        </BrowserRouter>
      </TooltipProvider>
    </SessionProvider>
  );
}

When enabled is false, both SessionProvider and AuthGuard are transparent — no redirects, no API calls.

Embedding in an iframe

When the app runs inside an iframe, SessionProvider detects embedding and skips the normal cookie refresh flow. Instead:

  1. The child posts { type: "timbal:request-session" } to window.parent.
  2. The parent responds with { type: "timbal:auth", token: "<access>", refreshToken?: "<refresh>" }.
  3. Tokens are stored in localStorage and fetchCurrentUser() runs as usual.

useSession() exposes isEmbedded: boolean so you can adjust UI (e.g. hide logout redirects that assume a top-level window).

// Parent page
iframe.contentWindow?.postMessage(
  { type: "timbal:auth", token: accessToken, refreshToken },
  "*",
);

window.addEventListener("message", (e) => {
  if (e.data?.type === "timbal:request-session") {
    // inject tokens as above
  }
});

useSession hook

Access the current session anywhere inside SessionProvider:

import { useSession } from "@timbal-ai/timbal-react";

function Header() {
  const { user, isAuthenticated, isEmbedded, loading, logout } = useSession();
  if (loading) return null;
  return (
    <header>
      {isAuthenticated ? (
        <>
          <span>{user?.email}</span>
          <button onClick={logout}>Log out</button>
        </>
      ) : (
        <a href="/login">Log in</a>
      )}
    </header>
  );
}

authFetch

A drop-in replacement for fetch that attaches the Bearer token from localStorage and auto-refreshes on 401. It's also the default fetch used by TimbalRuntimeProvider, so you only need to import it directly for your own API calls (e.g. loading workforce lists):

import { authFetch } from "@timbal-ai/timbal-react";

const res = await authFetch("/api/workforce");
if (res.ok) {
  const agents = await res.json();
}

Auth prop reference

Component Prop Type Default Description
SessionProvider enabled boolean true When false, session is always null and no API calls are made
AuthGuard requireAuth boolean false Redirect to login if not authenticated
AuthGuard enabled boolean true When false, renders children unconditionally

Other exports

Components

Export Description
TimbalStudioShell Floating sidebar + top bar + full-height TimbalChat. Most opinionated layout
StudioSidebar Floating workforce sidebar — collapses, mobile drawer, runtime portal anchor
ModeToggle Sun/moon theme toggle styled for the studio top bar
TimbalMark Liquid-metal brand mark — drop-in welcome icon
StudioWelcome Welcome screen with TimbalMark + staggered intro animation
TimbalChatShell Header + workforce picker + full-height TimbalChat
Thread Full chat UI — messages, composer, attachments, action bar
Composer Standalone composer bar (for custom thread layouts)
Suggestions Suggestion chip grid/row; use with useResolvedSuggestions
WorkforceSelector Styled native <select> for agent switching
MarkdownText Markdown renderer with GFM, math (KaTeX), and syntax highlighting
ToolFallback Animated "Using tool: …" indicator shown while a tool runs
ARTIFACT_AGENT_INSTRUCTIONS Markdown block to paste into agent system prompts
ArtifactRegistryProvider Scope custom artifact renderers
UiEventProvider Low-level provider for ui artifact emit actions
UiCustomNodeRegistryProvider Register { kind: "custom" } node renderers
ArtifactView Render a single artifact object
parseArtifactFromToolResult Parse tool output into an artifact
TooltipIconButton Icon button with a tooltip

Hooks

Export Description
useWorkforces Fetch {baseUrl}/workforce and track selection
useTimbalStream Low-level SSE chat state without <Thread>
useTimbalRuntime Access runtime context inside custom providers
useResolvedSuggestions Resolve static/async SuggestionsSource to an array
useOptionalSession Same as useSession but returns null when no SessionProvider is mounted

UI primitives

Re-exported Radix UI wrappers pre-styled to match the Timbal design system:

Button · Tooltip · TooltipTrigger · TooltipContent · TooltipProvider · Avatar · AvatarImage · AvatarFallback · Dialog · DialogContent · DialogTitle · DialogTrigger · Shimmer


Full example

App shell with optional auth, using TimbalChatShell (agent list + chat in one component):

// src/App.tsx
import {
  SessionProvider,
  AuthGuard,
  TooltipProvider,
  TimbalChatShell,
} from "@timbal-ai/timbal-react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";

const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;

export default function App() {
  return (
    <SessionProvider enabled={isAuthEnabled}>
      <TooltipProvider>
        <BrowserRouter>
          <AuthGuard requireAuth enabled={isAuthEnabled}>
            <Routes>
              <Route path="/" element={<Home />} />
            </Routes>
          </AuthGuard>
        </BrowserRouter>
      </TooltipProvider>
    </SessionProvider>
  );
}
// src/pages/Home.tsx
import { TimbalChatShell, Button, useSession } from "@timbal-ai/timbal-react";
import { LogOut } from "lucide-react";

const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;

export default function Home() {
  const { logout, isAuthenticated } = useSession();

  return (
    <TimbalChatShell
      brand={<span className="text-sm font-semibold">My App</span>}
      headerActions={
        isAuthEnabled && isAuthenticated ? (
          <Button variant="ghost" size="icon" onClick={logout} aria-label="Log out">
            <LogOut className="size-4" />
          </Button>
        ) : null
      }
      welcome={{ heading: "How can I help you today?" }}
      suggestions={[
        { title: "Summarize this week", description: "Recent activity at a glance" },
        { title: "What can you help with?" },
      ]}
      attachments
      debug={import.meta.env.DEV}
    />
  );
}

For a fully custom header, combine useWorkforces with TimbalChat instead of TimbalChatShell (see Workforce list hook).


Migrating from 0.4 to 0.5

0.5.0 slims the public API to the surface that blueprint apps actually use. Every feature still works — only the export list changed.

Re-brand via CSS variables, not internals

All sizing and class composites moved to internal modules. Override the CSS variables in your own :root / .dark blocks instead of importing helper strings:

:root {
  --studio-sidebar-width: 15rem;
  --studio-topbar-height: 3.25rem;
}

For colours, override the existing semantic tokens (--background, --foreground, --composer-bg, --bubble-user, --playground-from/via/to, …). See src/styles.css for the full list.

Removed from the public API

The following symbols are no longer exported. Most have no replacement because they were never meant to be public; for the rest, prefer the high-level shells or CSS-variable overrides:

  • All STUDIO_* layout constants (STUDIO_SIDEBAR_WIDTH, STUDIO_INSET_LEFT, STUDIO_SIDEBAR_COLLAPSED_STORAGE_KEY, STUDIO_SIDEBAR_PX_*, …) — override the matching --studio-* CSS variables instead.
  • All studio*Class / studioChromeShellStyle helpers — re-create the look with normal Tailwind classes against semantic tokens (bg-elevated-from, border-border, shadow-card, …).
  • All TIMBAL_V2_* button token records and TimbalV2Button — use the standard Button export from this package; it covers the same variants.
  • StudioSidebarPanel, StudioSidebarHeader/Nav/Footer/Entries/Backdrop/Tooltip/RuntimePortal/EntryMotion, StudioSidebarContext, useStudioSidebarLayout, useStudioSidebarCollapsed, useSidebarCollapsePhase, workforceItemId/Label/Initial — use StudioSidebar or TimbalStudioShell directly.
  • runThemeSanityCheck<Thread> already schedules the dev-only check.
  • SyntaxHighlighter, UserMessageAttachments, ComposerAttachments, ComposerAddAttachment, MessagePartPrimitive, ActionBarMorePrimitive, ErrorPrimitive, useAuiState, buttonVariants — internal composer/markdown details. Override the Composer / AssistantMessage slot via the components prop if you need a custom layout.

Everything else (the three shells, primitives, hooks, auth, artifact API, design-token CSS variables) is unchanged.


Mock UI demo

An offline Vite app lives in examples/mock-ui. It uses a scripted mock fetch (no API keys) and includes a component gallery for artifacts. See that folder’s README for run instructions.

Local development

Install via a local path reference:

{
  "dependencies": {
    "@timbal-ai/timbal-react": "file:../../timbal-react"
  }
}

Adjust the relative path to where timbal-react lives on your machine.

After editing source files, rebuild:

cd timbal-react
bun run build        # one-off build
bun run build:watch  # rebuild on every change

Vite picks up the new dist/ automatically via HMR — no reinstall needed.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages