One Desktop, Two Environments: Designing a Clean Windows + WSL AI Development Workflow

Windows handles the interface. Ubuntu/WSL runs OpenDesign, local AI agents, developer tools, services, credentials, and projects
Learn how to run OpenDesign as a Windows-facing application while keeping Codex, OpenCode, Claude, Gemini, Antigravity, Node, Git, credentials, services, and projects inside Ubuntu on WSL.
Windows and WSL AI development workflow

What is my workflow, and what did I want to achieve?

I run an AI agency, Artisan Intuition (artisanintuition.com), where I periodically build websites, landing pages, dashboards, product interfaces, campaign pages, and other digital experiences. A recurring part of that work is turning functional ideas into something that looks professional, credible, and visually strong enough to hold attention.

My usual workflow already combines tools such as Claude Code, Codex, Antigravity, and content-generation platforms. That works well for engineering and implementation, but I wanted to strengthen the design layer of the workflow as well.

OpenDesign caught my attention because it is open source and provides another way to explore layouts, visual patterns, interface directions, and design ideas that I can then refine into client-ready deliverables. I wanted to use it alongside the AI coding agents I already rely on, rather than introduce an entirely separate development environment.

That led to a more fundamental architectural question:

Where should the actual development environment live?

Windows gives me the desktop experience I want - Chrome, shortcuts, native applications, launchers, and a familiar UI. But increasingly, the development stack itself belongs in Linux: coding agents, Node, Git, package managers, project files, credentials, local services, and command-line tooling.

The obvious temptation was to split the setup.

Run OpenDesign on Windows.

Run OpenCode, Codex, Claude, Gemini, Antigravity, Node, Git, and the projects inside WSL.

That would work, but it would create two development environments with different paths, credentials, runtimes, process models, and tooling.

I wanted something cleaner.

The architecture I ended up with is:

Windows handles the interface. Ubuntu/WSL owns the runtime.

OpenDesign itself runs inside WSL alongside the AI coding agents and development tools. Windows simply presents the experience through Chrome and a few lightweight launchers.

The result feels like a Windows application without moving the actual development runtime out of Linux.


The architecture I wanted

The target was not:

Windows OpenDesign
→ WSL agents

It was this:

Windows and WSL AI development workflow

Windows 11
│
├── Chrome
├── Start / Stop / Restart launchers
│
└── http://127.0.0.1:7456
         │
         ▼
Ubuntu / WSL2
│
├── OpenDesign
├── OpenDesign systemd service
├── OpenCode
├── Codex
├── Claude
├── Gemini
├── Antigravity
├── Node / pnpm
├── Git
├── credentials
└── project files

The guiding rule became:

Keep the runtime, agents, credentials, services, tools, and projects together in Linux. Let Windows provide the interface.

That one decision simplified almost everything that followed.


Why WSL should own the development runtime

There are several ways to combine Windows and WSL.

One common approach is to install the main application on Windows, then connect it to tools and files living inside WSL.

That looks convenient initially, but it introduces ambiguity:

  • Windows paths versus Linux paths
  • where credentials live
  • which Node installation is being used
  • which environment owns package managers
  • where agent subprocesses execute
  • whether tools see Windows files or WSL files
  • which shell configuration is active
  • how localhost services are exposed
  • how browser callbacks work
  • how child processes are cleaned up
  • which runtime should be updated when something breaks

For AI coding agents, these details matter.

Tools such as OpenCode, Codex, Claude, Gemini, and Antigravity are not just chat interfaces. They inspect files, execute commands, launch processes, authenticate against remote services, and depend heavily on their local environment.

That makes the correct boundary clearer:

The agent runtime should live next to the project runtime.

Since my projects and CLI tooling already live inside Ubuntu on WSL, OpenDesign should live there too.

Windows should not become a second development machine.


Defining the boundary: Windows for UX, WSL for runtime

The final separation is simple.

Windows owns

  • Chrome
  • desktop shortcuts
  • Start menu integration
  • PowerShell launchers
  • CMD launchers
  • visual interaction
  • day-to-day desktop UX

WSL owns

  • OpenDesign
  • OpenCode
  • Codex
  • Claude
  • Gemini
  • Antigravity
  • Node
  • pnpm
  • Git
  • credentials
  • environment variables
  • project files
  • systemd services
  • local development processes
Windows vs WSL responsibility diagram

That gives the setup a much cleaner mental model.

Windows is not hosting the development environment.

Windows is exposing it.


Installing OpenDesign from source inside WSL

I installed OpenDesign directly from its Git repository inside Ubuntu.

My checkout lives under ~/tools/ directory:

For example:

~/tools/open-design

After installing dependencies, the obvious development command was:

pnpm tools-dev run web

This worked, but it revealed the first architectural problem.

A typical startup looked like:

Web:    http://127.0.0.1:42373/
Daemon: http://127.0.0.1:44757/

The ports changed every time.

That is fine for source development. The tooling can dynamically allocate free ports.

But it is inconvenient if you want OpenDesign to behave like a stable local application.

I did not want to bookmark a new URL every session or keep reconfiguring a Windows client.

That eventually pushed the setup away from dev-mode ports and toward one stable endpoint.


The od command collision

OpenDesign provides an od command.

Linux already has:

/usr/bin/od

which is the GNU octal dump utility.

That creates a real command collision.

Running:

type -a od

showed both versions.

The OpenDesign wrapper needed to appear before the Linux utility:

/home/<user>/.local/bin/od
/usr/bin/od
/bin/od

At first this looked like a minor PATH issue.

Later, it became one of the most important details in the architecture because systemd and interactive shells do not necessarily resolve commands the same way.

The lesson is simple:

Never assume a command name is unambiguous just because it works in your terminal.

When systemd, cron, WSL launchers, or non-interactive shells are involved, explicit command paths are safer.


Cleaning up PATH properly

During setup I also noticed that repeatedly sourcing .bashrc duplicated PATH entries.

The cause was the common pattern:

export PATH="$HOME/.local/bin:$PATH"

Every time .bashrc is sourced, the same directory gets prepended again.

I replaced that with an idempotent helper.

Conceptually:

path_prepend() {
  case ":$PATH:" in
    *":$1:"*) ;;
    *) PATH="$1:$PATH" ;;
  esac
}

That gave predictable ordering for:

  • ~/.local/bin
  • OpenCode
  • Vela
  • Composio
  • other CLI tools

and prevented PATH inflation.

This is a small detail, but in a WSL environment full of CLI tooling it is worth fixing early.


There were few issues starting Open Design succefully.

Resolving the Vela CLI error

Before the browser-integration issue, OpenDesign failed earlier in the onboarding flow with:

vela binary not found; install vela or configure VELA_BIN

This was not related to the Linux od command conflict. OpenDesign was trying to use the Vela CLI, but Vela was not installed or discoverable inside the WSL environment.

I installed Vela into a user-owned directory rather than mixing it into the system Node installation:

VELA_PREFIX="$HOME/.local/share/vela"

npm install --global \
  --prefix "$VELA_PREFIX" \
  @powerformer/vela-cli

Then I added its binary directory to PATH:

export VELA_PREFIX="$HOME/.local/share/vela"

and, using my path_prepend helper:

path_prepend "$VELA_PREFIX/bin"

The equivalent direct PATH configuration would be:

export PATH="$VELA_PREFIX/bin:$PATH"

After reloading the shell:

source ~/.bashrc

I verified the installation:

which vela
vela --version

The expected result was that vela resolved somewhere under:

~/.local/share/vela/bin/vela

At that point the original:

vela binary not found

error disappeared.

However, authentication exposed a second WSL-specific issue.

Running:

vela login

successfully generated a device activation URL and code, but Vela reported:

Warning: could not open browser automatically:
open browser: exec: "xdg-open": executable file not found in $PATH

That was a separate problem. Vela itself was now working, but it expected the standard Linux xdg-open mechanism to launch the authentication URL. Since my browser runs on Windows while Vela runs inside WSL, I needed a small WSL-to-Windows browser bridge.

That leads directly to the next part of the setup.


Browser launching from WSL into Windows

The next issue appeared during Vela authentication.

OpenDesign reached the login flow, but Vela could not open the browser.

The error was:

exec: "xdg-open": executable file not found in $PATH

WSL interoperability itself was working.

For example:

/mnt/c/Windows/explorer.exe https://example.com

could launch something on the Windows side.

But Linux applications expect Linux conventions.

Vela wanted:

xdg-open

Windows uses a different mechanism for opening URLs.

So I needed an explicit bridge.


Why explorer.exe was not enough

The first wrapper used:

explorer.exe

That worked for simple cases, but at one point Windows opened the Documents folder instead of the intended web page.

Then I tried:

cmd.exe /c start

That opened URLs more reliably, until query parameters contained:

&

The Windows command shell interpreted the ampersand as a command separator and truncated the URL.

That is a problem for authentication links, which often contain multiple query parameters.

The better approach was to call Windows’ registered URL protocol handler directly.

~/.local/bin/xdg-open

#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 1 ]]; then
    printf 'Usage: xdg-open URL\n' >&2
    exit 1
fi

exec /mnt/c/Windows/System32/rundll32.exe \
    url.dll,FileProtocolHandler "$1"

and then

chmod +x ~/.local/bin/xdg-open

I also created a matching browser wrapper:

~/.local/bin/win-browser

#!/usr/bin/env bash
exec /mnt/c/Windows/System32/rundll32.exe url.dll,FileProtocolHandler "$1"
chmod +x ~/.local/bin/win-browser

and exported:

export BROWSER="$HOME/.local/bin/win-browser"

This gave Linux tools a reliable way to say:

“Open this URL”

while Windows remained responsible for the actual browser.


Keeping Windows PATH out of Linux

My WSL configuration intentionally uses:

/etc/wsl.conf

including:

appendWindowsPath = false

I prefer this because I do not want every Windows executable directory injected into Linux PATH.

That keeps command resolution cleaner and reduces accidental collisions.

Instead of globally exposing Windows executables to Linux, I use targeted bridges for the exact integration points I need.

That leads to a better principle:

Prefer explicit interoperability over global path mixing.


NVM, Node, Corepack, and pnpm

Another issue appeared after opening a fresh terminal.

pnpm disappeared.

Further inspection showed that Node itself was not active.

Running:

nvm current

returned:

none

while:

nvm ls

showed Node was installed.

The problem was the default alias.

It pointed to:

lts/*

but the latest LTS alias had moved to a version that was not installed locally.

The fix was to pin the default Node version explicitly.

Conceptually:

nvm use 24
nvm alias default 24

That made new shells deterministic again.

For a development workstation with many CLI tools, I prefer explicit versions over moving aliases.

Automatic upgrades are convenient until one of the tools in the stack stops working.


Restoring pnpm through Corepack

Once Node was active again, Corepack was available.

I enabled it and restored pnpm through the Node toolchain.

# Verify Corepack is available from the active Node installation
command -v corepack
corepack --version

# Enable package-manager shims for the current Node version
corepack enable
hash -r

# Let Corepack provision the pnpm version required by the project
pnpm --version

The key decision here was not to mix:

  • Ubuntu’s Node packages
  • NVM-managed Node
  • random global npm installs
  • system package managers
  • Corepack-managed package managers

The cleaner rule is:

If NVM owns Node, keep the surrounding Node toolchain inside the same ecosystem.

That reduces version ambiguity later.


OpenDesign hosted mode versus local agents

OpenDesign supports different execution paths.

During setup I initially used the hosted login path.

That worked.

But it was not the architecture I wanted.

My objective was to use local coding agents already installed inside WSL.

That means the relevant execution model is:

OpenDesign
    ↓
Local agent CLI
    ↓
Local credentials
    ↓
Local project filesystem

rather than:

OpenDesign
    ↓
Hosted runtime

This distinction matters because authentication happens at different layers.

OpenDesign Hosted authentication does not authenticate Codex.

Vela authentication does not authenticate OpenCode.

Each local agent remains responsible for its own credentials and model configuration.


Codex authentication inside WSL

When I first selected Codex through OpenDesign, the connection failed with an authentication error.

The message indicated that the access token could not be refreshed.

The problem was not OpenDesign.

The Codex CLI itself needed to be authenticated inside WSL.

After logging into Codex directly in Ubuntu and validating that the CLI worked independently, OpenDesign’s connection test passed.

That produced a useful troubleshooting rule:

Validate the underlying CLI before debugging the orchestrator.

For any local agent, first confirm:

  • does the binary run?
  • is it authenticated?
  • can it see the expected models?
  • can it access the project?
  • can it execute independently?

Only then test it through OpenDesign.


Why the Codex model list looked outdated

At one point OpenDesign only showed older Codex models.

It looked like OpenDesign had a stale model catalog.

The actual cause was simpler.

The Codex CLI itself was outdated.

After updating Codex inside WSL, the newer models appeared.

That reinforced an important design principle:

Treat OpenDesign as the orchestrator, not the owner of every tool it invokes.

OpenCode, Codex, Claude, Gemini, and other local agents should be independently managed and updated.


OpenCode compatibility problem

OpenCode was the agent I originally expected to use most heavily.

OpenDesign detected it correctly.

But the connection test failed.

The error showed that OpenDesign was launching OpenCode with flags such as:

--dir

and:

--pure

My installed OpenCode version did not support those flags.

Running:

opencode run --help

confirmed that.

I then searched the OpenDesign source and found references to those arguments.

This was not:

  • a WSL problem
  • an authentication problem
  • a PATH problem
  • an OpenCode installation problem

It was an adapter compatibility issue.

I deliberately chose not to patch the OpenDesign checkout.

That decision was important.

I want:

git pull

to remain boring.

Local patches inside a frequently updated upstream repository create future merge work.

For now, Codex works through OpenDesign and OpenCode can be revisited when compatibility improves upstream. OpenCode is still an open issue at the time of writing this article.


Why I moved away from pnpm tools-dev run web

At this point everything basically worked, but I was still using:

pnpm tools-dev run web

That command is excellent if you are developing OpenDesign itself.

It starts:

  • the OpenDesign daemon
  • the web development server
  • development-mode routing
  • separate ports
  • source-oriented lifecycle tooling

But I was not modifying OpenDesign.

I was using it.

That changes the ideal runtime.


Moving to one stable OpenDesign endpoint

I tested:

od --no-open

This started OpenDesign on the predictable default endpoint:

http://127.0.0.1:7456

That was exactly what I wanted.

Initially, opening:

http://127.0.0.1:7456

in Windows Chrome returned:

404

The daemon was healthy, but the web UI had not yet been built.

Building the OpenDesign web package:

cd ~/tools/open-design

pnpm --filter @open-design/web build

the same endpoint served the UI successfully.

That simplified the architecture from:

Windows Chrome
    ↓
random web port
    ↓
random daemon port

to:

Windows Chrome
    ↓
127.0.0.1:7456
    ↓
OpenDesign web + daemon

Much cleaner.


What od --no-open means in this architecture

The command:

od --no-open

starts OpenDesign without trying to launch a browser automatically.

That is exactly what I want.

WSL owns the runtime.

Windows owns the browser.

The Windows launcher can decide when Chrome should open.

That keeps the responsibility boundary clean.


The first background-process attempt

I initially created custom scripts:

opendesign-start
opendesign-stop
opendesign-restart

The start script used:

nohup

and background execution.

This worked from an interactive WSL shell.

But launching it from Windows through:

wsl.exe

exposed a lifecycle issue.

The daemon could become healthy and then disappear after the short-lived Windows-to-WSL invocation ended.

That was a sign I was solving the wrong problem.

I did not need increasingly elaborate PID scripts.

I needed a real process manager.


Validating the runtime with a foreground launcher

Before switching to systemd, I created a simple foreground launcher.

~/bin/opendesign-run

#!/usr/bin/env bash
set -euo pipefail

export NVM_DIR="$HOME/.nvm"

if [ -s "$NVM_DIR/nvm.sh" ]; then
  . "$NVM_DIR/nvm.sh"
fi

nvm use default >/dev/null

cd "$HOME/tools/open-design"

exec od --no-open

The important line was:

exec od --no-open

No backgrounding.

No nohup.

No PID file.

When launched from Windows using:

wsl -d Ubuntu-24.04 -- bash -lc "opendesign-run"

the command stayed attached and OpenDesign worked correctly.

That proved the OpenDesign runtime itself was fine.

The remaining problem was service lifecycle.


Using systemd properly

My Ubuntu WSL installation already had systemd enabled.

So I created a systemd user service.

~/.config/systemd/user/open-design.service

[Unit]
Description=OpenDesign WSL Daemon
After=default.target

[Service]
Type=simple
ExecStart=/home/mumehta/bin/opendesign-run
WorkingDirectory=/home/mumehta/tools/open-design

Restart=on-failure
RestartSec=2

KillMode=control-group
TimeoutStopSec=15

[Install]
WantedBy=default.target

The service manages OpenDesign as a normal Linux process.

The key settings include:

Type=simple
ExecStart=...
WorkingDirectory=...
Restart=on-failure
KillMode=control-group

KillMode=control-group is especially useful.

The OpenDesign process tree includes:

  • Corepack
  • pnpm
  • Node
  • the OpenDesign daemon

Systemd manages the entire process group instead of requiring me to discover and kill individual child PIDs.

The lifecycle is now:

systemctl --user start open-design
systemctl --user stop open-design
systemctl --user restart open-design
systemctl --user status open-design

Logs are available with:

journalctl --user -u open-design

This is much cleaner than custom PID management.


The systemd od surprise

The first systemd attempt failed.

The log showed:

od: unrecognized option '--no-open'

That was confusing because:

od --no-open

worked in my normal terminal.

The explanation came back to the earlier command collision.

My interactive shell resolved:

od

to:

~/.local/bin/od

Systemd did not source .bashrc.

It resolved:

od

to:

/usr/bin/od

the GNU octal dump program.

So systemd was effectively trying to run:

/usr/bin/od --no-open

which obviously failed.

The solution was to stop relying on PATH.

The launcher now executes the OpenDesign wrapper explicitly.

~/bin/opendesign-run

#!/usr/bin/env bash
set -euo pipefail

export HOME="/home/mumehta"
export NVM_DIR="$HOME/.nvm"

# Load NVM explicitly because systemd does not source ~/.bashrc
if [ -s "$NVM_DIR/nvm.sh" ]; then
    . "$NVM_DIR/nvm.sh"
else
    echo "ERROR: NVM not found at $NVM_DIR/nvm.sh" >&2
    exit 1
fi

nvm use default >/dev/null

cd "$HOME/tools/open-design"

# Use the OpenDesign wrapper explicitly.
# Do not rely on PATH because Linux also provides /usr/bin/od.
exec "$HOME/.local/bin/od" --no-open

Conceptually:

exec "$HOME/.local/bin/od" --no-open

After that:

systemctl --user status open-design

showed:

Active: active (running)

and the logs showed:

[od] listening on http://127.0.0.1:7456

At that point, the WSL runtime was stable.


Why OpenDesign runs as a systemd user service

The service lives under:

~/.config/systemd/user/open-design.service

and is managed with:

systemctl --user

rather than a machine-wide root service.

That is intentional.

OpenDesign is part of my development environment, not a system daemon for every user.

It should run under my user context and naturally inherit access to:

  • my home directory
  • my project files
  • my credentials
  • my development tools
  • my Node environment

A user service is the right abstraction.


Making the launcher self-contained

One more important detail:

systemd does not automatically load .bashrc.

So opendesign-run cannot assume that NVM, PATH modifications, or shell aliases already exist.

The launcher explicitly prepares its own environment.

opendesign-run

#!/usr/bin/env bash
set -euo pipefail

export HOME="/home/mumehta"
export NVM_DIR="$HOME/.nvm"

if [ -s "$NVM_DIR/nvm.sh" ]; then
    . "$NVM_DIR/nvm.sh"
else
    echo "ERROR: NVM not found at $NVM_DIR/nvm.sh" >&2
    exit 1
fi

nvm use default >/dev/null

cd "$HOME/tools/open-design"

exec "$HOME/.local/bin/od" --no-open

It initializes:

  • HOME
  • NVM_DIR
  • NVM
  • the default Node version
  • the explicit OpenDesign wrapper path

That means the same launcher works consistently whether started from:

  • Ubuntu terminal
  • systemd
  • Windows PowerShell
  • Cmder
  • wsl.exe

This is much more robust than relying on interactive shell state.


Starting OpenDesign from Windows

Once systemd owned the Linux-side lifecycle, the Windows side became straightforward.

I keep the launcher scripts under:

C:\Users\<username>\tools\scripts\opendesign

The folder contains:

Start-OpenDesign.ps1
Stop-OpenDesign.ps1
Restart-OpenDesign.ps1

The start script:

  1. invokes WSL
  2. asks systemd to start OpenDesign
  3. waits for OpenDesign’s health endpoint
  4. opens the Windows browser

Start-OpenDesign.ps1

# Start-OpenDesign.ps1

$Distro = "Ubuntu-24.04"
$Url = "http://127.0.0.1:7456"

wsl.exe -d $Distro -- systemctl --user start open-design

if ($LASTEXITCODE -ne 0) {
    Write-Host "Failed to start OpenDesign."
    exit 1
}

for ($i = 0; $i -lt 30; $i++) {
    try {
        Invoke-WebRequest "$Url/api/health" `
            -UseBasicParsing `
            -TimeoutSec 1 `
            -ErrorAction Stop | Out-Null

        Write-Host "OpenDesign is ready."
        Start-Process -FilePath $Url
        exit 0
    }
    catch {
        Start-Sleep -Milliseconds 500
    }
}

Write-Host "OpenDesign started but did not become healthy in time."
exit 1

The stop script asks systemd to stop the service.

Stop-OpenDesign.ps1

# Stop-OpenDesign.ps1

$Distro = "Ubuntu-24.04"

wsl.exe -d $Distro -- systemctl --user stop open-design

if ($LASTEXITCODE -eq 0) {
    Write-Host "OpenDesign stopped."
    exit 0
}

Write-Host "Failed to stop OpenDesign."
exit 1

The restart script performs a systemd restart, waits for health, and opens the UI again.

Restart-OpenDesign.ps1

# Restart-OpenDesign.ps1

$Distro = "Ubuntu-24.04"
$Url = "http://127.0.0.1:7456"

wsl.exe -d $Distro -- systemctl --user restart open-design

if ($LASTEXITCODE -ne 0) {
    Write-Host "Failed to restart OpenDesign."
    exit 1
}

for ($i = 0; $i -lt 30; $i++) {
    try {
        Invoke-WebRequest "$Url/api/health" `
            -UseBasicParsing `
            -TimeoutSec 1 `
            -ErrorAction Stop | Out-Null

        Write-Host "OpenDesign restarted and is ready."
        Start-Process -FilePath $Url
        exit 0
    }
    catch {
        Start-Sleep -Milliseconds 500
    }
}

Write-Host "OpenDesign restarted but did not become healthy in time."
exit 1

I no longer need to manually open Ubuntu or navigate to the OpenDesign repository.


Health checks instead of arbitrary sleeps

The Windows start and restart scripts do not simply wait for a fixed number of seconds.

They poll:

http://127.0.0.1:7456/api/health

This matters.

A fixed sleep says:

“OpenDesign is probably ready by now.”

A health check says:

“OpenDesign is actually ready.”

If startup is quick, the browser opens quickly.

If startup takes longer after an update, the launcher waits.

If startup fails entirely, the launcher reports failure instead of opening a broken page.

That is the right behavior.


CMD wrappers for an application-like experience

PowerShell works well, but I wanted launching OpenDesign to feel even simpler.

So I added CMD wrappers:

Start-OpenDesign.cmd
Stop-OpenDesign.cmd
Restart-OpenDesign.cmd

They simply call the corresponding PowerShell scripts.

After adding the script directory to the Windows PATH, I can launch from:

  • Cmder
  • CMD
  • PowerShell

using:

Start-OpenDesign
Stop-OpenDesign
Restart-OpenDesign

The same files can also be used for:

  • desktop shortcuts
  • Start menu shortcuts
  • pinned launcher entries

At that point OpenDesign behaves much more like a normal Windows application while the runtime remains completely inside Linux.


Why I did not create a Windows Service

A Windows Service was an obvious possibility.

I deliberately did not use one.

The runtime already has a service manager:

systemd

Adding a Windows Service would produce:

Windows Service
    ↓
WSL
    ↓
systemd
    ↓
OpenDesign

That is unnecessary duplication.

It would introduce:

  • another startup lifecycle
  • WSL boot timing concerns
  • user-session differences
  • more complex debugging
  • another service manager to maintain

The cleaner model is:

Windows launches. WSL manages.

If I later want OpenDesign to start automatically when I log into Windows, Windows Task Scheduler would be a more appropriate integration than a Windows Service.


Should WSL remain running?

While OpenDesign is running, yes.

That is expected.

The runtime is:

Windows
    ↓
WSL
    ↓
systemd
    ↓
OpenDesign

There is no need to keep an Ubuntu terminal window visible.

The distro remains alive because OpenDesign is running.

When OpenDesign and other WSL processes stop, WSL can shut down naturally.


What happens if Windows shuts down abruptly?

There is no special daemon cleanup required.

When Windows shuts down:

  • WSL stops
  • systemd stops
  • OpenDesign stops
  • agent processes stop
  • child processes disappear with the WSL VM

After the next Windows boot, I simply start OpenDesign again.

There is no stale Linux daemon surviving across a reboot.

The only general caveat is the same as with any application: if the machine loses power while a file is actively being written, that write could be interrupted.

But the process lifecycle itself remains clean.


Keeping the OpenDesign repository clean

This became one of the most important maintainability decisions.

I do not put operational customization inside:

~/tools/open-design

The repository remains upstream-clean.

My custom files live outside it.

Linux-side customization

~/bin
~/.config/systemd/user
~/.local/bin

Windows-side customization

C:\Users\<user>\tools\scripts\opendesign

That means:

git pull

can remain straightforward.

No launcher scripts need to be merged.

No systemd file lives inside the repository.

No Windows integration lives inside the repository.

No local patch exists unless I intentionally choose to create one.

This dramatically reduces future maintenance.


Updating OpenDesign

The future update workflow remains simple.

Conceptually:

  1. Stop OpenDesign.
  2. Enter the Git checkout.
  3. Pull the latest source.
  4. Refresh dependencies if required.
  5. Rebuild the web package.
  6. Restart OpenDesign.
systemctl --user stop open-design

cd ~/tools/open-design

git pull
pnpm install
pnpm --filter @open-design/web build

systemctl --user start open-design
systemctl --user status open-design --no-pager

The Windows launchers and systemd configuration stay untouched unless OpenDesign itself changes its startup contract.

That is exactly what I want.


Updating agents independently

OpenCode, Codex, Claude, Gemini, and Antigravity are independent tools.

They should remain independently managed.

That means:

  • OpenDesign updates do not silently change Codex
  • Codex updates do not alter OpenDesign
  • OpenCode can move at its own pace
  • each tool keeps its own authentication
  • model availability follows the local agent version
  • agent-specific issues can be isolated more easily

This separation already helped diagnose both Codex and OpenCode issues.


Local OpenCode versus BYOK OpenCode

OpenDesign exposes both:

OpenCode
BYOK OpenCode

They are conceptually different.

Local OpenCode means OpenDesign launches the OpenCode CLI already installed in WSL.

That CLI uses its own:

  • credentials
  • provider configuration
  • model configuration
  • local environment

BYOK means:

Bring Your Own Key

That execution path is intended for API-backed usage where the model/provider credentials are supplied separately.

For my environment, local OpenCode is the natural fit because OpenCode already belongs to the WSL runtime.

The current blocker is adapter compatibility, not architecture.


Model strategy

Once Codex was updated and working through OpenDesign, the next decision became model selection.

For an iterative design environment, the strongest available model is not necessarily the best default.

OpenDesign encourages repeated cycles of:

  • layout refinement
  • spacing
  • typography
  • responsive fixes
  • component changes
  • visual iteration
  • implementation adjustments

Using the most expensive model for every small iteration is wasteful.

A more sensible strategy is:

  • fast/value model for routine changes
  • stronger reasoning model for difficult implementation work
  • top-tier model only when the task justifies it

The general principle is:

Use capability proportional to task difficulty.


The final architecture

Final architecture showing Windows launchers, WSL OpenDesign runtime, and local AI agents
Windows 11
│
├── Start-OpenDesign.cmd
│       ↓
├── Start-OpenDesign.ps1
│       ↓
├── wsl.exe
│       ↓
│
└──────────────► Ubuntu 24.04 / WSL2
                 │
                 ├── systemctl --user start open-design
                 │       ↓
                 ├── open-design.service
                 │       ↓
                 ├── ~/bin/opendesign-run
                 │       ↓
                 ├── ~/.local/bin/od --no-open
                 │       ↓
                 └── OpenDesign :7456
                         │
                         ├── Codex
                         ├── OpenCode
                         ├── Claude
                         ├── Gemini
                         ├── Antigravity
                         ├── Node / pnpm
                         ├── Git
                         ├── credentials
                         └── project files

Windows Chrome
      │
      └──────────────► http://127.0.0.1:7456

The Windows browser is merely presenting the application.

The entire development runtime remains inside WSL.


What I would do differently if starting again

With hindsight, I would skip many of the intermediate experiments.

I would go directly to this sequence:

  1. Install Ubuntu on WSL2.
  2. Keep development projects inside the Linux filesystem.
  3. Install AI coding agents inside WSL.
  4. Install OpenDesign inside WSL.
  5. Resolve the Linux od naming collision immediately.
  6. Make NVM and Node versions deterministic.
  7. Keep Windows PATH out of Linux unless explicitly needed.
  8. Add a reliable WSL-to-Windows browser bridge.
  9. Build the OpenDesign web package.
  10. Run OpenDesign through od --no-open.
  11. Keep the stable 7456 endpoint.
  12. Manage OpenDesign through a systemd user service.
  13. Make the systemd launcher self-contained.
  14. Keep operational scripts outside the Git checkout.
  15. Use Windows PowerShell only as a frontend launcher.
  16. Use health checks instead of arbitrary sleeps.
  17. Validate each local agent independently.
  18. Keep agent authentication independent from OpenDesign.
  19. Keep agent upgrades independent from OpenDesign.
  20. Avoid a Windows Service unless there is a real system-level requirement.

That would avoid most of the detours.


This pattern is bigger than OpenDesign

The most useful lesson from this setup is that it is not really an OpenDesign-specific pattern.

The same architecture works for many developer tools that:

  • run best in Linux
  • expose a local browser UI
  • launch command-line agents
  • depend on Linux project files
  • need credentials and package managers
  • are used from a Windows desktop

The reusable pattern is:

Windows
│
├── browser
├── shortcuts
├── launchers
└── desktop UX
        │
        ▼
WSL
│
├── services
├── agents
├── runtimes
├── credentials
├── package managers
├── project files
└── development processes

The important boundary is between:

presentation

and:

execution

Once that boundary is explicit, WSL stops feeling like “a Linux terminal inside Windows.”

It becomes the actual development machine.

Windows becomes the interface to it.


Final result

The finished setup gives me exactly what I wanted.

OpenDesign feels like a Windows application.

But technically, it is not running on Windows.

It runs inside WSL alongside the rest of the development stack.

That means:

  • OpenDesign stays in Linux.
  • OpenCode stays in Linux.
  • Codex stays in Linux.
  • Claude stays in Linux.
  • Gemini stays in Linux.
  • Antigravity stays in Linux.
  • Node stays in Linux.
  • pnpm stays in Linux.
  • Git stays in Linux.
  • credentials stay in Linux.
  • project files stay in Linux.
  • systemd manages service lifecycle.
  • Windows handles Chrome and launchers.
  • one stable localhost endpoint exposes the UI.
  • no Ubuntu terminal needs to remain open.
  • no random ports need to be remembered.
  • shutdown naturally cleans up the runtime.
  • the OpenDesign repository remains clean.
  • updates remain manageable.
  • agents can evolve independently.

What began as an OpenDesign setup problem turned into a broader architecture decision.

I did not need to choose between Windows and Linux.

I needed to decide which responsibilities belonged to each.

Windows is excellent at being my desktop.

WSL is excellent at being my development machine.

Once those boundaries were explicit, the setup became much simpler.

One desktop. Two environments. One development workflow.

Windows handles the interface. WSL owns the runtime.


Work Behind The Writing

This article comes from real-world AI and DevOps engineering work.

If the thinking here is useful, explore the projects behind it or get in touch about a similar technical problem.
comments powered by Disqus