introduction

An unbundled collection of copy-paste terminal UI primitives for AI coding agents built on React and Ink. Accessible, controlled, customizable, and open source.

quickstart
$bunx @trydecember/tui init
preview
loading virtual terminal...

Philosophy & Design Invariants

@trydecember/tui is an unbundled collection of copy-paste terminal primitives. Instead of distributing a monolithic npm package, you copy typed, raw component primitives directly into your repository.

01Unbundled Source Code: You own the components. Modify styling, diff folding algorithms, border glyphs, or animation timings directly in your repo.
02Purely Controlled Primitives: All components are stateless view components. Your agent state machine owns focus, keyboard listeners (useInput), and lifecycle transitions. Components never hijack stdin.
03React + Ink Runtime: Built for the modern terminal ecosystem. Renders in any terminal emulator running Node.js or Bun with zero browser or native GUI dependencies.
04Local Design Token Contract: Every component consumes design tokens from a central theme.ts file via your local TypeScript path alias (@/components/ui/theme).
05Zero Vendor Lock-In: No telemetry, no hosted dependencies, and no proprietary wrappers. Just clean, readable TypeScript and Ink.

Agent Turn Architecture

In an AI coding agent, a "turn" represents a single round of interaction: the user's prompt, the agent's chain-of-thought scratchpad, tool calls (bash, edits, searches), code diffs, and the final streaming markdown response. Here is how @trydecember/tui primitives compose into a production agent turn:

import React from 'react'
import { Box } from 'ink'
import { StreamingText } from '@/components/ui/streaming-text'
import { CollapsibleReasoning } from '@/components/ui/collapsible-reasoning'
import { ToolCallCard } from '@/components/ui/tool-call-card'
import { DiffViewer } from '@/components/ui/diff-viewer'
import { TokenGauge } from '@/components/ui/token-gauge'

interface ToolCall {
  name: string
  status: 'pending' | 'running' | 'completed' | 'failed'
  durationMs?: number
  argsSnippet?: string
}

interface AgentTurnProps {
  thought?: string
  isThinking?: boolean
  responseText?: string
  isStreaming?: boolean
  toolCalls?: ToolCall[]
  diff?: string
  tokensUsed?: number
  tokenLimit?: number
}

export function AgentTurn({
  thought,
  isThinking = false,
  responseText,
  isStreaming = false,
  toolCalls = [],
  diff,
  tokensUsed = 4120,
  tokenLimit = 128000,
}: AgentTurnProps) {
  return (
    <Box flexDirection="column" gap={1}>
      {/* 1. Agent chain-of-thought scratchpad */}
      {thought && (
        <CollapsibleReasoning
          thought={thought}
          isStreaming={isThinking}
          defaultCollapsed={!isThinking}
        />
      )}

      {/* 2. Tool executions (file edits, bash execution, searches) */}
      {toolCalls.map((call, idx) => (
        <ToolCallCard
          key={idx}
          toolName={call.name}
          status={call.status}
          durationMs={call.durationMs}
          argsSnippet={call.argsSnippet}
        />
      ))}

      {/* 3. File diff viewer with syntax highlighting */}
      {diff && <DiffViewer diff={diff} />}

      {/* 4. Progressive streaming agent response */}
      {responseText && (
        <StreamingText
          text={responseText}
          isComplete={!isStreaming}
        />
      )}

      {/* 5. Context window gauge */}
      <TokenGauge
        used={tokensUsed}
        total={tokenLimit}
        label="Context Window"
      />
    </Box>
  )
}