AI coding agent built from scratch in pure Go — CellGrid TUI, native MCP client, web scraping, and LSP diagnostics in a single binary.
Custom CellGrid frame-buffer renders markdown directly in the terminal — no external renderer dependency.
- Bold, italic,
inline code, headings, blockquotes, ordered/unordered lists (nested arbitrary depth) - Fenced code blocks with
\t→ spaces conversion, indentation preservation - Tables with smart column-width alignment, box-drawing characters (
│ ─ ┤ ├ ┼) - Horizontal rules, links, mixed content
- Incremental streaming:
parseMarkdown()on every render tick — no sudden jump from raw markdown to styled output - 10+ roundtrip tests verify text content survives parse → render → extract
- Auto-save conversations on exit (
~/.tinycode/sessions/TUI-*.json), with title/preview/metadata - Resume with
--resume=TUI-20260607-235959 - List sessions with
--list-sessions(shows title, message count, last active time) - AI-generated session titles: title hidden agent auto-names conversations via LLM. Falls back to first user message. (2deee5e)
- Input history: Up/Down arrows to recall previous inputs, Esc to exit browse mode
- Status bar shows
hist 3/5during history browsing
- Incremental CellGrid Rendering — msgDirty/msgRowCount tracking. View() skips unchanged messages, only re-renders from first dirty onward. CellGrid no longer Reset() every frame. Benchmark: ~2.3ms regardless of message count (10/50/100 tested). (ffbbf22)
- Reasoning folding: click
[+]/[-]markers to expand/collapse LLM reasoning blocks. Multi-step agent loop now creates one assistant message per step, each with its own[-]reasoning +→ Calling tools:list. (c5f854f) - Tool call display:
→ Calling tools:with bullet list during tool execution. Duplicate tool calls in the same step no longer appear twice (agent.go fired OnToolCall twice per tool — fixed in cc5a224) - Character-level selection: drag-select any range of viewport text, Ctrl+C copies rendered text (not raw markdown). Range check prevents single-click selects from triggering copy. (ea6711c)
- [Copy] buttons: click to copy assistant response to clipboard. Fixed: button row now counted in msgRowCount so subsequent messages don't overlap. (f9143cc). Fixed: button stops working after incremental render — activeButtons preserved per MsgIdx across renders. (8dffd64)
- Spinner:
⣾ ⣽ ⣻ ⢿ ⡿ ⣟braille spinner in status bar during streaming. Tick pipeline kept alive even during idle. Spinner continues across intermediate steps. (42b8794, 9a0f55b, afc14e1) - Auto-scroll: viewport follows streaming output, pauses when user scrolls up
- Status bar: mode icon, model name, provider, token/tool/msg counts, session duration, transient status messages
- ReAct loop with tool calling support (24 tools: bash, read_file, write_file, search_files, edit, apply_patch, git_*, web, LSP, task, task_collect, todo, sandbox_allow, load_skill, skill_manage)
- 6 agents: plan (read-only whitelist), build (full access), explore (read_file + search_files only), general (full execution sub-agent), compact (history compression), title (session naming)
- Permissions engine:
Rulesetwith last-match-wins{action, resource, effect}rules replacing DeniedTools/AllowedTools. Supports whitelist (*: deny+ specific allows) and blacklist (*: allow+ specific denies). - Task tool: Delegate to sub-agents via
task({agent, goal}). Sync mode (block until done) or bg mode (returns task_id, collect withtask_collect). Sub-agent steps don't count against parent's step budget. - OnStepDone callback: after each step's tools complete, the agent fires
OnStepDone— the TUI creates a new assistant message for the next step. Each step gets its own reasoning + tool calls display. - Agent integration test framework: 13 tests using MockLLM step-by-step
- Streaming reasoning + text deltas
- Tool call lifecycle displayed in real-time
- Multi-turn history compression: Hermes-style head/tail/middle summarization
- 1M token context (DeepSeek V4 Flash) with automatic threshold lowering on
context_length_exceedederrors
- Config:
"lsp": { "enabled": true }in config.json (default: disabled). 7 supported languages with auto-detection: Go (gopls), Python (pyright), TypeScript/JS (typescript-language-server), Rust (rust-analyzer), C++ (clangd), Java (Eclipse JDT). - 4 tools exposed to LLM:
lsp_definition(→Definition at path:line:col),lsp_references(→Found N references),lsp_hover(→ type info + docs),lsp_symbols(→ all symbols in file). All requirefile_path,line,character(0-indexed). - Architecture: Each call starts a fresh LSP process:
exec.Command("gopls")→ stdin/stdout pipes → JSON-RPC with Content-Length framing →Initialize→StartReader()(background diagnostics listener) → tool operation →Close(). - Incremental Diagnostics —
SnapshotBaselinecaptures diagnostic state before edit/write_file/apply_patch,GetNewDiagnosticscomputes delta. Tools report only new errors via LSP — LLM sees focused feedback. (a2e3e07) - TUI Error Tracking — LSPDiagMsg carries per-file diagnostic sets. Status bar shows
errors: Nwith live count./diagnosticscommand lists all current file errors in viewport. (290818a) - Mock test framework —
io.Pipebased, no LSP server required, 15+ tests covering all 4 tool types. (8065ae5) - Limitation: Per-call process startup (~500ms overhead). Not a persistent LSP connection despite the long-lived design intent.
- TodoStore: In-memory task list with CRUD (create/read/update/merge/delete/summary). Enforces one
in_progress, max 256 items, max 4000 chars per task. - todo tool: OpenAI function-calling schema, registered in all modes. LLM calls it to plan and track multi-step work.
- Compression protection: After context compression, active todo items (pending + in_progress) are re-injected so the LLM doesn't redo completed work.
- Housekeeping mute: When all tool calls are
todo, the model's text reply is suppressed — no noise, just progress markers. - Session recovery: On
--resume, reverse-scans history for the latest todo result and restores the store. - TUI rendering:
▾ Todo (2/6)with[x]completed,[>]in_progress,[ ]pending,[~]cancelled markers.
- edit tool: Search/replace editing (old_string + new_string). 7 fuzzy strategies (exact → line-trimmed → ws-normalized → indent-flexible → escape-normalized → unicode-normalized → block-anchor) + indentation correction. Validates uniqueness. Multiple edits per call. LSP integration. 14 tests. (d067156, 34d2c17)
- apply_patch tool: V4A multi-file patch format. Supports UPDATE (line-level -/+ hunks), ADD (create files), DELETE (remove files). Two-phase execution: validate all, then apply. Multi-file in one call. 9 tests. (9045176)
- write_file preserved for creating new files and full rewrites. Three tools form a complementary editing system.
- Native MCP client: Connect to MCP servers via stdio (subprocess) or HTTP (remote endpoint). Config-driven via
mcp_serversin config.json. - Auto-discovery: On startup, connects to all servers, calls
tools/list, and registers each discovered tool as an independentmcp_<server>_<tool>agent tool with its original JSON Schema. - Transport: stdio (exec.Command with pipes, Content-Length framing) or HTTP POST (configurable headers, JSON-RPC 2.0).
- Resources:
resources/listandresources/readsupport for MCP resources. - Security: SSRF protection for HTTP transport — blocks private IPs (RFC 1918/loopback/link-local), fails closed on DNS failure. Localhost allowed for dev use.
- Graceful degradation: Server connection failure logs a warning and skips that server — other servers still work. 22 tests. 359 total. (e31c08b, cc1ba8e, 7b4fe75)
- web_search: Searches the web using DuckDuckGo Lite (zero config, no API key). Optional SearXNG fallback configured via
searxng_urlin config.json. Returns numbered results with title, URL, description. (5c90f86, 4cb7ce1, 405bc87) - web_extract: Fetches and extracts web page content as Markdown. 5-level fallback chain: direct HTTP → Cloudflare bypass (UA retry) → Google Cache → Wayback Machine (CDX + id_ format) → Chromium headless. SSRF protection: DNS resolution + IP blacklist (RFC 1918, loopback, cloud metadata). LLM summarization: content >5000 chars auto-summarized via provider. (5c90f86, 4cb7ce1, 4bf27e5)
- GitHub Actions: Two workflows — main.yml (build + lint + test on push/PR) and release.yml (cross-compile + GitHub Releases on tags v*)
- Makefile improvements: test target preserves exit code with pass/fail message; releases target cross-compiles all platforms + .tar.gz archives
- 359 tests passing
- SKILL.md-based discovery — three-layer scan: embedded (skill/builtin/) → ~/.tinycode/skills/ → project .tinycode/skills/ (upward search). Later sources override earlier. (cbd6db3)
- /skill command in TUI — /skill lists available skills; /skill loads full SKILL.md content as system message. (cbd6db3)
- Skill index auto-injected into system prompt at startup. Startup shows "13 tools, 2 skills loaded". (8fa8800)
- 2 builtin skills: code-review, git-commit (as markdown files in skill/builtin/)
- 6 agents: plan, build (primary) + explore, general (subagents) + compact, title (hidden)
- 11 new tests across skill package and tui package — 359 tests total. (cbd6db3)
┌──────────────────────────────────────────────────────────┐
│ TinyCode │
├──────────────────────┬───────────────────┬────────────────┤
│ TUI (Bubble Tea) │ Agent Layer │ Tool Layer │
│ │ (ReAct Loop) │ (21 tools + MCP)│
│ CellGrid │ │ │
│ Viewport │ Plan (primary) │ bash │
│ Input Area │ Build (primary) │ read_file │
│ Status Bar │ Explore (sub) │ write_file │
│ Command Palette │ General (sub) │ edit │
│ Todo Display │ Compact (hidden) │ apply_patch │
│ Reasoning Fold │ Title (hidden) │ search_files │
│ │ │ task │
│ │ Registry: │ todo │
│ │ Get/Set/Switch │ memory │
│ │ ToolAllowedFor │ load_skill │
│ │ Subagent→task │ skill_manage │
│ │ │ lsp_* (4) │
│ │ │ sandbox_allow │
│ │ │ web_search │
│ │ │ web_extract │
└──────────────────────┴───────────────────┴────────────────┘
6 agents configured in agent/config.go, managed by agent/registry.go:
| Agent | Mode | Hidden | Tools | Steps | Purpose |
|---|---|---|---|---|---|
| plan | primary | read_file, search_files, git_, web_, lsp_*, todo, load_skill | 20 | Read-only analysis (whitelist, no bash) | |
| build | primary | * (all 24 tools) | 50 | Full access implementation | |
| explore | subagent | read_file, search_files | 15 | Fast read-only code search | |
| general | subagent | * except {task, task_collect, skill_manage} | 20 | Full-execution parallel sub-agent | |
| compact | primary | ✅ | (no tools) | 1 | History compression |
| title | primary | ✅ | (no tools) | 1 | Session title gen |
- Primary agents: user-switchable via Tab or /plan /build
- Subagents: invoked via
tasktool with independent ReAct context - Hidden agents: pure LLM calls (no tools), used internally
Component → []CellChunk → wordWrap → Grid.AppendChunk → Grid.Render() → viewport
CellGrid— flat array ofCell{ Rune, Style, Width }, auto-grows as content is addedCellChunk— struct withText stringandStyle CellStyle(Bold, Italic, Underline, Fg, Bg)wordWrap— splits text at width, preserves leading spaces, returns[]CellChunkFill— appliesSelectionStyleto a rectangular cell rangeExtractText— returns plain text within a range, handles CJK multi-cell characters- Incremental rendering: msgDirty/msgRowCount tracking. First dirty → end. ~2.3ms constant.
type Tool struct {
Name string
Description string
Parameters map[string]any
Execute func(ctx, args) (string, error)
}Line-level editing (3 tools):
write_file— create new files / full rewritesedit— search/replace with 7 fuzzy strategies + indentation correctionapply_patch— V4A multi-file patch (UPDATE/ADD/DELETE)
Sandbox (3 layers + dialog):
- Command blacklist (bash tool)
- Path restriction (default: project directory only)
- Interactive TUI dialog with 4 options:
- Allow once — one tool call only, no cache
- Allow session — cached for session, persisted to session file, restored on resume
- Always allow — cached + persisted to
config.json, loaded on every startup - Deny — blocks access, dialog dismissed
- User whitelist (allow/deny/always prompt via TUI dialog)
read_filetriggers dialog (previously returned text hint to LLM) — e2832e3- Dialog auto-shows on View() even without keypress — 57359db
Permissions: ToolAllowedFor(cfg, toolName) — checked before every tool execution. Plan mode denies write/git/task/skill_manage.
type LLMProvider interface {
Chat(ctx, ChatRequest) (*ChatResponse, error)
Name() string
}- DeepSeek (default): streaming SSE support,
deepseek-v4-flash - MockLLM: step-by-step scripted responses for agent loop testing
- ProviderRegistry: switch providers at runtime via Tab
History threshold: 50% of context window
Head: system + first 2 exchanges (preserved)
Tail: last 2 exchanges (preserved, anchored on latest user msg)
Middle: → LLM summarization → [COMPRESSED HISTORY] system message
Active TODO injected: [ACTIVE TODO ITEMS] after compression
/compresscommand for manual trigger- Auto-recovery:
context_length_exceeded→ lowers threshold - Todo protection: active items re-injected in compressed output
User Input (textarea)
↓
ChatMsg → agent.Run() → ReAct Loop
│ ├── LLM provider (streaming SSE)
│ ├── Tool execution (permissions checked)
│ └── No tool call → return final answer
↓
streamCh (buffered 200)
↓
TUI Update() → ToolCallMsg / StreamMsg / StreamDone
↓
TUI View() → Component.Render() → CellChunks → CellGrid
↓
viewport.SetContent() → terminal display
agent/ Agent loop, LLM provider, context compression, registry
config/ Config loading (JSON, env, CLI flags)
lsp/ LSP client (gopls), diagnostics, Formatter, touch
session/ Session persistence (JSON files, metadata, listing, fork)
skill/ SKILL.md discovery (3-layer), Load/LoadOnce/CRUD
tool/ Tool definitions (21 tools + MCP: edit, todo, skill, LSP, web, mcp)
tui/ Bubble Tea TUI (CellGrid, components, key/mouse, cmd palette)
types/ Shared types (Message, ToolCall, StreamCallbacks)
main.go CLI entry point with cobra
| Package | Purpose |
|---|---|
bubbletea |
TUI framework, event loop |
bubbles/viewport |
Viewport widget |
bubbles/textarea |
Input textarea |
bubbles/spinner |
Loading spinner |
lipgloss |
ANSI style management |
go-runewidth |
CJK character width calculation |
go-openai |
LLM provider (OpenAI-compatible) |
goldmark |
Markdown parser |
cobra |
CLI flag handling |
- Incremental CellGrid Rendering — msgDirty tracking, View() skips unchanged messages. Benchmark: ~2.3ms regardless of session size. (ffbbf22)
- Agent-level unit testing framework — 13 integration tests using MockLLM step-by-step. (a3868e2)
- Multi-agent session tree —
/fork+/sessionbranching conversations. (93f1665) - Theming — default + nord palettes,
/themecommand, persists to config.json. (e4bcf85, 533d167) - Session management — delete, export Markdown, search via CLI flags. (2236e84)
- LSP — long-lived connection, background diagnostics, mock test framework, incremental diagnostics (SnapshotBaseline+GetNewDiagnostics), TUI error tracking (LSPDiagMsg, status bar "errors: N", /diagnostics command). (2ab4338, ace09ff, a2e3e07, 290818a)
- GitHub Actions CI/CD + Makefile improvements — main.yml (build+lint+test), release.yml (cross-compile+release), Makefile test/releases targets. (ab07697, bddeed5)
- Skills & Subagents — SKILL.md-based discovery + /skill command + 2 builtin skills. 3 new subagents: general (parallel research), compact (history compression), title (session naming). /explore command removed (explore kept as subagent). (cbd6db3, 8fa8800, adfa51b, c0b8ae8)
- Todo Feature — P0+P1+P2 Complete — TodoStore + todo tool + JSON Schema (P0), TUI rendering with [x][>][ ][~] markers (P1), compression protection + housekeeping mute + session recovery (P2). 21 new tests. (2f51d06, 94db0e3, 25caefc)
- Todo TUI display fixes — Always render TODO between reasoning and tool calls (not gated by todoDirty). Hide
todofrom tool call list. Persist across CellGrid rebuilds. Add blank line separator between TODO and→ Calling tools:. Render-acknowledge gate for concurrency safety. (720c8b6, 12ae5db, 01410a9) |- [x] Line-Level Code Edit — edit + apply_patch — Search/replace edit tool with 7 fuzzy strategies + indentation correction. V4A multi-file patch format. 23 new tests. (d067156, 9045176, 34d2c17) - Title Agent & Session Titles — title hidden agent generates conversation titles via LLM after first exchange. Applied on session save. (2deee5e)
- Edit Fuzzy Matching — 7 fallback strategies (line-trimmed, ws-normalized, indent-flexible, escape-normalized, unicode-normalized, block-anchor). Indentation correction. 6 new tests. (34d2c17)
- Web Tools Phase 1-3 — web_search (DuckDuckGo Lite + SearXNG), web_extract (HTTP→CF→Cache→Wayback→Chromium, SSRF, LLM summary). 26 new tests. 21 tools total. (5c90f86, 4cb7ce1, 4bf27e5)
- SearXNG Config —
searxng_urlfield in config.json, wired via SetSearXNG(). (405bc87) - MCP Client (4 Phases) — stdio/HTTP transports, auto-discovery, agent.Tool registration, resources, SSRF security. 22 tests. 359 total. (e31c08b, cc1ba8e, 7b4fe75)
- Permissions engine (Ruleset) — Replaced DeniedTools/AllowedTools with
{action, resource, effect}Ruleset. Last-match-wins evaluation. Whitelist mode (*: deny+ specific allows). FilterTools for sub-agent creation. (378a0fd) - Async task + task_collect — Background task manager for parallel sub-agent execution.
task({..., bg:true})returns task_id immediately.task_collect({id})waits for completion. (dd1688f) - Concurrent tool execution — Multiple tool calls in the same step run concurrently via goroutines + result channel. Agent waits for all to complete before next step. (e5fd457)
- Sub-agent sandbox propagation —
PermissionRequest.AgentLabeltracks which agent requested path access. TUI dialog shows[general] Write to /path?.SetAgentLabel()called before sub-agent creation. (d0653ee) - Prompt improvements — Build mode system prompt guides LLM to use
task()for parallel delegation, use relative paths, and separate paragraphs with blank lines. Explore agent has dedicated PROMPT_EXPLORE-style prompt. (effbe2c, a02d894, f66f151) - Command palette UX fix — Selecting a command from palette fills the input box. User presses Enter again to execute. No more immediate execution + stale text. (118e938)
- Status bar processing indicator — Animated dot spinner + ● fallback indicator during streaming. Shows when agent is actively processing. (6adfda0)
- Mouse selection clamp —
posFromCoordclamps to last valid row instead of returningOffset: -1when dragging past content end. 5 new tests. (39e80e5) - Multi-message step split — Each agent step creates its own assistant message with independent reasoning, tool calls, TODO snapshot.
OnStepDonecallback fires between steps. (c5f854f, 4ef98ff) - TODO snapshot per message — Each assistant message carries the TODO list state at creation time. Resume restores TODO from history. Todo render ack fixed — ackCh moved outside TODO injection block to prevent agent deadlock. (e34143c, 633a641)
- Remove redundant UI —
⏳ Running...indicator removed (spinner in status bar is sufficient).(processing...)text in input area removed.Response:label removed (message color differentiates roles). (4e832f1, b35bdd7) - [Copy] button fixes — Button row counted in
msgRowCountso next message doesn't overlap.activeButtonspreserved perMsgIdxacross renders so button stays clickable after incremental rebuild. (f9143cc, 8dffd64) - Spinner pipeline fix — Tick kept alive even during idle.
StatusStreamingpreserved across intermediate steps so spinner doesn't stop mid-task. Switched to brailleDottype (user terminal supports it). (9a0f55b, afc14e1, 42b8794) - Tab mode switch — Tab now always switches plan/build mode (removed empty-input guard). (c75d9ae)
- Ctrl+C copy fix — Character-level copy requires non-zero selection range (click without drag no longer copies single char). Ctrl+C twice now quits. (ea6711c)
- Permission dialog — 4 options (Allow once=true once, Allow session=session persisted, Always allow=config persisted, Deny). Dialog auto-shows on View() without keypress. Dialog dismissed after Deny (Resolved field). Alignment:
>separate column,[1][2]aligned. (3e6f306, 57359db, 792120f, e8cff4f, de01f54) - read_file triggers dialog — Previously returned text hint to LLM; now calls
RequestPermission()likewrite_filedoes. (e2832e3) - Session resume restores history —
inputHistorypopulated from loaded user messages.historyPosreset to -1 so Up goes to most recent entry. (444dd04, 2e6f9b3) - Duplicate tool call fix —
OnToolCallfired twice per tool (main goroutine + execution goroutine). Removed duplicate from execution goroutine. (cc5a224)
(All planned features have been implemented. MCP client supersedes the original Plugin System proposal.)
Built with ❤️ and Go