Building a Claude Code Terminal — One Window, No Compromise
CLAW was the quick win. A polished launcher that opens Claude Code in Windows Terminal — but still two windows. Still switching tabs. Still losing context between the workbench and the session.
Today was about fixing that properly.
The Name Problem
The project started as claude-term. Accurate but uninspiring. Now it's provisionally called Crustacean — a crab shell built around Claude. It writes itself, but it may not be the final name. The code ships when the name is right.
What It Is
A purpose-built TUI application with an embedded terminal. Not a launcher that opens something else — the terminal runs inside the app. Claude Code lives in the centre panel. The project browser lives on the left. Commands and session history sit alongside it. One window, one workflow.
┌─────────────────────────────────────────────────────────┐
│ crustacean — // claude terminal — indigo-nx │
├──────────┬──────────────────────────────────┬───────────┤
│ PROJECTS │ [Sonnet 4.6 v] [BROWSE] ... │ SESSIONS │
│ ├──────────────────────────────────┤ │
│ > viewsh │ │ [*] gigs │
│ gigs │ D:\indigoNx>claude │ ... │
│ vessel │ │ [-] vessel│
│ irnode │ (Claude Code running here) │ ... │
│ blog │ │ │
├──────────┤ │ [POP] │
│ COMMANDS │ │ │
│ /new │ │ │
│ /resume │ │ │
└──────────┴──────────────────────────────────┴───────────┘
The Embedded Terminal
This is the whole problem on Windows. Claude Code is a full TUI — it uses escape codes, cursor positioning, live streaming output, interactive prompts. Pipe it through subprocess and it detects no real TTY, degrades, strips its own colour and interaction. You need a real pseudo-terminal.
On Windows that means ConPTY — the Windows pseudo-console API, available since Windows 10 1809. The pywinpty package wraps it. You spawn cmd.exe inside a ConPTY, read output in a background thread, feed it through pyte (a terminal emulator state machine that tracks a character grid including colours, bold, cursor position) and render that grid as Rich.Text objects inside a Textual Strip.
Claude Code running inside this widget thinks it's in a real terminal. It gets full colour, full interactivity, streaming output — the complete experience.
What Got Built Today
Architecture Decision
Two approaches were considered:
- Embedded only — everything in the one window
- External only — Claude always in a separate Terminal window, sidebar just for navigation
Neither was quite right. The decision: embedded as primary, pop-out as option.
The embedded terminal is the default and the main workflow. But the sessions panel has a POP button on every session — one click and that session opens in an external Windows Terminal window. Want a cleaner workspace for a long session? Pop it out. Want everything in one place? Leave it embedded.
What's Working
- Full three-panel layout — project browser, terminal, sessions
- Real PTY via winpty (ConPTY) with ANSI parsing via pyte
- Project browser scans workspace, detects stack (Next.js, Python, Rust, Arduino, Go)
- Click a project — terminal injects
cd /d <path> - Model dropdown — Sonnet 4.6, Opus 4.6, Haiku 4.5, persisted between sessions
- Session tracking — all sessions saved to disk, shown in right panel
- Pop-out button wired up on each session
- PyInstaller exe built and confirmed running
- Desktop shortcut created
Bugs Fixed
A few things caught during the first full test run:
- Emoji encoding — folder icons and status bullets in the tree were Unicode symbols (
📁,●). Windows Terminal was throwing encoding errors. Replaced with ASCII:[D],[*],[-]. - Em-dash in command list — the
—character in the command panel caused encoding failures. Replaced with regular-. - SessionPanel missing — the sessions panel was fully built but not wired into the layout. Fixed — it now renders as the right sidebar.
- Exe driver error — PyInstaller was building in GUI mode (
console=False), which strips the console from the process. Textual's Windows driver needs a real console to initialise. Rebuilt withconsole=True.
The PTY: Lessons Learned
Building the embedded terminal turned up a handful of things worth noting for anyone going down this path:
- The pip package is
pywinptybut the import iswinpty— this caused the first 20 minutes of confusion - The
envparameter forwinpty.PTY.spawn()must be a null-delimited string, not a dict — the API is not what you'd expect cmd.exetakes time to produce its first output after spawn — don't assume silence means failure- Textual's
Workerthread usesself.app.call_from_thread()notself.call_from_thread()— the app reference is correct, not self text.render()returns a generator — must be converted to list before passing toStrip()textual-terminal(a third-party PTY widget) only works on Unix — it importsfcntlwhich doesn't exist on Windows. Not usable here at all.
Key Bindings
| Key | Action |
|-----|--------|
| Ctrl+N | New Claude Code session in current project |
| Ctrl+R | Resume last session (--continue) |
| Ctrl+E | End session (sends /exit) |
| Ctrl+T | Focus terminal |
| Ctrl+P | Focus project browser |
| Ctrl+Q | Quit |
Stack
- Python 3.12
- Textual (TUI framework, panels, keybindings)
- pywinpty 3.0.3 (ConPTY wrapper)
- pyte 0.8.2 (terminal emulator / ANSI parser)
- Rich (rendering, bundled with Textual)
Current Status
The framework is built. Layout renders correctly. PTY spawns. ANSI pipeline is wired. The exe runs.
What's not fully tested yet: Claude Code streaming inside the embedded terminal under load, the pop-out flow end-to-end, session resume from the sidebar. These are the next things to verify in a proper sit-down test session.
The infrastructure is done. The remaining work is tuning and testing the live experience — which is the part you can't shortcut.
The launcher was CLAW. The full TUI is this — whatever it ends up being called. One window, proper terminal, project browser, session history. The Claude Code workbench.