Skip to content

About

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

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

644 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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. /files needs pdfjs-dist only for PdfViewer, which FilePreview uses for PDFs; without it the viewer says so and falls back to the browser's own PDF viewer. The ui README covers its worker and asset setup through configurePdfViewer.

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";

An app that runs its own Tailwind v4 compile imports the source entry instead, and lists no node_modules paths:

@import "tailwindcss";
@import "@tangle-network/sandbox-ui/tailwind.css";

The entry carries Brand's tokens and utility registrations, this package's runtime CSS, and @source lines for its own dist and the @tangle-network/ui peer, so every component utility compiles once, in the app's own layer order. Agent apps import @tangle-network/agent-app/tailwind.css, which includes this entry.

Dashboard page spacing

For apps whose pages already provide their own gutters, pass contentClassName="px-0 pb-0 lg:px-0" to DashboardLayout. New Sandbox is at the top of the sidebar; notifications sit above the account menu. Set notificationsEnabled={false} when the app has no notification feed. The desktop has no top bar; the mobile menu opens a focus-managed drawer.

Use sidebarLeading={({ collapsed }) => <WorkspaceSwitcher compact={collapsed} />} for workspace controls; the mobile drawer supplies collapsed: false. A static node opens in a Workspace menu on the collapsed rail. topBarLeading remains a relocated legacy alias, and topNavLinks render as sidebar links. collapseEmptyTopBar is retained as a no-op. DashboardPageHeader keeps title, description and actions aligned without adding a card.

Workspace integration access

Use IntegrationsPanel from /integrations for the provider catalog, logos, search, connection status, and account actions. Do not render a second connection catalog above it. Supply getConnectionContext for a short access label on each connected card. Supply getConnectionActions for authorized workspace actions in that card's menu. Each action has an id, label, optional disabled flag, and onSelect callback. The panel waits for asynchronous actions and surfaces failures. Keep workspace authorization and binding persistence in the consuming application's server. When a provider has multiple accounts, include distinct account labels and identities in its workspace actions.

Embed a sandbox app

Use EmbeddedAppView when a workspace presents a running web app beside an agent conversation. Render it in a container with a defined height.

import { EmbeddedAppView } from "@tangle-network/sandbox-ui/workbench/embedded-app"

<main className="h-full min-h-0">
  <EmbeddedAppView
    app={{
      id: selectedApp.id,
      name: selectedApp.name,
      status: selectedApp.previewStatus,
      previewUrl: selectedApp.previewUrl,
      statusMessage: selectedApp.previewError,
    }}
    allowedOrigins={workspace.previewOrigins}
    readiness={selectedApp.httpReadiness}
    access={workspace.enforcedPreviewAccess}
    toolbarActions={<AppActions app={selectedApp} />}
    onRetry={() => recheckPreview(selectedApp.id)}
  />
</main>

The host supplies the app list, stable identity, persistence, authorization, and readiness result. Set status: "ready" only after an app-level HTTP probe succeeds. A preview route becoming available does not prove app health. Supply a resolved, credential-free HTTPS URL, or HTTP loopback for local development. Pass allowedOrigins from product policy or the enforcing service, independent of the URL. The host must authorize access before it returns the URL. The component shows starting and unavailable states without mounting a frame. Its ready view uses the same URL validation and iframe isolation as PreviewView. The compact header uses the shared 56px workspace pane row and provides Reload and Open in new tab. Place product actions in toolbarActions to share that header without a second toolbar. Pass service-observed readiness and enforced access to show their status inside the pane. Show process and port state beside the pane if your runtime exposes those separately. Do not infer either state from the preview URL or iframe load.

Show a shared agent messaging connection

Use AgentMessagingConnection in any app that enrolls a member with a private agent target and a shared messaging route. The host owns authentication, the member grant, canonical target resolution, and route readiness.

import {
  AgentMessagingConnection,
  type AgentEnrollmentViewState,
  type SharedMessagingRouteViewState,
} from "@tangle-network/sandbox-ui/connections";

type MessagingConnectionProps = {
  enrollment: AgentEnrollmentViewState;
  route: SharedMessagingRouteViewState;
  enroll: () => Promise<void>;
  openMessages: () => void;
  refresh: () => Promise<void>;
};

export function MessagingConnection({
  enrollment,
  route,
  enroll,
  openMessages,
  refresh,
}: MessagingConnectionProps) {
  return (
    <AgentMessagingConnection
      enrollment={enrollment}
      route={route}
      onEnroll={enroll}
      onOpenMessages={openMessages}
      onRetry={refresh}
    />
  );
}

Report eligible only after the host checks the member's current grant. Report enrolled only after it resolves the authorized, canonical private agent target. Give the component display labels, not raw enrollment, instance, sandbox, or session IDs. Report route ready only when the shared route is valid for this application and member. An attached provider line on its own does not prove that state. The component never attaches, buys, or reassigns a provider identity. It shows a messaging action only when both the target and route are confirmed.

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,
      }}
    />
  );
}

Compact workspace files

New agent products compose their application shell with AgentWorkspaceCompanion from @tangle-network/agent-app/workspace-react. Use WorkspaceFilesPane for its Files content and WorkspaceArtifactView for technical artifact rendering. Both are exported from @tangle-network/sandbox-ui/workspace.

<WorkspaceFilesPane
  paths={workspacePaths}
  selectedPath={selectedPath}
  onSelect={openFile}
  onBack={closePreview}
  preview={selectedArtifact ? {
    path: selectedArtifact.path,
    content: <WorkspaceArtifactView artifact={selectedArtifact} hideTitleBlock />,
    actions: <DownloadFile artifact={selectedArtifact} />,
  } : undefined}
/>

Pass either a recursive root: FileNode or canonical paths, including trailing slashes for empty folders. A root input returns its original node as the second onSelect argument; flat paths have no node metadata. The pane uses the shared searchable file tree and retains it while a preview is open. Returning to files preserves expansion, selection, and scroll. Native search ends when a result is selected. Adapters own loading, permissions, errors, file reads, and actions; emptyState customizes an empty workspace. The preview header exposes Back to files, the selected path, and supplied actions without a second title frame. Set hideTitleBlock on the nested artifact renderer when the containing pane already identifies the file. Download, format, toolbar, and content controls remain available; standalone rendering keeps its existing title defaults.

Existing sandbox workbench

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 and your composer as composer. The default places the session title and status in the transcript pane header without a second shell row. Use centerHeader={null} when the transcript should have no pane frame either. WorkspaceLayout leaves its center row out until a caller supplies centerHeader. Closed panes get compact edge controls with reserved space beside the transcript. surface="inset" raises the center and right panes as one surface inside an even gutter on the recessed backdrop. Its center always has a 48px header: closed-pane controls sit flat in its top-right corner, and an open right pane shares that header height and a divider. rightOpenLabel, rightCloseLabel, and rightControlHint name the right-pane controls and their tooltip. It owns 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

Markdown prose uses --foreground and --muted-foreground as HSL channel values. Set them on the nearest product wrapper when its colors differ from the brand theme.

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

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.

DiffView from /workbench defaults to a dark code theme. If the host app offers a light/dark switch, pass its selected theme as themeType. The diff renderer updates when that value changes.

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 WorkspaceFilesPane, WorkspaceArtifactView, SandboxWorkbench, WorkspaceLayout, DirectoryPane, RuntimePane, StatusBar
/workbench DiffView, ChangesPane, CodeView, artifact and preview components
/openui OpenUIArtifactRenderer and schema types for structured artifact rendering
/files FileTree, FilePreview, FileTabs, FileArtifactPane, PdfViewer, FileCard, FileList
/workbench/embedded-app EmbeddedAppView and preview status types without terminal or editor modules
/dashboard Sidebar, DashboardLayout, BillingDashboard, UsageChart
/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
/tailwind.css Tailwind v4 source entry: tokens, runtime CSS, and the @source lines for this package and @tangle-network/ui

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