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>
)
}