jeeVee Blog

My setup is built around docker compose, with a web app using the Monaco editor as the main interface.

Because my app runs in the browser, I keep several tabs open and work on multiple tasks in parallel. When a task needs my attention, I review the progress, approve it or add additional guidance, and then let it carry on while I switch to another tab.

img

Isolation

Each application under development gets its own docker container. The same goes for supporting services like LSPs and the LLM gateway—they each live in their own container too. This modular approach makes it easy to add new capabilities, even when they have different runtime requirements.

  %%{init: {'theme':'dark', 'themeVariables': {'primaryColor': '#6366f1', 'primaryTextColor': '#fff', 'primaryBorderColor': '#818cf8', 'lineColor': '#a5b4fc', 'secondaryColor': '#1e1b4b', 'tertiaryColor': '#312e81'}}}%%
block
columns 1
  db[["Web Editor"]]
  blockArrowId6<["&nbsp;&nbsp;&nbsp;"]>(down)
  block:FE
    Mixers["Mixers"]
    Flows
  end
  blockArrowId7<["&nbsp;&nbsp;&nbsp;"]>(down)
  block:BE
    Services["Common Services"]
    LSPs
    LLM["LLM Proxy"]
    Webdav["Webdav"]
    Redis
  end
  block:BE2
    DevContainerA["Dev Container"]
    DevContainerB["Dev Container"]
    DevContainerC["Dev Container"]
  end
  blockArrowId8<["&nbsp;&nbsp;&nbsp;"]>(down)
  D["Unified storage /source"]

  style db fill:#6366f1,stroke:#818cf8,stroke-width:3px,color:#fff,rx:10
  style LSPs fill:#a855f7,stroke:#c084fc,stroke-width:2px,color:#fff,rx:8
  style D fill:#0ea5e9,stroke:#38bdf8,stroke-width:3px,color:#fff,rx:10
  style FE fill:#0f172a,stroke:#6366f1,stroke-width:2px,rx:12
  style BE fill:#0f172a,stroke:#a855f7,stroke-width:2px,rx:12
  style BE2 fill:#0f172a,stroke:#f43f5e,stroke-width:2px,rx:12
  classDef front fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff,rx:8
  classDef back fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff,rx:8
  classDef container fill:#f43f5e,stroke:#fb7185,stroke-width:2px,color:#fff,rx:8
  class Mixers,Services,LLM,Redis front
  class Flows,Webdav back
  class DevContainerA,DevContainerB,DevContainerC container

Mixers

Mixers bring together multiple backend services into a cohesive, reusable unit. For example, I have a mixer that runs all the verification Objectives for a workspace. This way, the Planner can verify that nothing was broken and that new code changes actually implement what was intended.

Here is a snippet from the mixer that verifies the Objectives for a workspace:

 1export async function verifyObjective(context: Context): Promise<VerifyObjectiveResult> {
 2    const objectiveResults: string[] = [];
 3    if (context.workspace.objective) {
 4        for (const objective of context.workspace.objective) {
 5            const objectiveResult = await runTask(objective, context);
 6            objectiveResults.push(objectiveResult.result);
 7        }
 8    }
 9    
10    const agentResponse = await invokeAgent(
11        "general/find_files_with_errors",
12        objectiveResults.join('\n')
13    );
14    
15}

Mixers are lightweight orchestration helpers—they save agents from having to reinvent common sequences of steps.

Flows

Flows are simple wrappers around Mixers that make them launchable from the UI. Any logical grouping of tasks that a user might want to trigger—like the Planner, Coder, or local file search—is modeled as a Flow. These show up in the launcher dropdown, as seen below.

Code

LSPs

I run LSPs as separate docker containers, which makes it much easier to manage the dependencies each one needs. On top of that, there’s a unified LSP service interface that can communicate with the appropriate LSP for any given workspace.

Development containers

Every project I work on runs in a docker container. I never do any development directly on the host OS because I value the ability to:

Perhaps an even more salient reason: coding agents can generate catastrophic commands, and I don’t want those executed on my host OS. If they do generate destructive commands, I want the ability to recover to a previous stable state of the container.

I probably should have named this file verify_container_setup.ts instead of container_setup.ts (although it is under the /objectives/ folder where I keep all the Objectives for each project). lsp-gw is my LSP Gateway—the common interface for all the LSPs.

Code

1const dockerRestartResult = await dockerRestart('lsp-gw');
2console.log(`### dockerRestartResult: [${JSON.stringify(dockerRestartResult)}].`);
3
4return JSON.stringify(dockerRestartResult);

The Planner executes this container_setup.ts Objective when it is working on configuring the docker image of a dev container. So I’m not only using objectives to compile, test, etc. within the container, but also to set up the container images themselves.

As I mentioned before, I usually have two objectives for each project as shown in the screenshot below:

  1. Building the docker environment of the project (the container_setup.ts file)
  2. Compiling, testing, linting, etc the project within the container (the run_tests.ts file)

Code

Putting this modular foundation in place was a significant time investment, but it was also an enjoyable experience. Now it’s even more gratifying to use it and be able to add new extensions when I need them that validate the modularity.

<< Previous Post

|

Next Post >>