← All posts

Panda-Develop

OpenAI released GPT‑4.1 on April 14, 2025. I have the same computer today that I had then, and today there are models running on that computer that are better than GPT‑4.1.

The Panda-Develop app icon: a flat navy-and-white panda face
RTX 309024 GB, shared with work and games
Qwen3.8 27B131,072 ctx · vision · thinking
One daemonGPU arbiter + agent loop + tray

Panda-Develop is my in-house coding agent front end and local-LLM arbiter in one. It is a FastAPI daemon on 127.0.0.1 with two faces: a system tray icon that owns the GPU, and a native Qt desktop client. The rest of this post is a tour of what it does and why each piece exists.

Why Build Your Own?

I built this because I am annoyed by the agentic coding front ends currently available to me. Either I don’t like the interface, or, worse, it doesn’t support something I need.

The issue I hit consistently is memory management when running local models on my primary machine. I use this box for work and for gaming, so it has a very strong graphics card with a lot of video memory, which makes it ideal for local models. The problem is that tools like Ollama and LM Studio don’t give you enough control over loading and unloading without extra manual steps.

My tool fuses the two halves: a chat front end, and a tray service that manages the communication with Ollama and ComfyUI. What the pairing buys is one component that knows what is allowed to hold the card, which means it can hand a conversation as much context as the machine can currently spare instead of a number I picked once and forgot.

The other half of the motivation is that third-party plugins are built for frontier cloud models and degrade badly on local ones. Each of those failure modes turned into a design decision:

What breaks on a local modelWhat I did instead
Multi-thousand-token system prompts eat the usable contextShort system prompt, measured context budget
20+ tools → selection errors and schema bloatSix base tools; everything else opted into per conversation
Fuzzy search/replace blocks local models emit incorrectlyWhole-file write plus line-anchored replace, validated with retry
Silent context overflow, then degradation into nonsenseExplicit compaction, marked in the transcript
No awareness of VRAM; happily OOMs the boxVRAM arbitration is a first-class feature
Model load/unload is someone else’s problemIt is this tool’s core job

Provisioning, Not Just Loading

Loading a local model is normally presented as a one-click affair. Pick a model, it loads, off you go. On a card that is also running your desktop, it is a provisioning decision. The more memory you give to the model, the less you have to actually run your machine and the software you are building. I had to create an algorithm for the tool to use to ensure it doesn't brick the machine.

Qwen3.8 27B, weights          17.36 GB
KV cache, per 8,192 tokens   + 0.30 GB
desktop and browser, in use  ~ 2.00 GB
headroom floor, reserved     + 1.50 GB
                               on a 24 GB card

Image generation sharpens all this, though it isn’t the only reason any of it exists. ComfyUI wants around 10 GB and the coding model wants 17 to 20, so the two are declared conflicting, and declared conflicts are checked before free space. A freshly started ComfyUI is only sitting on about 0.2 GB, so there looks to be plenty of room right up until the first image generation takes the card out from under everything. When something is in the way, the daemon says so and asks rather than auto-killing:

Loading this model needs 19.6 GB of VRAM.
ComfyUI (image gen) (~10.0 GB) is holding the GPU and cannot run at the same time.
Stop it and continue?

The tray icon answers the resulting question without my clicking anything: what is holding the card right now? It stamps a letter per resident service and uses colour as the modifier.

IconMeaning
faint grey ringnothing resident, the card is free
L greenan LLM is resident
C blueComfyUI (image gen) is up
LC purpleboth, contended
L / C amberstarting, stopping, or loading
L redresident but spilled to CPU (~37% slower)
? greycannot reach the daemon

The tray menu is a flat list of profiles. A profile is a named model-plus-settings combination, so “same model, bigger context” is another one-click entry rather than a nested menu.

A Tour

The front end is conversation-first. Every session is stored in SQLite, so the thread and its entire tool-call history survive a restart, a crash, or a week off. The sidebar is the list of them, each pinned to a workspace root.

The Panda-Develop desktop client: conversation list on the left, transcript on the right, model and VRAM in the header
The client. Fifty threads in the sidebar, each labelled with its workspace and profile. The header carries the resident model and what it is holding (20.9 of 24 GB); the meter at top right is this thread’s context budget.

A turn streams over SSE: text as it arrives, tool calls as collapsible cards, approval prompts inline. Reads run freely. Edits, MCP calls and unrecognised shell commands stop and ask, showing the exact proposed change first. Safe read-only commands (git status, pytest) are auto-approved by prefix. A config-driven deny list goes the other way: shutdown, diskpart, reg delete, rm -rf /, git push --force, pip install are refused outright, cannot be approved, and beat session-wide auto-approve. They are matched as substrings rather than prefixes, so a chained cd x && shutdown /s can’t smuggle one past. This runs on my daily driver with a shell tool in the list; that had to be built in from the start rather than retrofitted.

An approval card asking to allow a chained run_command, with Deny and Allow buttons
An approval. The subtitle is the policy explaining itself: cd isn’t on the auto-approved list, and because the command is chained no prefix check applies to the rest of it anyway.

The file browser is a lazily-expanding tree of the session’s workspace, and it isn’t decorative. It is how I check the agent’s work without alt-tabbing to an editor. Markdown opens rendered, with a Raw toggle when I want to fix a line myself, and every other text file opens straight into an editable pane with its own Save button, so a quick fix never needs a trip outside the tool. Images render inline, and anything else says plainly that it’s binary and can’t be previewed.

The workspace file tree beside a rendered README.md, with Raw and Save controls in the corner
Editing in place. The workspace tree on the left, README.md rendered on the right, Raw and Save sitting right where I’d reach for them.

Starting a new conversation is where the design shows. Four choices, and the dialog states plainly that three of them are permanent:

  • Workspace. The root every file tool resolves against, with path traversal rejected. A guardrail, not a sandbox: run_command is a real shell, so the agent can reach anything I could.
  • Profile. Local Qwen3.8/3.6/Coder builds plus Muse Glimmer, and Deepseek v4 Flash as a cloud option that bypasses the arbiter entirely. The picker is arbiter-aware: every entry shows its cost and what it would leave free.
  • Tools. Six core tools are always present; everything else (charts, diagrams, image generation, MCP servers) is opted in per conversation and fixed at creation.
  • Reasoning effort isn’t fixed at creation; the Think dropdown changes it anytime. Each model exposes what it supports, and each level reserves real context, from 16,384 tokens at low up to 122,880 at max.
The New conversation dialog: workspace, a list of profiles, a reasoning row, and tool chips
Everything a thread is, decided once. Each profile previews what it costs and what it would leave free, and the footer does the subtraction: 131,072 of context, less the 16,384 reserved for output at this reasoning level, is 114,688 tokens to actually work in.

The Numbers on the Screen

Three readouts, because all three are things I used to guess at.

tok/s, measured live. Includes network time on cloud profiles, and it’s the fastest way to notice a model has spilled to CPU.

The context budget meter, top right of the first screenshot, fills toward compaction at 80% of the real, reserve-adjusted token count: 114,688 here, not the profile’s raw 131,072.

Compaction, marked in the transcript. Crossing the line triggers a summary with a visible marker, so “the model got dumber” becomes “the model compacted here.” Cloud profiles carry a running cost estimate in the same footer.

The desktop client isn’t the only way in. A companion mobile app talks to the same daemon, so a thread I started at my desk is right there to pick back up from my phone.

The Panda-Develop mobile app showing a list of existing conversation threads, each labelled with its workspace path and profile
Same daemon, different screen. The mobile client reads the same SQLite-backed threads as the desktop app, so a conversation started at the desk is ready to continue on the go.

Where It Stands

I now have a coding tool with a UX that works exactly the way I like it. I no longer max my Claude subscription every single week because my local models are capable of handling so much work.

The same machine that couldn’t run a useful coding agent locally in April 2025 now runs one that beats what I was paying for then.