← Back to Too Good to Share

Too Good to Share / Series

CCSwitchboard

3. The components, part by part

Here is the whole thing in one drawing, followed by a plain breakdown of each part: what it is responsible for, and exactly how it talks to the others. Keep one rule in mind while you read, because it explains everything else: no component ever calls another component. They only ever call the relay in the middle, on a timer, asking "anything new?" The relay is dumb PHP on shared hosting with no way to push, so everyone polls it.

The lifecycle of a single job: dispatch out to the relay, run and stream, and the result back to the same thread
The lifecycle of a single job: dispatch out to the relay, the agent runs it and streams the output back, and the result travels out to the extension, which types it into the same chat thread. One loop, out and back.
1

The relay

Plain PHP + SQLite on dabblelabs.uk. The hub.

Holds everything in one small SQLite file: the job queue, the repo locks, results, per-thread Claude Code sessions, and the streamed output. Deliberately dumb - it just answers questions. Every other part polls it, and nothing talks to anything except the relay.

Endpoints it serves

  • POST job.php takes a new job. Body {payload, thread, continue, readonly}; grabs the repo lock, returns {id} or a 409 if the repo is busy.
  • GET poll.php hands the agent the oldest pending job and flips it to running in one step.
  • POST result.php stores a finished result and the Claude Code session id, then frees the lock.
  • append.php / output.php the streaming pair: the agent appends each output line, the feed reads back everything newer than the last line it saw.
  • wake.php the "repo free" nudge queue; register_tab.php maps a thread name to its browser tab; session.php looks up a session id to resume.
  • heartbeat.php notes the agent is alive; cancel.php flags a job to be killed.
2

The agent

C# tray app on the VM. Runs the actual work.

A .NET tray app running four workers at once, so several jobs can be in flight. When it sees a job it spawns a headless Claude Code run for it and streams that run's output back line by line. It is the only part that ever launches Claude Code.

How it talks to the relay

  • Polls GET poll.php every 2s for a new job.
  • Runs claude -p ... --output-format stream-json and posts each line to append.php as it arrives.
  • On finish, POST result.php with the result and the session id (so a later continue job can resume the same conversation).
  • Pings heartbeat.php every 30s, and checks cancel.php every 2s while a run is live.
  • If a run goes silent for 90s it kills the process tree and marks the job timed out.
The agent running quietly in the VM system tray
The agent lives in the VM's system tray, out of the way. It sits there polling the relay and spawning headless Claude Code runs, with no window to babysit.
3

The browser extension

MV3 extension in Brave. The keystone.

Lives on the Claude.ai page. It reads Claude.ai's dispatch blocks straight out of the chat, sends them to the relay, and types results and wake prompts back into the input on its own. The fiddliest piece by a distance.

How it talks

  • Finds a ccsw code block by anchoring on the thumbs-up feedback button, which only appears under Claude.ai's replies and never under mine.
  • The page itself cannot call the relay (cross-origin), so the extension's background worker does the POST job.php instead.
  • Polls GET result.php?id= every 3s for a finished job, and wake.php every 3s for a "repo free" nudge, then types the text into the right tab and clicks send, retrying with backoff until the input clears.
  • Auto-clicks the "Always allow" button on a Claude Code tool-permission dialog (the MCP approval gate) so a woken run is not left waiting on me.
  • A declarative rule rewrites its User-Agent header, because the host's firewall rejects requests with a blank one.
Close-up of the browser extension's per-thread status pills
A close-up of the extension's per-thread status pills. Each pill tracks one thread's job, so at a glance I can see which threads are running, which are waiting, and which have a result ready.
4

The popup

C# WPF tray app on the host. The doorbell.

A small tray app on the host machine. When a job finishes it shows a notification in the top-right corner; click it and it raises Brave and focuses the exact tab that dispatched the job. It only pops for jobs marked final, so intermediate steps stay quiet.

How it talks

  • Polls GET jobs.php?status=done every 3s and pops a notification only for jobs flagged final.
  • On click it raises the Brave window, then POST focus_request.php with the thread name so the extension focuses the tab that fired the job.
A desktop notification popping in the top-right corner when a job finishes
The desktop notification the popup shows when a job finishes. It only fires for jobs marked final, and clicking it raises Brave and focuses the exact tab that dispatched the job.
5

The feed

A page on the relay. A terminal that is not a terminal.

feed.php?job_id=X renders a live Claude Code run as styled HTML, not as raw terminal text. Because Claude Code emits structured stream-json, the feed can show tool calls as cards, code as code blocks, and thinking collapsed away.

How it talks

  • Polls GET output.php?job_id=&after= every 1s, asking only for lines newer than the last one it drew.
  • Polls GET status.php?id= every 2s for the run's live status header.

Read the grid top to bottom and the shape falls out: five parts, one relay in the middle, and every arrow is a poll on a timer. That single constraint - shared hosting cannot push, so everything asks - is why the whole system looks the way it does.