diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 5379fb865..5cd4f7145 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -84,3 +84,21 @@ jobs: - name: Build web run: bun run --cwd web build + + desktop: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Bun + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.11 + + - name: Install desktop dependencies + run: bun install --cwd packages/desktop --frozen-lockfile + + - name: Run desktop tests + run: bun run --cwd packages/desktop test diff --git a/docs/desktop-app-design.md b/docs/desktop-app-design.md new file mode 100644 index 000000000..350ff180e --- /dev/null +++ b/docs/desktop-app-design.md @@ -0,0 +1,972 @@ +# OpenClaude Desktop App Design + +**Date**: 2026-05-03 +**Status**: Draft +**Scope**: Full GUI desktop app with CLI parity, built on OpenClaude SDK + +## 1. Overview + +Build a professional Electron desktop application inside the OpenClaude monorepo (`packages/desktop/`) that provides full CLI parity — all OpenClaude features accessible through a modern GUI. The app is built on top of the OpenClaude SDK and follows a wave-based PR strategy for incremental delivery. + +### Key Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Runtime** | Electron 39 | **Stability & compatibility:** Mature ecosystem, battle-tested by VS Code, Slack, Discord. **Node.js native:** SDK runs directly in main process — no sidecar process needed for MCP servers, tool execution, file operations. **Tauri trade-offs:** Smaller binary (~10-50MB vs ~150MB) but requires sidecar for any Node.js work, plus Rust learning curve for team contributions. Open to discussion in PR thread if strong preference for Tauri emerges. | +| **Repo structure** | Same repo, `packages/desktop/` | **Direct SDK access:** Import from `@gitlawb/openclaude` workspace without publishing to npm first. **Shared CI:** Reuse existing test infrastructure, smoke tests, type checking. **Unified development:** One clone for CLI + desktop; contributors can work on both without separate repos. **Trade-off:** Larger repo, but manageable with workspaces and clear `packages/` boundaries. | +| Scope | Full GUI (CLI parity) | All CLI features in GUI form | +| Architecture | Feature-based monolith | Proven pattern, clean PR boundaries | +| IPC | tRPC | Type-safe, compile-time checks | +| State management | Jotai + Zustand | Jotai for atoms, Zustand for complex state | +| Database | SQLite + Drizzle ORM | Lightweight, embedded, zero config | +| Terminal | External terminal | Simpler, avoid xterm.js complexity | +| UI framework | Tailwind + Radix/shadcn | Modern, performant, customizable | +| Code editor | Monaco Editor | VS Code engine, full language support | +| Build system | electron-vite + electron-builder | Fast dev builds, multi-platform packaging | + +## 2. Electron Process Architecture + +``` +Renderer (React 19 + Tailwind + shadcn + Jotai/Zustand) + │ + │ tRPC Client (type-safe) + │ +Preload (contextBridge + tRPC proxy) + │ + │ IPC + │ +Main Process + ├── tRPC Server (routers per feature) + ├── SDK Host (OpenClaude SDK orchestration) + ├── Database (SQLite + Drizzle) + ├── Window Manager + ├── File System Access + └── Auto-updater + Protocol Handler +``` + +### Process Responsibilities + +**Main Process**: +- Hosts the OpenClaude SDK — renderer never accesses SDK directly +- Runs tRPC server with feature-based routers +- Manages SQLite database via Drizzle ORM +- Handles file system operations, window management +- Manages auto-updater and protocol handlers + +**Preload Script**: +- Minimal — only tRPC proxy via contextBridge +- Small attack surface +- No direct Node.js API exposure to renderer + +**Renderer Process**: +- Pure React application +- No Node.js API access — all I/O through tRPC +- Feature-based organization (chat, tools, editor, settings, projects, mcp, skills, stats) +- Jotai for atomic/reactive state, Zustand for complex stores + +## 3. Directory Structure + +``` +packages/desktop/ +├── package.json +├── electron.vite.config.ts +├── tsconfig.json +├── tailwind.config.ts +├── drizzle.config.ts +│ +├── src/ +│ ├── main/ +│ │ ├── index.ts # App entry, window management +│ │ ├── ipc/ +│ │ │ ├── trpc.ts # tRPC init + context +│ │ │ ├── routers/ +│ │ │ │ ├── chat.ts +│ │ │ │ ├── tools.ts +│ │ │ │ ├── settings.ts +│ │ │ │ ├── projects.ts +│ │ │ │ ├── mcp.ts +│ │ │ │ ├── skills.ts +│ │ │ │ └── stats.ts +│ │ │ └── index.ts # Router aggregation +│ │ ├── db/ +│ │ │ ├── schema/ # Drizzle schema definitions +│ │ │ ├── migrations/ +│ │ │ └── client.ts +│ │ ├── sdk/ +│ │ │ ├── host.ts # SDK lifecycle management +│ │ │ ├── permissions.ts # Permission mode enforcement + auto-approve logic +│ │ │ ├── pluginHost.ts # Plugin lifecycle, isolation, health monitoring +│ │ │ └── events.ts # SDK event → tRPC bridge +│ │ ├── services/ +│ │ │ ├── updater.ts +│ │ │ └── protocol.ts +│ │ └── utils/ +│ │ +│ ├── preload/ +│ │ └── index.ts # contextBridge + tRPC proxy +│ │ +│ ├── renderer/ +│ │ ├── index.html +│ │ ├── main.tsx +│ │ ├── App.tsx +│ │ ├── features/ +│ │ │ ├── chat/ +│ │ │ │ ├── components/ +│ │ │ │ │ ├── PermissionModeSelector.tsx # Mode dropdown in chat input +│ │ │ │ │ ├── PermissionDialog.tsx # Tool approval dialog +│ │ │ │ │ └── PermissionBadge.tsx # Current mode indicator +│ │ │ │ ├── hooks/ +│ │ │ │ └── store.ts +│ │ │ ├── tools/ +│ │ │ │ ├── components/ +│ │ │ │ └── store.ts +│ │ │ ├── editor/ +│ │ │ │ ├── components/ +│ │ │ │ └── hooks/ +│ │ │ ├── settings/ +│ │ │ │ ├── components/ +│ │ │ │ └── store.ts +│ │ │ ├── projects/ +│ │ │ │ ├── components/ +│ │ │ │ └── store.ts +│ │ │ └── mcp/ +│ │ │ ├── components/ +│ │ │ └── store.ts +│ │ │ ├── diff/ +│ │ │ │ ├── components/ +│ │ │ │ │ ├── DiffViewer.tsx # Unified + split diff views +│ │ │ │ │ ├── DiffHeader.tsx # File path, +/- counts, view toggle +│ │ │ │ │ ├── DiffNavigation.tsx # Next/prev change buttons +│ │ │ │ │ ├── InlineDiff.tsx # Inline diff in chat messages +│ │ │ │ │ └── DiffMinimap.tsx # Minimap overview for large diffs +│ │ │ │ ├── hooks/ +│ │ │ │ │ └── useDiff.ts # Diff computation + view mode +│ │ │ │ └── store.ts +│ │ │ ├── skills/ +│ │ │ │ ├── components/ +│ │ │ │ │ ├── SkillList.tsx # Slash command list + search +│ │ │ │ │ ├── SkillCard.tsx # Individual skill display +│ │ │ │ │ ├── SkillExecutor.tsx # Skill execution + args form +│ │ │ │ │ ├── PluginDashboard.tsx # Plugin list + status + health +│ │ │ │ │ ├── PluginCard.tsx # Single plugin: status, skills, agents, tools +│ │ │ │ │ └── PluginError.tsx # Error display with fix suggestions +│ │ │ │ ├── hooks/ +│ │ │ │ │ └── useSkillExecution.ts # Skill invocation logic +│ │ │ │ └── store.ts +│ │ │ └── stats/ +│ │ │ ├── components/ +│ │ │ │ ├── StatsOverview.tsx # Summary cards + activity heatmap +│ │ │ │ ├── StatsModels.tsx # Per-model usage charts +│ │ │ │ ├── ActivityHeatmap.tsx # GitHub-style contribution grid +│ │ │ │ ├── TokenChart.tsx # Token usage over time graph +│ │ │ │ ├── CostBreakdown.tsx # Cost per model pie/bar chart +│ │ │ │ └── SessionTimeline.tsx # Session activity timeline +│ │ │ ├── hooks/ +│ │ │ │ └── useStats.ts # Stats data fetching + date range +│ │ │ └── store.ts +│ │ ├── components/ +│ │ │ ├── ui/ # shadcn components +│ │ │ └── layout/ # App shell, sidebar +│ │ ├── stores/ +│ │ │ ├── theme.ts +│ │ │ └── app.ts +│ │ ├── lib/ +│ │ │ ├── trpc.ts # tRPC client setup +│ │ │ └── utils.ts +│ │ └── styles/ +│ │ └── globals.css +│ │ +│ └── shared/ +│ ├── types.ts +│ ├── trpc-routers.ts +│ └── constants.ts +│ +├── resources/ # App icons, binaries +├── scripts/ +└── tests/ + ├── main/ + ├── preload/ + └── renderer/ +``` + +## 4. Permission System + +The desktop app implements a 4-mode permission system. Unlike the CLI (which defaults to bypass), the desktop app defaults to the safest mode. + +### Permission Modes + +| Mode | Behavior | Use Case | +|------|----------|----------| +| **Ask permissions** (default) | Prompts user for every tool call — file reads, writes, bash commands, etc. | Maximum safety, full control | +| **Accept edits** | Auto-approves file read/write operations, prompts for bash commands and other risky operations | Balanced — trust file edits, verify commands | +| **Plan mode** | AI plans first, shows plan, asks for approval before executing any action | Review before execution | +| **Bypass permissions** | Skips all permission checks — equivalent to `--dangerously-skip-permissions` | Power users, trusted environments | + +### Permission Flow + +``` +SDK requests tool execution + │ + ┌────┴────┐ + │ SDK Host │ ← checks current permission mode + └────┬────┘ + │ + ┌─────┴─────────────────────────────┐ + │ │ +[Ask / Accept edits] [Plan mode] [Bypass] + │ │ │ + ├── Auto-allowed? │ │ + │ (Accept edits: file ops) │ ├── Execute + │ │ │ │ immediately + │ ├── Yes → Execute │ │ + │ └── No → Show dialog │ │ + │ │ │ │ + │ ┌──────┴──────┐ Show plan │ + │ │ │ │ │ + │ [Approve] [Reject] [Approve plan] │ + │ │ │ │ │ + │ Execute Cancel Execute plan │ + │ step by step │ + └──────────────────────────────┴─────────────────────────┘ +``` + +### Permission Mode Selector + +- Displayed in the chat input area as a dropdown/toggle +- Persisted per-session in database (default from global settings) +- Can be changed mid-conversation +- Bypass mode requires explicit confirmation dialog ("Are you sure?") +- Visual indicator shows current mode at all times (color-coded badge) + +### Permission Categories (for Accept Edits mode) + +| Category | Ask | Accept Edits | Plan | Bypass | +|----------|-----|-------------|------|--------| +| File read | prompt | auto | plan | auto | +| File write/create | prompt | auto | plan | auto | +| File delete | prompt | prompt | plan | auto | +| Bash command | prompt | prompt | plan | auto | +| Web search | prompt | auto | plan | auto | +| MCP tool call | prompt | prompt | plan | auto | + +## 5. SDK Integration Layer + +The main process wraps the OpenClaude SDK in a **SDK Host** layer that bridges SDK events to tRPC subscriptions. The SDK host enforces the permission system before executing any tool. + +``` +Renderer Main Process +──────── ──────────── +useChat() hook SDK Host Layer + → trpc.chat.sendMessage() ├── createSession(provider, model, permissionMode) + ← subscription: onStream ├── sendMessage(sessionId, content) + ← subscription: onToolCall ├── on('stream', chunk → tRPC.emit) + ← subscription: onPermissionReq ├── on('toolCall', → permission check) + → trpc.tools.approve() │ ├── auto-approve? → execute + → trpc.tools.reject() │ └── needs approval? → emit onPermissionReq + └── approveTool/rejectTool + Database Layer + ├── saveMessage() + ├── saveToolResult() + └── trackTokenUsage() +``` + +### tRPC Router Structure + +**chatRouter**: +- `sendMessage` — mutation, sends user message via SDK +- `onStream` — subscription, streams AI response chunks +- `onToolCall` — subscription, notifies tool execution requests +- `onPermissionRequest` — subscription, prompts user for tool approval (Ask/Accept Edits modes) +- `approveTool` / `rejectTool` — mutations, tool approval flow +- `setPermissionMode` — mutation, changes active permission mode for session +- `getHistory` — query, fetches chat history from DB +- `listSessions` — query, lists conversation sessions + +**toolsRouter**: +- `getToolResult` — query, fetches tool execution result +- `listTools` — query, lists available tools for current session + +**settingsRouter**: +- `getProviders` — query, lists configured providers +- `setApiKey` — mutation, saves API key (keytar/encrypted) +- `getModelProfiles` — query, lists custom model profiles +- `getPreferences` / `setPreferences` — preferences CRUD + +**projectsRouter**: +- `listProjects` — query, lists all persisted projects sorted by lastOpenedAt +- `openFolder` — mutation, opens native folder picker, validates path, saves to DB +- `openProject` — mutation, switches active project by id +- `getProject` — query, returns single project with git metadata +- `validateProject` — query, checks project path exists and is accessible +- `getGitStatus` — query, returns git status for project (branch, staged, unstaged) + +**mcpRouter**: +- `listServers` — query, lists configured MCP servers +- `addServer` / `removeServer` — mutations +- `getServerTools` — query, lists tools from specific MCP server +- `getServerStatus` — query, health check + +**skillsRouter**: +- `listSkills` — query, discovers and returns all available skills (file-based, plugin, bundled, MCP) +- `getSkillDetail` — query, returns skill metadata (description, args, frontmatter, source plugin) +- `executeSkill` — mutation, invokes a skill by name with arguments (passes through to SDK host) +- `onSkillOutput` — subscription, streams skill execution output +- `listPlugins` — query, lists installed plugins with status (loaded/error/loading/disabled), manifest data +- `getPluginDetail` — query, returns plugin's contributed skills, agents, tools, health status +- `installPlugin` / `uninstallPlugin` — mutations, plugin lifecycle with validation and rollback on failure +- `enablePlugin` / `disablePlugin` — mutations, toggle without uninstalling +- `onPluginStatusChange` — subscription, real-time plugin status updates (loading→loaded, loaded→error, etc.) +- `getPluginErrors` — query, returns recent plugin errors with stack traces and fix suggestions + +**statsRouter**: +- `getOverview` — query, returns session counts, token totals, active days, streaks, peak hours +- `getModels` — query, returns per-model usage breakdown (tokens, cost, request count) +- `getDailyActivity` — query, returns daily activity data for heatmap visualization +- `getTokenTrend` — query, returns time-series token usage for charts (7d/30d/all-time) +- `getCostBreakdown` — query, returns cost distribution by model/provider +- `getSessionStats` — query, returns current session real-time stats (tokens used, cost, duration) + +## 6. Database Schema (SQLite + Drizzle) + +### Tables + +**projects**: +- id, name, path, gitBranch, gitRemoteUrl, gitProvider, gitOwner, gitRepo, lastOpenedAt, createdAt + +**sessions**: +- id, projectId, title, provider, model, permissionMode (ask|accept_edits|plan|bypass), createdAt, updatedAt, **archivedAt** (soft delete — null = active) + +**messages_jsonl** (Hybrid Storage — SDK-compatible): +- id (INTEGER PK), sessionId, lineNumber, **content** (TEXT — raw SDK JSONL line, untouched), uuid, parentUuid, role (user|assistant|tool|system), createdAt, toolName (extracted metadata for indexing) +- **Why hybrid:** SDK uses JSONL + parentUuid tree structure. Relational tables break fork support + require conversion overhead. Hybrid preserves SDK format (content column = raw JSONL) + adds metadata columns for indexed queries. +- **Indexes:** sessionId, role, toolName, createdAt, uuid +- **SDK compatibility:** Load = read content → return JSONL array directly. Save = append JSONL + extract metadata. Zero conversion overhead. + +**settings**: +- key, value (JSON), updatedAt + +**mcpServers**: +- id, name, command, args (JSON), env (JSON), status (enum: stopped|running|error|starting), createdAt + +**providerKeys**: +- id, provider (UNIQUE), encryptedKey, createdAt (encrypted via OS keychain in PR4+) + +**plugins**: +- id, name, version, source (enum: marketplace|local|git), path, enabled, status (enum: loaded|error|loading|disabled), manifest (JSON), installedAt, updatedAt, lastError + +**NOTE:** Original design had separate `messages` + `toolCalls` tables (flat relational). Replaced with `messages_jsonl` hybrid approach after SDK compatibility analysis (2026-05-06). SDK's JSONL format + parentUuid tree structure cannot be flattened without breaking forkSession + adding conversion overhead. + +## 7. Wave-Based PR Plan + +### Wave 1: Foundation (4 PRs, parallel) + +**PR1: Electron Shell + Build System** (~1500 lines) +- Files: `package.json`, `electron.vite.config.ts`, `src/main/index.ts`, `src/preload/index.ts`, `scripts/`, `resources/` +- Content: electron-vite setup, main process entry (window creation, app lifecycle), preload script skeleton, build scripts +- Tests: Main process lifecycle tests, window creation tests + +**PR2: tRPC Infrastructure** (~1200 lines) +- Files: `src/main/ipc/trpc.ts`, `src/main/ipc/index.ts`, `src/shared/`, `src/preload/` (tRPC bridge), `src/renderer/lib/trpc.ts` +- Content: tRPC server setup, context, base router, preload tRPC proxy, renderer tRPC client, shared type definitions +- Tests: tRPC router unit tests, IPC integration tests + +**PR3: Database Layer** (~1500 lines) +- Files: `src/main/db/`, `drizzle.config.ts`, `scripts/db/` +- Content: Drizzle schema (all tables), SQLite client, migration system, seed data, query helpers +- Tests: Schema tests, migration tests, CRUD operation tests + +**PR4: Frontend Draft / UI Reference** (~2000 lines) +- Files: `src/renderer/` (skeleton), `tailwind.config.ts`, `src/renderer/components/ui/`, `src/renderer/styles/` +- Content: Vite renderer config, Tailwind setup, 10-15 shadcn base components, App shell layout, theme system, routing skeleton +- Tests: Component rendering tests, theme toggle tests +- **Note**: PR4 branch (`desktop/pr4-react-ui-shell`) serves as a visual reference and will not be merged as-is. Final UI implementation will be built per-wave. + +### Wave 2: Core Features — Phase A (2 PRs, parallel, requires Wave 1) + +PR5 (Chat UI) requires an active provider connection and API key to function. Therefore SDK Host and Settings must land first. + +**PR5: SDK Host Integration** (~2500 lines) +- Files: `src/main/sdk/`, `src/main/ipc/routers/chat.ts`, `src/main/ipc/routers/tools.ts` +- Content: SDK host (session lifecycle, provider management), event→tRPC bridge, streaming pipeline, tool approval flow, **permission mode enforcement** (`permissions.ts` — auto-approve logic per mode), error handling +- Tests: SDK host unit tests, session lifecycle tests, event bridge tests, **permission enforcement tests** + +**PR6: Settings UI + Provider Setup** (~1800 lines) +- Files: `src/renderer/features/settings/`, `src/main/ipc/routers/settings.ts` +- Content: Provider configuration forms, API key management (encrypted storage), model selection, preferences, theme toggle, settings store +- Tests: Settings form tests, provider config tests + +### Wave 2: Core Features — Phase B (2 PRs, parallel, requires Phase A) + +**PR7: Chat UI + Streaming + Projects** (~4000 lines) +- Files: `src/renderer/features/chat/`, `src/renderer/features/projects/`, `src/main/ipc/routers/chat.ts`, `src/main/ipc/routers/projects.ts` +- Content: Message list component, input area with file attachment, streaming message display, markdown rendering, code block rendering, **PermissionModeSelector** (dropdown in chat input), **PermissionDialog** (tool approval modal), **PermissionBadge** (mode indicator), **ProjectSelector** (folder picker + recent projects), **NewChatForm** (project selection flow), chat store, project store +- Tests: Message rendering tests, streaming mock tests, input component tests, permission mode selector tests, approval dialog tests, project selection tests + +#### PR5 Detail: Chat Input — Rich Text & Mentions + +**Custom textarea** (not TipTap/Lexical — keep simple): +- Plain `