Claude Code (Part 9)
Neil Haddley • February 22, 2026
Using an AI coding agent to create an AI coding agent
In a twist of recursive thinking, I built a local AI coding agent using Claude Code, another AI coding agent. I started with a single prompt to define the tech stack (Next.js 15, TypeScript, Tailwind, and Ollama with qwen2.5-coder:7b) and built up to a full implementation with a three-column web UI and a CLI. Along the way I worked through file trees, chat interfaces, and project-specific instructions via AGENT.md — and ended up with a fully functional AI coding agent, built by an AI coding agent.
Prompt
MARKDOWN
1Build a lightweight, local AI coding agent (inspired by Open Hands) with a browser UI and a CLI. The agent uses Ollama (default model `qwen2.5-coder:7b`) 2 3## Tech Stack 4- **Next.js 15** (App Router) + TypeScript + Tailwind CSS for UI 5- **CLI**: TypeScript, run with `tsx` 6- **LLM**: Ollama (URL configurable) 7 8## Web UI (Three‑Column Layout) 9- **Left sidebar (224px)**: File tree (depth 4, ignores blacklisted dirs). 10- **Center**: File viewer. 11- **Right sidebar (384px)**: Chat panel with SSE‑streamed agent responses, expandable tool call badges, model selector (only tool‑capable models enabled). 12- **Workspace**: Folder picker (path saved to localStorage); validated on next load. 13 14Do you have any questions?
PROMPT
1If AGENT.md present in workspace root, append (≤10k chars) to system prompt as `## Project instructions`.
Prompt
MARKDOWN
1## User Interface – Center Column (Split View) 2The center column is divided into two resizable sections: 3 4- **Top – File Viewer**: Displays the content of the selected file (read‑only). Its height shrinks when the terminal panel is opened. 5- **Bottom – Terminal Panel**: A collapsible panel (default height 260px) that can be expanded or hidden via a 26‑px toggle bar (▸ Terminal) at the very bottom of the UI. 6 7### Terminal Panel Features 8- **Command Execution**: Runs shell commands via `spawn('sh', ['-c', ...])`, streaming real‑time output over SSE (`/api/terminal`). 9- **Color‑Coded Output**: 10 - `$` command – blue 11 - stdout – white 12 - stderr – red 13 - system messages (e.g., "Command killed") – grey italic 14- **Command History**: Navigate with up/down arrows (deduplicated, capped at 100 entries). 15- **Process Control**: 16 - **Kill button** – sends abort signal via `AbortController` (also responds to Ctrl+C). 17 - **Clear button** – resets the output display.
Command line interface (CLI)
File: src/lib/agent.ts
Signature
TYPESCRIPT
1export async function runAgent( 2 messages: Message[], 3 model: string, 4 workspace: string, 5 ollamaUrl = DEFAULT_OLLAMA_URL, // default: "http://localhost:11434" 6 emit: (event: SSEEvent) => void, 7): Promise<void>
Purpose
Runs the agentic loop for a single conversation turn. It sends messages to an Ollama model, executes any tool calls the model requests, and feeds results back until the model produces a final text response. Results are delivered incrementally via the emit callback rather than returned — this keeps the function transport-agnostic (it is used by both the HTTP SSE route and the CLI).
Parameters
| Parameter | Description |
|---|---|
messages: Message[] | Conversation history from the caller (user/assistant turns). |
model: string | Ollama model name, e.g. "qwen2.5-coder:7b". |
workspace: string | Absolute path to the workspace directory. Passed to every tool call and used to load AGENT.md. |
ollamaUrl: string | Base URL of the Ollama instance. Defaults to http://localhost:11434. |
emit: (event: SSEEvent) => void | Callback invoked for every observable event (text chunks, tool calls, errors, done). |
SSE Events Emitted
type | When | Key fields |
|---|---|---|
text | Model produces output text (streamed in ~40-char chunks) | content: string |
tool_call | Before a tool is executed | id, name, args |
tool_result | After a tool returns | id, name, result |
error | Ollama request fails or streaming throws | message: string |
done | Loop exits normally | — |
How It Works
1. History initialisation
CODE
1system prompt (base rules + optional AGENT.md content) 2 └─ buildSystemPrompt(workspace) reads AGENT.md from workspace root, 3 appends it under "## Project instructions" (capped at 10 000 chars) 4incoming messages (user / assistant turns)
2. The loop (max MAX_TOOL_ITERATIONS = 20 iterations)
3. Tool call resolution (native vs. text fallback)
Ollama models on the TOOL_CAPABLE_PREFIXES list return tool calls in the structured message.tool_calls field. Models that don't support native function calling may embed JSON in their text response instead. extractTextToolCalls() (src/lib/parseToolCalls.ts) handles the fallback:
- Scans for markdown code fences containing JSON.
- Tries to parse the entire string as JSON.
- Uses a bracket scanner for partial/embedded JSON objects.
When the fallback fires, msg.tool_calls and msg.content are normalised before being pushed to history so that subsequent turns remain consistent.
4. Available tools
Defined in TOOL_DEFINITIONS (src/lib/tools.ts) and dispatched via dispatchTool:
| Tool name | What it does | Key limits |
|---|---|---|
read_file | Reads a workspace file, returns content with line numbers | Path traversal blocked by resolveSafe() |
write_file | Creates or overwrites a file; creates parent dirs | Sets 0o755 for .sh, .py, etc. |
run_command | Runs a shell command via exec in the workspace dir | 30 s timeout, 1 MB output buffer |
search_files | grep -r -n across the workspace | Up to 100 matches |
5. Final response delivery
After the last tool round-trip the model should produce a plain text response (no further tool calls). Two paths:
textContentis set — the non-streamingchat()call already has the full text. It is broken into 40-character chunks and emitted with a 10 ms artificial delay to give the UI a smooth streaming feel.textContentis empty — the model returned an empty content body (some Ollama models do this). A second, streamingchatStream()call is made and each chunk is forwarded directly.
























