Building a personal ADE
I just want to paste a screenshot
At work we use remote Coder workspaces for our dev environments. This comes with many benefits!
The downside: my code doesn’t live on my laptop, so most tools can’t touch it. To run agents in our harness of choice at work, Claude Code, my options are the CLI in a terminal over SSH or the VS Code extension. Both work, but you can’t paste images into the CLI over SSH and neither runs multiple agents in a way I like.
I’d used purpose-built agent UIs for my personal work, so I knew there was a better way: an Agentic Development Environment, a self-contained app for running agents in parallel, usually with a built-in terminal, diff viewer, pull request tracking, and the like.
So I need an ADE that wraps Claude Code and works over SSH. That removes Cursor Glass, OpenCode, and Conductor, which are either their own harness or do not work over SSH. Ideally it also runs in the browser, so there’s nothing to install.
Someone already built this
T3 Code is an open source ADE that wraps Claude Code, as well as a handful of other agents. It supports image pasting, running multiple agents, and has a proper diff viewer. Best of all, it runs in the browser. I’d used it at home, so I knew I could stand the whole thing up on the workspace, forward its port to my MacBook, and have Claude Code in something that felt built for it.
I also knew it had some rough edges and some UI decisions I didn’t love. Small compared to the CLI’s and the extension’s problems, but enough that I figured I could do better, at least for my specific use case.
And since it’s open source, the efficient path was obvious! Fork it, tweak to taste, done. But since I was curious how these things actually work, building my own sounded like fun, and it was an excellent opportunity to practice using agents to build a whole app end to end.
I expected to give it a few hours, hit a wall, and throw in the towel. But what the hell, no effort is wasted.
Claude, build me an Agentic Development Environment
Still under the impression that this would be a quick experiment, I decided to name my app “Jetty” (a place to dock your agents). Had I known that I’d end up actually using it, I probably wouldn’t have named it after myself.
At its core, Jetty needs to pass my prompts to a Claude Code instance, and then forward back Claude’s actions to me. Fortunately, Anthropic provides the Agent SDK, a way to programmatically pilot an already authenticated claude CLI.
When the Jetty server receives a prompt from the client, it sends it to Claude through the Agent SDK. As events stream back, Jetty normalises them into its own format. Different harnesses emit different event shapes, and I want Jetty to eventually support multiple harnesses1. If Claude Code’s tool_use and Codex’s function_call both become a Jetty tool_call, Jetty’s UI only needs to worry about rendering tool_call.
Each conversation becomes a Jetty thread, persisted in SQLite. Each thread stores a generated title, a log of the conversation in Jetty’s event format, and the relevant Claude session id. The Agent SDK persists sessions on disk, so Jetty only keeps that id, allowing Claude to resume later with the full conversation history. Jetty’s event log is derived from Claude’s stream and is only ever sent to the client.
The client (a React SPA) talks to the server over a WebSocket, and every update the server pushes is one of two message types: an update to the chrome, which is the shell around the threads (projects, the thread list), or an event inside a specific thread (tool call, response).
The client stays subscribed to the 5 most recently viewed threads, so bouncing between them is never blocked waiting for fresh data. When the client receives an update for a thread, it stores it in IndexedDB for fast app loads.
The server also serves the client, so everything is handled by one Bun process on one port; Jetty is only ever one port forward away from my MacBook.
So, about that screenshot
When you paste an image into Claude Code locally, the claude process asks your OS for the clipboard’s contents and pulls the image bytes out itself. This is why Claude Code has you paste images with ⌃V and not ⌘V; the terminal can’t paste images, so Claude Code implements its own paste handling. Different terminals and screenshot tools change this behaviour, but that’s the high level shape of it.
I use CleanShot though, which puts a reference to the captured image’s file on the clipboard. ⌘V pastes the plain text file path, which Claude Code recognises as an image path and reads from disk.
Both of these are no-ops over SSH. Your workspace’s claude receives the ⌃V command, checks the OS clipboard, and of course finds nothing; your screenshot lives in your clipboard, on your machine. If your screenshot tool pastes file paths, the workspace’s claude receives ~/Library/.../CleanShot 2026-07-22.png, a path that has never existed on the workspace.
Jetty’s client lives in the browser, on the same machine as your clipboard. And because browsers natively handle image pasting, ⌘V hands the browser the actual bytes. They travel up the WebSocket through the SSH tunnel, arrive at the workspace, and then get passed to Claude Code as part of your prompt.
Gone are the days of keeping an images directory on the workspace and firing screenshots over via scp.
Oh boy, do I have an ADE to sell you
A few days later I had something I actually wanted to use.
Jetty is quickly becoming how I work with agents. It supports image attachments and runs parallel agents in a way I find intuitive; I can paste a screenshot into one agent, ⌃t to open a new thread, kick off some more work, then ⌃1 back to the original thread to check the diff.
Every thread in Jetty belongs to a project, which is just a directory on the workspace. It’s the directory the Agent SDK passes to Claude Code as its working directory. Selecting a project can be done entirely in the Jetty client; Jetty browses the workspace’s filesystem over the WebSocket.
Jetty is open source.
Agents all the way down
I built Jetty entirely with agents, mostly Claude Fable 5 and Grok 4.5. This was slightly more strategic than me prompting Claude to “build me an Agentic Development Environment, make no mistakes”. Because I wanted to actually understand how my ADE worked, I first spent around an hour with Claude Fable 5 planning out what the structure of Jetty would be. That is, the SPA client, the translation adapter for future harnesses, and the event log in SQLite.
This went quite smoothly because we were able to borrow from existing open source ADEs, namely T3 Code and OpenCode. There were many prompts of “Claude, how does T3 Code do XYZ?”.
Once we had a plan, I had Claude split it into “chunks”, written to a chunks.md file in the repo, to keep track of what was done and what wasn’t. When starting one, I had Claude create an implementation plan for it, stored as chunk-name.md. For example, image support was 08-image-paste.md.
Once Claude drafted a chunk’s plan, I’d discuss and tweak the plan with Claude until I was happy with it. Then I would tell Claude to “go build it”. A few minutes later I would review the output, asking Claude questions about how things worked when I was unsure. A few of these question sessions lasted over an hour, and I learnt a lot!
I should make clear that this chunk based approach was purely for my benefit to keep up with Claude. It worked for me as a natural brake to slow the fuck down. That is, to ensure the codebase was not growing at a pace I could not keep up with. When work shifted towards the client implementation, this chunk based approach naturally ceased, as I’m much more familiar with FE than BE and I can keep up much more easily just by reading the diffs Claude makes as it goes.
After each chunk was built, I had Claude delete its chunk file to prevent context bloat. I want future agents to learn the codebase from the code, not markdown.
Mind the missing planks
There are still some tools that Claude Code exposes that Jetty does not handle. The most notable gap is subagents and workflows. Jetty currently doesn’t surface when a subagent or workflow is active. This is particularly annoying, as I use these features a lot! I’m still exploring how I want Jetty to surface these in the UI.
Recently I also ran into the issue that when Claude called the AskUserQuestion tool, Jetty had no way to surface this to me, so Claude got no answers back.
That being said, I find that I can use Jetty for most of my work, and the more I use it, the more I can tweak it to exactly my preferences. And tweaking it is easy! When I hit an issue, I screenshot it, paste it into Jetty, and wait for Jetty to fix itself.
Footnotes
-
Currently Jetty only supports Claude Code as it’s what we use at work ↩