Skip to content

Repository files navigation

Tangle Network Banner

npm license stars

@tangle-network/sandbox-ui

React component library for Tangle Sandbox — a shadcn-style primitive layer plus higher-order sandbox surfaces for agent chat, files, runtime state, artifacts, and dashboard views.

Browse the live Storybook to inspect component states in dark and light themes at desktop and mobile widths.

Install

npm install @tangle-network/sandbox-ui

Required peers: react and react-dom 18 or 19, @tangle-network/agent-interface ^1.0.0 || ^2.0.0, @tangle-network/brand ^1.6.0, and @tangle-network/ui ^11.10.0. Optional peers are required only by the subpaths that use them; see package.json. /editor needs its tiptap, yjs and @hocuspocus/provider peers only when it renders an editor, and the @tangle-network/ui README holds the table of which surface needs which.

Usage

import {
  SandboxWorkbench,
  type FileNode,
  type SessionMessage,
  type SessionPart,
} from "@tangle-network/sandbox-ui";

Import styles in your app root:

import "@tangle-network/sandbox-ui/styles";

Fonts

sandbox-ui references the following font families in its design tokens but does not bundle them — consumer apps must load the fonts themselves. This is deliberate: a URL @import inside a library CSS bundle breaks when downstream apps chain-import the stylesheet (see CHANGELOG 0.10.9 for the full reasoning).

Family Role Used as CSS variable
Geist UI body text --font-sans
Geist Mono Code, terminal --font-mono
Outfit Display / headings (default theme) --font-display
Manrope Display / headings (vault theme) --font-display
Inter UI body (vault theme) --font-sans

Pick one loading strategy that fits your app:

1. Self-hosted via @fontsource/* (recommended — no external network request):

npm install @fontsource/geist-sans @fontsource/geist-mono @fontsource/outfit @fontsource/manrope @fontsource/inter
// app entry
import "@fontsource/geist-sans/400.css";
import "@fontsource/geist-sans/500.css";
import "@fontsource/geist-sans/600.css";
import "@fontsource/geist-sans/700.css";
import "@fontsource/geist-mono/400.css";
import "@fontsource/geist-mono/500.css";
import "@fontsource/outfit/500.css";
import "@fontsource/outfit/700.css";
import "@fontsource/manrope/500.css";
import "@fontsource/manrope/700.css";
import "@fontsource/inter/400.css";
import "@fontsource/inter/600.css";

2. Google Fonts via HTML <link>:

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" />

Any family you omit falls back per the --font-* token chain (e.g. --font-sans falls back to "DM Sans", ui-sans-serif, system-ui, sans-serif).

If you are building on the sandbox SDK directly, use useSdkSession to turn raw SDK/session-gateway events into the messages + partMap model that ChatContainer and SandboxWorkbench expect:

import {
  SandboxWorkbench,
} from "@tangle-network/sandbox-ui";
import { useSdkSession } from "@tangle-network/sandbox-ui/sdk-hooks";

function App() {
  const {
    messages,
    partMap,
    isStreaming,
    appendUserMessage,
    beginAssistantMessage,
    applySdkEvent,
    completeAssistantMessage,
    failAssistantMessage,
  } = useSdkSession();

  async function runTurn(text: string) {
    appendUserMessage({ content: text });
    const assistantMessageId = beginAssistantMessage();

    try {
      for await (const event of sdk.streamPrompt(text)) {
        applySdkEvent(event, { messageId: assistantMessageId });
      }
      completeAssistantMessage({ messageId: assistantMessageId });
    } catch (error) {
      failAssistantMessage(
        error instanceof Error ? error.message : "Agent run failed",
        { messageId: assistantMessageId },
      );
    }
  }

  return (
    <SandboxWorkbench
      session={{
        messages,
        partMap,
        isStreaming,
        onSend: runTurn,
      }}
    />
  );
}

Compose sandbox applications around SandboxWorkbench when you want the library’s default operating model:

const root: FileNode = {
  name: "agent",
  path: "/home/agent",
  type: "directory",
  children: [],
};

const messages: SessionMessage[] = [];
const partMap: Record<string, SessionPart[]> = {};

<SandboxWorkbench
  title="Tax filing workspace"
  directory={{
    root,
    visibility: {
      hiddenPathPrefixes: ["/home/agent/tax_toolkit"],
    },
  }}
  session={{
    messages,
    partMap,
    isStreaming: false,
    presentation: "timeline",
    onSend: console.log,
  }}
  runtime={{
    title: "Runtime",
  }}
/>;

FileTreeVisibilityOptions is a UI-layer policy only. Sensitive paths still need to be hidden and denied by the app/backend layer.

The three-pane shell

For a chats rail · transcript + composer · artifacts layout, hand SandboxWorkbench a SessionSidebar as its rail, your composer as composer, and centerHeader={null} for the quiet center. WorkspaceLayout owns the pane sizes and, when you pass layout.leftOpen / layout.onLeftOpenChange, the open state too.

const [chatsOpen, setChatsOpen] = useState(true);

<SandboxWorkbench
  centerHeader={null}
  rail={
    <SessionSidebar
      variant="quiet"
      groupBy="status"
      showUpdatedAt
      fill
      className="border-r-0"
      title="Chats"
      createLabel="New chat"
      items={threads}
      currentItemId={threadId}
      onSelectItem={(item) => navigate(item.href)}
      onCreate={createThread}
      onCollapse={() => setChatsOpen(false)}
    />
  }
  session={{ messages, partMap, isStreaming }}
  composer={<ChatComposer onSend={runTurn} />}
  artifacts={[
    { id: "changes", kind: "custom", title: "Changes", icon: GitCompare, pinned: true, content: <ChangesPanel /> },
    ...openFiles,
  ]}
  layout={{
    leftOpen: chatsOpen,
    onLeftOpenChange: setChatsOpen,
    keyboardShortcuts: true,
    leftCollapsedControl: (
      <button type="button" aria-label="Show chats" onClick={() => setChatsOpen(true)}>
        <PanelLeftOpen className="h-4 w-4" />
      </button>
    ),
  }}
/>;

Each SessionSidebarItem can carry an icon (rendered in a 16×20 slot, with the status dot as a corner badge) and a meta line; formatRelativeAge from /workspace formats the age for that line.

Theming And Retheming

There is a built-in Tangle default theme, but consumers can restyle the library in three layers:

  1. Pick a built-in surface theme
  2. Override semantic tokens
  3. Wrap higher-level components when you want a different product composition

1. Pick a Built-in Theme

WorkspaceLayout and SandboxWorkbench support:

  • theme="vault" — light theme with solid surfaces
  • No theme prop — default dark theme

They also support density="comfortable" and density="compact".

<SandboxWorkbench
  layout={{
    theme: "vault",
    density: "comfortable",
  }}
  session={{ ... }}
/>

If you are not using SandboxWorkbench, you can set the same attributes yourself:

<div data-sandbox-ui data-sandbox-theme="vault" data-density="compact">
  <YourSandboxApp />
</div>

2. Override Semantic Tokens

The shared visual contract lives in src/styles/tokens.css. The important tokens are:

  • surfaces: --bg-root, --bg-card, --bg-elevated, --bg-section, --bg-input
  • text: --text-primary, --text-secondary, --text-muted
  • brand: --brand-cool, --brand-glow, --brand-purple
  • accent surfaces: --accent-gradient-strong, --accent-surface-soft, --accent-surface-strong, --accent-text
  • borders: --border-subtle, --border-default, --border-accent
  • radii/shadows: --radius-*, --shadow-card, --shadow-dropdown, --shadow-accent

App-level overrides can be scoped to a wrapper:

.tax-theme {
  --brand-cool: hsl(187 75% 54%);
  --brand-glow: hsl(164 74% 56%);
  --bg-root: hsl(222 18% 9%);
  --bg-card: hsl(223 20% 12%);
  --border-accent: hsl(187 75% 48% / 0.35);
  --font-sans: "Satoshi", ui-sans-serif, system-ui, sans-serif;
}
<div className="tax-theme">
  <SandboxWorkbench ... />
</div>

3. Know When To Wrap Instead Of Override

Token overrides are the right tool when you want:

  • a different brand color system
  • different typography
  • tighter or roomier density
  • a more consumer-facing or operator-facing tone

Wrap or compose on lower-level exports when you want:

  • a different page shell
  • different header chrome
  • a different artifact tab model
  • app-specific empty states and actions

The higher-order dashboard/billing surfaces are now accent-token driven rather than hardcoded to the default Tangle look. The main seams are:

  • DashboardLayout.className, sidebarClassName, contentClassName
  • BillingDashboard.className, cardClassName
  • PricingCards.className, cardClassName
  • UsageChart.className
  • StandalonePricingPage.className

For that, compose directly from:

  • /workspace
  • /chat
  • /run
  • /files

Current Reality

Retheming is absolutely supported, but the documentation was thinner than it should be. The token layer is strong; the higher-level surfaces are themeable, but more opinionated. For a radically different product look, prefer keeping the token contract and wrapping the higher-level workbench/chat surfaces rather than fighting every internal class.

Docs

Guide Description
Sidebar Composable Rail + Panel sidebar system (architecture, components, full API)

Subpath Exports

Subpath Description
/primitives Button, Card, Dialog, Badge, Input, Select, Table, Tabs, Toast, etc.
/chat ChatContainer, ChatInput, ChatMessage, AgentTimeline, ThinkingIndicator
/run ToolCallFeed, RunGroup, InlineToolItem, ExpandedToolDetail
/workspace SandboxWorkbench, WorkspaceLayout, DirectoryPane, RuntimePane, StatusBar
/openui OpenUIArtifactRenderer and schema types for structured artifact rendering
/files FileTree, FilePreview, FileTabs, FileArtifactPane
/dashboard Sidebar, DashboardLayout, BillingDashboard, UsageChart, ProfileSelector
/editor TipTap collaborative editor (requires optional peers)
/terminal xterm.js terminal view (requires optional peers)
/markdown Markdown renderer with GFM, code blocks, copy button
/auth AuthHeader, GitHubLoginButton, UserMenu
/pages Pre-built billing, pricing, profiles pages
/hooks useSSEStream, useAuth, usePtySession, useRunGroups, etc.
/sdk-hooks Lightweight session/stream hooks without the React Query CRUD hook bundle
/stores Session and chat nanostores
/types TypeScript types for messages, parts, runs, sessions
/utils cn, formatDuration, timeAgo, tool display helpers
/styles Compiled CSS bundle

Stack

  • Radix UI primitives
  • Tailwind CSS v4
  • Lucide icons
  • CVA for variant management
  • Shared semantic tokens for default dark and vault light sandbox themes
  • ESM-only, tree-shakeable, fully typed

License

Apache-2.0

About

React component library for AI agent interfaces — chat, terminal, file browser, tool calls, dashboards

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages