DEV Community

Rishabh Kharyal
Rishabh Kharyal

Posted on

I Built a Desktop GUI for Claude Code in One Day — Here's How

I Built a Desktop GUI for Claude Code in One Day — Here's How

Claude Code is amazing. But I got tired of the terminal.

So I built this:

Claude Cowork

TL;DR:

  • 🚀 Run 3 AI tasks in parallel
  • 📝 See live diffs before files change
  • 💾 115MB portable — works offline
  • 🖥️ Windows + Mac + Linux from one codebase

⬇️ Download | 📦 GitHub


💡 What I Learned (The Short Version)

  1. Electron isn't dead — shipped to 3 platforms in 6 minutes
  2. Zustand > Redux — for apps this size, simplicity wins
  3. Ship broken, fix fast — users find bugs faster than you

Want the technical deep-dive? Keep reading. Want to try it? Download here.


🤔 The Problem: Terminal-Only AI

If you've used Claude Code, you know it's incredibly capable. It can read your codebase, write files, run commands, and reason about complex problems.

But it runs only in the terminal.

For quick tasks, that's fine. But when you're:

  • Running multiple AI tasks simultaneously
  • Reviewing file changes before approving them
  • Context-switching between different projects
  • Showing work to non-technical stakeholders

...the terminal becomes a bottleneck.

I wanted something that felt more like pair programming with a colleague than typing into a black box.


🛠️ The Solution: Claude Cowork

Claude Cowork Screenshot

Claude Cowork is a desktop application that wraps the official Claude Agent SDK with:

Feature What It Does
Parallel Task Queue Run up to 3 AI tasks simultaneously
Live Diff Visualization See file changes before they happen
Session Management Persist conversations in SQLite
Plugin System Auto-discover MCP servers and skills
Cross-Platform Single codebase → Win/Mac/Linux installers

Let me walk you through how I built each piece.


🏗️ Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                    Electron Main Process                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │ IPC Handler │  │ Session DB  │  │ Settings Manager│  │
│  │             │  │  (SQLite)   │  │  (MCP + Skills) │  │
│  └──────┬──────┘  └──────┬──────┘  └────────┬────────┘  │
│         │                │                   │           │
│         └────────────────┼───────────────────┘           │
│                          │                               │
│              ┌───────────▼───────────┐                   │
│              │  Claude Agent SDK     │                   │
│              │  (Task Runner)        │                   │
│              └───────────────────────┘                   │
└─────────────────────────────────────────────────────────┘
                           │
                           │ IPC Bridge
                           ▼
┌─────────────────────────────────────────────────────────┐
│                   React 19 Frontend                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐  │
│  │ Sidebar  │  │ Chat View│  │ Progress │  │Settings │  │
│  │          │  │ + Diffs  │  │  Panel   │  │  Modal  │  │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘  │
│                                                          │
│                    Zustand Store                         │
└─────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Tech Stack:

  • Electron 39 — Cross-platform desktop framework
  • React 19 — UI with latest features (Suspense, transitions)
  • Zustand — Lightweight state management
  • better-sqlite3 — Session persistence
  • @anthropic-ai/claude-agent-sdk — The AI brain

⚡ Deep Dive #1: Parallel Task Queue

The killer feature. Most AI coding tools process one request at a time. Claude Cowork can handle three.

The Problem

Claude Agent SDK runs tasks synchronously. If you start a task, you wait until it completes.

The Solution

I implemented a task queue manager that:

  1. Accepts unlimited tasks into a queue
  2. Runs up to MAX_CONCURRENT_TASKS (3) simultaneously
  3. Broadcasts status updates via IPC
// src/electron/ipc-handlers.ts

const MAX_CONCURRENT_TASKS = 3;
const taskQueue: QueuedTask[] = [];
const runningTasks = new Map<string, { task: QueuedTask; sessionId: string }>();

async function processTaskQueue() {
  while (runningTasks.size < MAX_CONCURRENT_TASKS) {
    const nextTask = taskQueue.find(t => t.status === "queued");
    if (!nextTask) break;

    // Start task in background
    nextTask.status = "running";
    nextTask.startedAt = Date.now();

    const session = await createSession(nextTask.cwd);
    runningTasks.set(nextTask.id, { task: nextTask, sessionId: session.id });

    // Run Claude (non-blocking)
    runClaude({
      sessionId: session.id,
      prompt: nextTask.prompt,
      cwd: nextTask.cwd,
      onEvent: (event) => broadcast(event)
    }).then(() => {
      completeTask(nextTask.id);
      processTaskQueue(); // Check for more tasks
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The UI

Users see a collapsible panel showing:

  • 🟢 Running tasks (with stop button)
  • 🟡 Queued tasks (with cancel button)
  • Toast notifications when tasks complete
// TaskQueuePanel.tsx
<div className="task-queue-panel">
  {runningTasks.map(task => (
    <div key={task.id} className="task-item running">
      <span className="pulse-dot" />
      <span>{task.prompt.slice(0, 40)}...</span>
      <button onClick={() => cancelTask(task.id)}>Stop</button>
    </div>
  ))}
</div>
Enter fullscreen mode Exit fullscreen mode

Result: Queue "write tests for auth module", "update README", and "fix TypeScript errors" — they all run in parallel.


📝 Deep Dive #2: Live Diff Visualization

When Claude edits a file, you should see exactly what changed before it happens.

The Implementation

I intercept Edit and Write tool calls and render inline diffs:

// EventCard.tsx

function DiffView({ oldContent, newContent, filePath }: DiffProps) {
  const changes = diffLines(oldContent, newContent);

  return (
    <div className="diff-container">
      <div className="diff-header">
        <span className="file-icon">📄</span>
        <span className="file-path">{filePath}</span>
      </div>
      <pre className="diff-content">
        {changes.map((change, i) => (
          <span
            key={i}
            className={
              change.added ? 'diff-added' :
              change.removed ? 'diff-removed' :
              'diff-unchanged'
            }
          >
            {change.value}
          </span>
        ))}
      </pre>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Before vs After

Terminal (Claude Code):

Editing src/utils.ts...
Done.
Enter fullscreen mode Exit fullscreen mode

Claude Cowork:

// src/utils.ts
  export function formatDate(date: Date) {
-   return date.toString();
+   return date.toISOString().split('T')[0];
  }
Enter fullscreen mode Exit fullscreen mode

You see the change, approve it, and move on. No guessing.


🔌 Deep Dive #3: Plugin System (MCP + Skills)

Claude Cowork auto-discovers capabilities from two sources:

1. MCP Servers

Model Context Protocol servers extend Claude's abilities. I built a settings manager that:

// settings-manager.ts

const BUILT_IN_MCP_SERVERS: MCPServer[] = [
  {
    id: "filesystem",
    name: "Filesystem",
    description: "Read and write files",
    enabled: true,
    builtIn: true,
    config: { command: "npx", args: ["-y", "@anthropic-ai/mcp-filesystem"] }
  },
  {
    id: "fetch",
    name: "Web Fetch",
    description: "Fetch URLs and web content",
    enabled: false,
    builtIn: true,
    config: { command: "npx", args: ["-y", "@anthropic-ai/mcp-fetch"] }
  }
];

// Also loads external servers from ~/.claude/settings.json
Enter fullscreen mode Exit fullscreen mode

2. Skills Auto-Discovery

Skills are markdown files with instructions. Claude Cowork scans ~/.claude/skills/ and presents them in the settings modal:

function discoverSkills(): SkillInfo[] {
  const skillsDir = join(homedir(), ".claude", "skills");
  const skills: SkillInfo[] = [];

  for (const folder of readdirSync(skillsDir)) {
    const skillPath = join(skillsDir, folder, "SKILL.md");
    if (existsSync(skillPath)) {
      const content = readFileSync(skillPath, "utf-8");
      const { name, description, triggers } = parseSkillFrontmatter(content);
      skills.push({ id: folder, name, description, triggers, enabled: true });
    }
  }

  return skills;
}
Enter fullscreen mode Exit fullscreen mode

Result: Install any Claude Code skill → it automatically appears in Claude Cowork.


🚀 Deep Dive #4: Cross-Platform CI/CD

One codebase should produce installers for all platforms. Here's my GitHub Actions workflow:

# .github/workflows/build.yml

name: Build & Release

on:
  push:
    tags: ['v*']

jobs:
  build:
    strategy:
      matrix:
        include:
          - os: windows-latest
            platform: win
          - os: macos-latest
            platform: mac
          - os: ubuntu-latest
            platform: linux

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci
      - run: npm run dist:${{ matrix.platform }}

      - uses: actions/upload-artifact@v4
        with:
          name: claude-cowork-${{ matrix.platform }}
          path: |
            release/*.exe
            release/*.dmg
            release/*.AppImage
            release/*.deb
Enter fullscreen mode Exit fullscreen mode

Push a tag → Get installers for 3 platforms in ~6 minutes.

Output Sizes

Platform Format Size
Windows NSIS Installer 129 MB
Windows Portable EXE 115 MB
macOS DMG 147 MB
Linux AppImage 131 MB
Linux .deb 89 MB

🐛 Challenges & Solutions

Challenge 1: Native Modules on Windows

better-sqlite3 requires native compilation. electron-builder handles this, but I hit symlink permission errors on Windows.

Solution: Add signAndEditExecutable: false to skip code signing during development.

Challenge 2: State Sync Between Processes

Electron has two processes (main + renderer). They need to stay in sync.

Solution: Zustand store in renderer + IPC event broadcasting from main:

// Main process
function broadcast(event: ServerEvent) {
  BrowserWindow.getAllWindows().forEach(win => {
    win.webContents.send("server-event", event);
  });
}

// Renderer process
useEffect(() => {
  window.electron.onServerEvent((event) => {
    handleServerEvent(event); // Updates Zustand store
  });
}, []);
Enter fullscreen mode Exit fullscreen mode

Challenge 3: Stopping Running Tasks

Claude Agent SDK doesn't expose a clean abort mechanism.

Solution: AbortController pattern:

export async function runClaude(options: RunOptions): Promise<RunnerHandle> {
  const abortController = new AbortController();

  // Pass signal to SDK (if supported) or handle manually
  const promise = claude.run({
    ...options,
    signal: abortController.signal
  });

  return {
    promise,
    abort: () => abortController.abort()
  };
}
Enter fullscreen mode Exit fullscreen mode

🎓 What I Learned

  1. Electron is still viable — Despite the "Electron bad" memes, it ships fast and works everywhere.

  2. React 19 is nice — Suspense boundaries and transitions make async UI smoother.

  3. Zustand > Redux — For apps this size, Zustand's simplicity wins.

  4. CI/CD is table stakes — Automated builds save hours and catch platform-specific bugs early.

  5. Ship early — The first version had bugs. Users found them faster than I would have.


🔮 What's Next

  • [ ] GUI settings for API keys — Remove dependency on Claude Code CLI
  • [ ] Multi-agent orchestration — Spawn specialized agents for different tasks
  • [ ] Git integration — Auto-commit checkpoints during long tasks
  • [ ] Project memory — Remember context across sessions

🎮 Try It Yourself

Download: GitHub Releases

Requirements:

  • Claude Code installed and authenticated
  • Node.js 18+ (for development)

Run from source:

git clone https://github.com/Skills03/Claude-Cowork.git
cd Claude-Cowork
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

💭 Final Thoughts

Claude Code changed how I write software. Claude Cowork makes that experience visual, parallel, and shareable.

If you're building AI-powered developer tools, the playbook is:

  1. Wrap existing SDKs (don't reinvent)
  2. Add the UX layer users actually need
  3. Ship cross-platform from day one
  4. Open source everything

The best tools feel like extensions of your brain. That's what I'm building toward.


Thanks for reading! If you found this useful:

  • Star the repo on GitHub
  • Follow me on LinkedIn for more builds
  • DM me if you want to collaborate

Built with Claude Code + Claude Cowork (yes, recursively) 🔄

Top comments (0)