diff --git a/.opencode/skills/codenomad-architecture-guide/SKILL.md b/.opencode/skills/codenomad-architecture-guide/SKILL.md new file mode 100644 index 00000000..0066a61f --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/SKILL.md @@ -0,0 +1,153 @@ +--- +name: codenomad-architecture-guide +description: | + Comprehensive architecture and SDK navigation guide for the CodeNomad codebase. + + **When to use:** Load this skill when you need to navigate the CodeNomad monorepo, understand cross-package dependencies, work with the OpenCode SDK V2, or ensure you don't miss related code when implementing features or fixing bugs. This skill covers the 6 functional areas (ServerBackend, UserInterface, DesktopClient, SpeechAndAudio, BuildAndPackaging, CloudflareDeployment), OpenCode SDK V2 integration patterns, critical schema behaviors, and feature traces with decision branches. + + **Trigger contexts:** Working on CodeNomad features, debugging cross-area issues, integrating OpenCode SDK APIs, adding UI components, implementing server routes, or navigating the monorepo structure. + + **Permission required:** Agent must explicitly request or be granted permission to load this skill. +--- + +# CodeNomad Architecture & SDK Navigation Skill + +## Quick Start (by contribution frequency) + +- **UI component/feature (60%)** → Read `references/ui-conventions.md` → Check i18n +- **Server route/feature (25%)** → Read `references/server-conventions.md` → Check `references/feature-traces.md` +- **Bug fix (10%)** → Use Navigation Guide below → Check `references/feature-traces.md` +- **Desktop/Plugin (5%)** → Read `references/desktop-conventions.md` +- **Not covered?** → See "Escape Hatch" at bottom + +## 1. Architecture Overview + +CodeNomad is a multi-platform desktop application with a Fastify backend and SolidJS frontend. + +### 6 Functional Areas (from RPG analysis) + +| Area | Entities | Key Responsibility | +|------|----------|-------------------| +| **UserInterface** | 613 | SolidJS components, stores, hooks, i18n, API client | +| **ServerBackend** | 418 | Fastify routes, auth, workspaces, filesystem, speech | +| **SpeechAndAudio** | 74 | Speech synthesis, voice mode, conversation mode | +| **DesktopClient** | 59 | Electron main, Tauri Rust, preload, IPC | +| **BuildAndPackaging** | 28 | Build scripts, packaging, resource bundling | +| **CloudflareDeployment** | 3 | Edge deployment, asset serving | + +### Package Map + +- `packages/server/` — Fastify backend, workspaces, auth, speech, sidecars +- `packages/ui/` — SolidJS frontend, stores, components, i18n +- `packages/electron-app/` — Electron desktop wrapper +- `packages/tauri-app/` — Tauri desktop wrapper (Rust + webview) +- `packages/opencode-plugin/` — OpenCode plugin integration + +### Key Entry Points + +- **Server:** `packages/server/src/index.ts` (CLI entry) +- **UI:** `packages/ui/src/main.tsx` (app bootstrap) +- **Electron:** `packages/electron-app/electron/main/main.ts` +- **Tauri:** `packages/tauri-app/src-tauri/src/main.rs` + +## 2. Navigation Guide + +### Finding Code in the Codebase + +Use grep and file search tools to navigate: + +**Search by intent:** +- `grep "permission approval" packages/ui/src/components/` +- `grep "session list" packages/ui/src/stores/` +- `grep "workspace create" packages/server/src/server/routes/` + +**Search by imports:** +- Find what uses a module: `grep "import.*from.*module-path" packages/` +- Find exports: `grep "^export" packages/server/src/api-types.ts` + +**Cross-reference by feature:** +- Server API types: `packages/server/src/api-types.ts` +- UI type mirrors: `packages/ui/src/types/` +- SDK wrappers: `packages/ui/src/lib/sdk-manager.ts` + +## 3. SDK Schema Verification (Mandatory) + +**SDK Note:** The OpenCode SDK is an external package (`@opencode-ai/sdk/v2/client`). Its implementation lives outside this repository. + +- After `npm install`, you can inspect types in `node_modules/@opencode-ai/sdk/v2/client.d.ts` +- **Fallback:** Read the actual usage patterns in CodeNomad code (see `references/sdk-api-reference.md` for file locations) +- When in doubt, check how the SDK is imported and used in existing CodeNomad files + +This skill provides navigation and patterns, not definitive schemas. + +## 4. Anti-Patterns + +### Common Mistakes + +| Mistake | Correct Approach | Reference | +|---------|-----------------|-----------| +| Import `enMessages` directly | Use `t()` or `tGlobal()` | `packages/ui/src/lib/i18n/index.tsx` | +| Set `metadata: { flag: true }` on assistant parts | Use client-side registry | `packages/ui/src/stores/session-compaction.ts` | +| Call `client.session.*` directly without worktree routing | Use `getOrCreateWorktreeClient()` | `packages/ui/src/stores/worktrees.ts` | +| Forget SSE disconnection handling | Add handlers | `packages/ui/src/lib/event-source-handlers.ts` | +| Add hardcoded strings without i18n | Add to English + all 7 locales | `packages/ui/src/lib/i18n/messages/` | +| Modify server route without checking UI API client | Trace full feature flow | `references/feature-traces.md` | +| Change API type without checking UI type matches | Check UI types mirror server types | `packages/ui/src/types/` vs `packages/server/src/api-types.ts` | + +## 5. Platform Integration Checklist + +### Desktop Platform Rules + +- **Existing IPC/handlers (pre-Tauri):** MUST implement in both Electron + Tauri +- **New features:** Implement in Electron first, Tauri if time permits +- **Native APIs (dialogs, notifications):** Use `packages/ui/src/lib/native/` abstraction + +### Checklist + +- [ ] Electron main-process changes? (`packages/electron-app/electron/main/`) +- [ ] Tauri Rust changes? (`packages/tauri-app/src-tauri/src/`) +- [ ] Preload API exposure? (`packages/electron-app/electron/preload/`) +- [ ] Native abstraction? (`packages/ui/src/lib/native/`) + +## 6. Implementation Checklist + +Before submitting changes: + +- [ ] Run impact analysis: `grep "YOUR_EXPORT_NAME" packages/` to find all usages +- [ ] Check i18n: Search for hardcoded strings in modified files +- [ ] Verify file length: Check line count (warn >500, reject >800 source; >1000 tests) +- [ ] Check DesktopClient: Does this need IPC/main-process changes? +- [ ] Verify SDK compatibility: Check types in `node_modules/@opencode-ai/sdk/v2/client.d.ts` +- [ ] Cross-area check: If modifying server routes, check UI stores and API clients +- [ ] Check anti-patterns: Review "Common Mistakes" section above +- [ ] API compatibility: If changing `api-types.ts`, check UI type matches + +## 7. Escape Hatch + Update Criteria + +### Not Covered? + +If your change involves areas not documented here: + +1. Read package entry points and scan directory structure +2. Ask the user before proceeding with unfamiliar code + +### Update This Skill If + +- You discover a new SDK gotcha not documented in `references/sdk-critical-behaviors.md` +- You add a new cross-area feature flow (add to `references/feature-traces.md`) +- File paths or conventions change significantly +- You find an anti-pattern occurring repeatedly +- SDK schemas change and examples become outdated + +## Reference Files + +| File | Purpose | +|------|---------| +| `references/architecture-overview.md` | Package structure, functional areas, entry points | +| `references/ui-conventions.md` | SolidJS, i18n, stores, components, testing | +| `references/server-conventions.md` | Fastify, API types, config, testing | +| `references/desktop-conventions.md` | Electron + Tauri parity, native abstractions | +| `references/sdk-api-reference.md` | OpenCode SDK V2 categories and signatures | +| `references/sdk-critical-behaviors.md` | Schema gotchas, limitations, decision matrix | +| `references/sdk-integration-patterns.md` | Client lifecycle, error handling, optimistic updates | +| `references/feature-traces.md` | End-to-end flows with decision branches | diff --git a/.opencode/skills/codenomad-architecture-guide/references/architecture-overview.md b/.opencode/skills/codenomad-architecture-guide/references/architecture-overview.md new file mode 100644 index 00000000..1b4acf78 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/architecture-overview.md @@ -0,0 +1,76 @@ +# Architecture Overview + +## Package Structure + +| Package | Purpose | Key Subdirectories | +|---------|---------|-------------------| +| `packages/server/` | Fastify backend | `src/server/routes/`, `src/workspaces/`, `src/auth/`, `src/speech/` | +| `packages/ui/` | SolidJS frontend | `src/components/`, `src/stores/`, `src/lib/`, `src/types/` | +| `packages/electron-app/` | Electron desktop wrapper | `electron/main/`, `electron/preload/`, `electron/resources/` | +| `packages/tauri-app/` | Tauri desktop wrapper | `src-tauri/src/`, `src-tauri/capabilities/` | +| `packages/opencode-plugin/` | OpenCode plugin integration | `plugin/lib/`, `plugin/codenomad.ts` | +| `packages/cloudflare/` | Edge deployment | `src/`, `scripts/` | + +## Functional Areas (from RPG) + +### UserInterface (613 entities) +- **Components:** JSX components in `packages/ui/src/components/` +- **Stores:** Signal-based state in `packages/ui/src/stores/` +- **Hooks:** Reusable logic in `packages/ui/src/lib/hooks/` +- **i18n:** 7-locale translation system in `packages/ui/src/lib/i18n/` +- **API Client:** SDK wrapper in `packages/ui/src/lib/sdk-manager.ts` + +### ServerBackend (418 entities) +- **API Routes:** Fastify route handlers in `packages/server/src/server/routes/` +- **Authentication:** Auth manager, session manager, token manager in `packages/server/src/auth/` +- **Background Processes:** Process spawn and management in `packages/server/src/background-processes/` +- **Configuration:** YAML-based settings in `packages/server/src/settings/` +- **Filesystem:** Restricted file browser in `packages/server/src/filesystem/` +- **Workspaces:** Git worktrees, runtime management in `packages/server/src/workspaces/` + +### SpeechAndAudio (74 entities) +- **Speech Synthesis:** OpenAI-compatible provider in `packages/server/src/speech/` +- **Voice Mode:** Real-time voice state management in `packages/server/src/plugins/voice-mode.ts` +- **Conversation Mode:** Client-side speech queue in `packages/ui/src/stores/conversation-speech.ts` + +### DesktopClient (59 entities) +- **Electron Main:** Process manager, menu, IPC in `packages/electron-app/electron/main/` +- **Tauri Rust:** CLI manager, certificate management in `packages/tauri-app/src-tauri/src/` +- **Preload:** API exposure layer in `packages/electron-app/electron/preload/` + +### BuildAndPackaging (28 entities) +- **Build Scripts:** Version sync, icon generation, resource copying +- **Packaging:** Server resource bundling, node runtime preparation + +### CloudflareDeployment (3 entities) +- **Edge Functions:** Asset serving with cache headers in `packages/cloudflare/src/index.ts` + +## Key Entry Points + +| Entry Point | File | Purpose | +|-------------|------|---------| +| Server CLI | `packages/server/src/index.ts` | Parses CLI options, starts HTTP server | +| UI Bootstrap | `packages/ui/src/main.tsx` | Initializes SolidJS app, mounts to DOM | +| Electron Main | `packages/electron-app/electron/main/main.ts` | Creates window, starts CLI process | +| Tauri Main | `packages/tauri-app/src-tauri/src/main.rs` | Rust entry, sets up window and CLI | +| Plugin Entry | `packages/opencode-plugin/plugin/codenomad.ts` | Initializes CodeNomad plugin tools | + +## Inter-Area Dependencies + +``` +UserInterface → ServerBackend (via SDK HTTP calls) +UserInterface → SpeechAndAudio (via conversation-speech store) +DesktopClient → UserInterface (hosts the UI in a native window) +DesktopClient → ServerBackend (spawns and manages server process) +ServerBackend → SpeechAndAudio (delegates to speech providers) +ServerBackend → CloudflareDeployment (fetches remote assets) +``` + +## Finding Code by Area + +| Area | Directory Patterns | Search Command | +|------|------------------|----------------| +| UserInterface | `packages/ui/src/components/`, `packages/ui/src/stores/` | `grep "query" packages/ui/src/` | +| ServerBackend | `packages/server/src/server/routes/`, `packages/server/src/workspaces/` | `grep "query" packages/server/src/` | +| DesktopClient | `packages/electron-app/electron/main/`, `packages/tauri-app/src-tauri/src/` | `grep "query" packages/*-app/` | +| SpeechAndAudio | `packages/server/src/speech/`, `packages/ui/src/stores/conversation-speech.ts` | `grep "query" packages/**/speech*` | diff --git a/.opencode/skills/codenomad-architecture-guide/references/desktop-conventions.md b/.opencode/skills/codenomad-architecture-guide/references/desktop-conventions.md new file mode 100644 index 00000000..4ba27754 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/desktop-conventions.md @@ -0,0 +1,136 @@ +# Desktop Conventions + +## Dual Platform: Electron + Tauri + +CodeNomad supports two desktop platforms: +- **Electron** (primary, mature) +- **Tauri** (emerging, Rust-based) + +## Electron + +### Directory Structure + +``` +packages/electron-app/electron/ +├── main/ # Main process code +│ ├── main.ts # Entry point, window management +│ ├── menu.ts # Application menu +│ ├── ipc.ts # IPC handlers +│ ├── storage.ts # File system storage +│ ├── permissions.ts # Media permissions +│ ├── user-shell.ts # Shell command execution +│ └── process-manager.ts # CLI process management +├── preload/ # Preload scripts +│ └── index.cjs # API exposure to renderer +└── resources/ # Bundled resources + └── cli-supervisor.cjs # Process supervisor +``` + +### Main Process Responsibilities + +- Create and manage browser windows +- Spawn and monitor CLI server process +- Handle native APIs (file dialogs, notifications) +- Manage application lifecycle + +### IPC Pattern + +```typescript +// Main process handler +// packages/electron-app/electron/main/ipc.ts +function setupCliIPC() { + ipcMain.handle("dialog:open", async (_, options) => { + return dialog.showOpenDialog(options) + }) +} + +// Preload exposure +// packages/electron-app/electron/preload/index.cjs +contextBridge.exposeInMainWorld("electronAPI", { + openDialog: (options) => ipcRenderer.invoke("dialog:open", options) +}) +``` + +## Tauri + +### Directory Structure + +``` +packages/tauri-app/ +├── src-tauri/ +│ ├── src/ # Rust backend code +│ │ ├── main.rs # Entry point +│ │ ├── cli_manager.rs # CLI process management +│ │ ├── cert_manager.rs # TLS certificate management +│ │ └── linux_tls.rs # Linux TLS handling +│ └── capabilities/ # Permission capabilities +└── src/ # Frontend code (same as UI) +``` + +### Rust Backend + +- Commands exposed to frontend via `#[tauri::command]` +- Process management similar to Electron's process-manager.ts +- Certificate management for HTTPS + +### Command Pattern + +```rust +// packages/tauri-app/src-tauri/src/main.rs +#[tauri::command] +fn open_dialog(options: DialogOptions) -> Result { + // Implementation +} +``` + +## Parity Rules + +| Scenario | Rule | +|----------|------| +| Existing IPC/handlers (pre-Tauri) | MUST implement in both Electron + Tauri | +| New features | Implement in Electron first, Tauri if time permits | +| Native APIs | Use `packages/ui/src/lib/native/` abstraction layer | + +## Native Abstractions + +CodeNomad abstracts native APIs to work across Electron, Tauri, and Web: + +| Feature | Abstraction File | +|---------|-----------------| +| File dialogs | `packages/ui/src/lib/native/native-functions.ts` | +| Desktop file drop | `packages/ui/src/lib/native/desktop-file-drop.ts` | +| Electron-specific | `packages/ui/src/lib/native/electron/functions.ts` | +| Wake lock | `packages/ui/src/lib/native/wake-lock.ts` | +| Remote windows | `packages/ui/src/lib/native/remote-window.ts` | +| CLI restart | `packages/ui/src/lib/native/cli.ts` | + +### Abstraction Pattern + +```typescript +// packages/ui/src/lib/native/native-functions.ts +export type NativeDialogResult = string | string[] | null + +export async function openNativeFileDialogs( + options?: Omit +): Promise { + const result = await openNativeDialog({ mode: "file", multiple: true, ...options }) + // Platform-specific implementation +} +``` + +## Platform Detection + +```typescript +// packages/ui/src/lib/runtime-env.ts +export function isElectronHost(): boolean { /* ... */ } +export function isTauriHost(): boolean { /* ... */ } +export function isWebHost(): boolean { /* ... */ } +``` + +## Checklist for Desktop Features + +- [ ] Electron main-process changes? (`packages/electron-app/electron/main/`) +- [ ] Tauri Rust changes? (`packages/tauri-app/src-tauri/src/`) +- [ ] Preload API exposure? (`packages/electron-app/electron/preload/`) +- [ ] Native abstraction? (`packages/ui/src/lib/native/`) +- [ ] Cross-platform test (Electron + Tauri + Web) diff --git a/.opencode/skills/codenomad-architecture-guide/references/feature-traces.md b/.opencode/skills/codenomad-architecture-guide/references/feature-traces.md new file mode 100644 index 00000000..286d32d8 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/feature-traces.md @@ -0,0 +1,181 @@ +# Feature Traces + +End-to-end feature flows with decision branches and mechanism references. + +## Permission Flow (with branches) + +1. **Server:** Backend emits SSE event `permission.asked` or `permission.updated` + - Events are pushed through the instance event stream + +2. **UI Store:** `packages/ui/src/stores/instances.ts` receives via `serverEvents` handler + - **Branch:** IF `isPermissionAutoAcceptEnabled(instanceId, sessionId)` is true + - **Mechanism:** `drainAutoAcceptPermissions()` in `packages/ui/src/stores/permission-auto-accept.ts` + - **Action:** Automatically calls `Permission.reply()`, skips modal display + - **File:** `packages/ui/src/stores/permission-auto-accept.ts:drainAutoAcceptPermission()` + - **Branch:** ELSE (normal flow) + - **Mechanism:** Permission queued in `permissionQueues` signal + - **Action:** Display approval modal + - **File:** `packages/ui/src/components/permission-approval-modal.tsx` + +3. **UI Store:** `packages/ui/src/stores/message-v2/bridge.ts` calls `upsertPermissionV2()` + - Adds permission to message store for display in chat + +4. **UI Component:** Modal displays (if not auto-accepted) + - Shows permission details and allow/deny/once buttons + +5. **User Action:** Calls `packages/ui/src/stores/instances.ts:sendPermissionResponse()` + - Validates permission still pending + - Prepares reply payload + +6. **SDK Call:** `client.permission.reply()` via `packages/ui/src/lib/opencode-api.ts` + - Wrapped with `requestData()` for error handling + +7. **Optimistic Update:** `removePermissionV2()` in bridge + - Immediately removes from local store + - UI updates without waiting for server + +8. **SSE Confirmation:** Server emits `permission.replied` event + - **Branch:** IF SSE is connected + - Bridge reconciles (no-op if already removed optimistically) + - **Branch:** IF SSE is disconnected during reply + - **Mechanism:** `serverEvents` reconnection triggers `syncPendingPermissions()` in `packages/ui/src/stores/instances.ts` + - **Action:** Re-fetches pending permissions, reconciles state + - If permission was already replied, it disappears from queue + +--- + +## Session Lifecycle (with branches) + +1. **UI:** `packages/ui/src/stores/session-api.ts:fetchSessions()` calls `client.session.list()` + - Uses root worktree client (no worktree slug needed for listing) + +2. **Server:** Backend returns session array via API response + - Includes status, title, parentID, version + +3. **UI:** Normalizes with `toClientSession()` → stores in `session-state.ts` + - Maps SDK types to UI types + - Preserves existing local state (title, model, status) + - **Branch:** IF session has `parentID` set + - **Mechanism:** Child session, no additional fetch + - **Branch:** IF session has no `parentID` and is expanded + - **Mechanism:** `fetchSessionChildren()` called recursively + - **File:** `packages/ui/src/stores/session-api.ts` + +4. **SSE:** Server pushes updates via instance event stream + - **Branch:** IF `message.part.delta` event + - **Mechanism:** Incremental text update streamed to UI + - **File:** `packages/ui/src/stores/message-v2/bridge.ts:updateMessagePartDelta()` + - **Branch:** IF `session.status` changed + - **Mechanism:** Update session indicator, idle timers, status badges + - **File:** `packages/ui/src/stores/session-status.ts` + - **Branch:** IF `message.part.updated` (completed) + - **Mechanism:** Finalize part content, update tool call state + +5. **UI:** Bridge reconciles SSE events with local state + - Handles optimistic update conflicts + - Merges server truth with local pending operations + +--- + +## Speech Flow (with branches) + +1. **UI:** User enables conversation mode + - **File:** `packages/ui/src/stores/conversation-speech.ts:setConversationModeEnabled()` + - **Branch:** IF `isConversationModeAvailable()` returns false + - **Mechanism:** Show error toast + - **File:** `packages/ui/src/lib/notifications.tsx:showToastNotification()` + - **Action:** Abort speech setup, keep existing state + - **Branch:** IF available + - **Mechanism:** Sync setting to server, initialize speech queue + +2. **Server:** `packages/server/src/server/routes/speech.ts` exposes capabilities + - Returns available TTS/STT providers and models + - **File:** `packages/server/src/speech/service.ts:getSpeechCapabilities()` + +3. **Provider:** `packages/server/src/speech/providers/openai-compatible.ts` synthesizes audio + - Converts text to audio bytes + - **Branch:** IF provider returns error + - **Mechanism:** Return error status to UI + - **File:** `packages/ui/src/components/speech-action-button.tsx` + - **Action:** Display error state, allow retry + - **Branch:** IF successful + - **Mechanism:** Stream audio data to client + +4. **UI:** `packages/ui/src/lib/hooks/use-speech.ts` streams audio playback + - Creates MediaSource for streaming playback + - Appends audio chunks to source buffer + - **Branch:** IF user interrupts (clicks stop or sends new message) + - **Mechanism:** Stop playback, clear queue + - **File:** `packages/ui/src/stores/conversation-speech.ts` + - **Action:** Abort current playback, discard pending chunks + - **Branch:** IF audio completes naturally + - **Mechanism:** Mark playback complete, process next queue item + +--- + +## Background Process Flow (with branches) + +1. **Plugin:** `packages/opencode-plugin/plugin/lib/background-process.ts` creates agent tools + - Defines `run_background_process`, `list_background_processes`, `stop_background_process` + - Validates commands stay within workspace base directory + +2. **Server:** `packages/server/src/background-processes/manager.ts` spawns process + - Uses `spawn` with shell command + - Captures stdout/stderr to log files + - **Branch:** IF spawn fails (command not found, permission denied) + - **Mechanism:** Emit error event, update process status to "error" + - **File:** `packages/server/src/background-processes/manager.ts` + - **Action:** Notify client of failure, keep process record with error state + - **Branch:** IF spawn succeeds + - **Mechanism:** Track PID, stream output, update index + +3. **UI:** `packages/ui/src/stores/background-processes.ts` polls/listens + - Fetches process list periodically + - Subscribes to SSE events for process updates + - **Branch:** IF process completes AND `notify=true` was set + - **Mechanism:** Show completion notification + - **File:** `packages/ui/src/lib/notifications.tsx` + - **Action:** Toast notification with process title and exit code + - **Branch:** IF process errors + - **Mechanism:** Update UI with error status, allow viewing logs + +4. **UI:** `packages/ui/src/components/background-process-output-dialog.tsx` displays stream + - Opens dialog showing real-time output + - Uses ANSI renderer for colored terminal output + - **Branch:** IF user clicks "Stop" + - **Mechanism:** Call `stop_background_process` tool + - **Action:** Send SIGTERM, then SIGKILL if needed + +--- + +## Git Clone Flow (with branches) + +1. **UI:** User initiates clone from UI or command + - **File:** `packages/ui/src/components/folder-selection-view.tsx` or command palette + +2. **Server:** `packages/server/src/server/routes/workspaces.ts` receives request + - Validates `repositoryUrl` and `destinationPath` + - **File:** `packages/server/src/workspaces/git-clone.ts:cloneGitRepository()` + +3. **Validation:** `packages/server/src/workspaces/git-clone.ts` + - **Branch:** IF destination is filesystem root or home folder + - **Mechanism:** Throw `GitCloneError` with 400 status + - **Action:** Return error to client + - **Branch:** IF destination exists and not empty (and cleanup=false) + - **Mechanism:** Throw `GitCloneError` with 409 status + - **Action:** Return error, suggest cleanup or different path + - **Branch:** IF validation passes + - **Mechanism:** Proceed to clone + +4. **Clone Execution:** + - **Branch:** IF destination exists and cleanup=true + - **Mechanism:** `replaceDestinationAfterSuccessfulClone()` + - **Action:** Clone to temp path, swap directories, delete old + - **File:** `packages/server/src/workspaces/git-clone.ts` + - **Branch:** IF destination doesn't exist or is empty + - **Mechanism:** `runGitClone()` direct to destination + - **Action:** Standard `git clone` execution + +5. **Result:** Return `{ path: destinationPath }` on success + - Workspace manager picks up new folder + - UI navigates to new workspace diff --git a/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md b/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md new file mode 100644 index 00000000..636a0744 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md @@ -0,0 +1,109 @@ +# SDK API Reference + +## Overview + +CodeNomad uses the OpenCode SDK V2 (`@opencode-ai/sdk/v2/client`) via `createOpencodeClient()`. + +**Note:** The SDK implementation lives outside this repository. + +- After `npm install`, inspect types in `node_modules/@opencode-ai/sdk/v2/client.d.ts` +- **Fallback:** Use the CodeNomad wrapper locations documented below as the source of truth +- When node_modules is unavailable, read how the SDK is imported in existing files + +## SDK Methods Used by CodeNomad + +### Session + +**SDK:** `client.session.promptAsync({ sessionID, content, command?, agent? })` +**Wrapper:** `packages/ui/src/stores/session-actions.ts` +```typescript +const response = await requestData( + client.session.promptAsync({ sessionID, content }), + "session.promptAsync" +) +``` + +**Other Session Methods Used:** +- `client.session.list()` — List all sessions +- `client.session.create({ parentID? })` — Create new session +- `client.session.get({ sessionID })` — Get session info +- `client.session.delete({ sessionID })` — Delete session +- `client.session.children({ sessionID })` — Get child sessions +- `client.session.diff({ sessionID })` — Get file changes +- `client.session.revert({ sessionID, messageID? })` — Revert code +- `client.session.summarize({ sessionID })` — Generate summary +- `client.session.messages({ sessionID })` — List messages +- `client.session.update({ sessionID, ... })` — Update session properties +- `client.session.command({ sessionID, command })` — Send command +- `client.session.shell({ sessionID, command })` — Execute shell command +- `client.session.abort({ sessionID })` — Abort active session + +**Note on Message Deletion:** The SDK does not expose a typed method for message deletion. CodeNomad uses a raw client call: +```typescript +// packages/ui/src/stores/session-actions.ts:451-457 +await requestData( + (client as any).client.delete({ + url: `/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`, + }), + "session.message.delete", +) +``` + +### Part + +**SDK:** `client.part.delete({ sessionID, messageID, partID })` +**Wrapper:** `packages/ui/src/stores/session-actions.ts:deleteMessagePart()` +```typescript +await requestData( + client.part.delete({ sessionID: sessionId, messageID: messageId, partID: partId }), + "part.delete", +) +``` + +**⚠️ Constraint:** Message must retain ≥1 part. Delete entire message if removing last part. + +**Note on Part Updates:** CodeNomad does not currently use `client.part.update()`. Part modifications are handled through other mechanisms. + +### Permission + +**SDK:** `client.permission.reply({ requestID, reply: "allow" | "deny" | "once" })` +**Wrapper:** `packages/ui/src/stores/instances.ts:sendPermissionResponse()` + +**Other Permission Methods:** +- `client.permission.list()` — Get pending permissions + +### Question + +**SDK:** `client.question.reply({ requestID, answers: string[][] })` +**Wrapper:** `packages/ui/src/stores/instances.ts:sendQuestionReply()` + +**Other Question Methods:** +- `client.question.list()` — Get pending questions +- `client.question.reject({ requestID })` — Reject question + +### File + +**SDK:** `client.file.list({ path })` — List directory contents +**Wrapper:** `packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx` + +**SDK:** `client.file.read({ path })` — Read file content +**Wrapper:** `packages/ui/src/components/instance/shell/right-panel/RightPanel.tsx` + +**SDK:** `client.file.status()` — Get Git status of files +**Wrapper:** `packages/ui/src/components/instance/shell/right-panel/useGitChanges.ts` + +### Config + +**SDK:** `client.config.get()` — Get current configuration +**Wrapper:** `packages/ui/src/lib/hooks/use-instance-metadata.ts` + +**Note:** `client.config.update()` and `client.config.providers()` are available but configuration updates flow through server routes instead. + +## SDK Categories Not Currently Used + +The following SDK categories are available but not actively used by CodeNomad: + +- `client.find.*` — File/symbol search (CodeNomad uses server routes) +- `client.global.*` — Global config/health (CodeNomad uses server meta endpoint) +- `client.app.*` — App logging/agents +- `client.worktree.*` — Git worktree management (CodeNomad uses server routes) diff --git a/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md b/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md new file mode 100644 index 00000000..67607a4c --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md @@ -0,0 +1,109 @@ +# SDK Critical Behaviors + +## Upstream OpenCode Behaviors + +The following behaviors are implemented in the upstream OpenCode SDK/server, not in the CodeNomad repository. They affect how CodeNomad must interact with the SDK. + +## Critical Behaviors Table + +| Behavior | Detail | Impact | Verification | +|----------|--------|--------|--------------| +| `ignored: true` on assistant parts | Backend only checks for user parts | Assistant parts still sent to AI model | Observe via SSE behavior; not verifiable locally | +| Part delete | Message must retain ≥1 part | Delete entire message if last part | `packages/ui/src/stores/session-actions.ts` | +| Metadata on assistant parts | Passed as `providerMetadata` to ai SDK | Flat objects cause fatal schema violations | Avoid setting metadata on assistant parts | +| Session revert | Only restores files to Git snapshot | Not an undo mechanism for messages | Test via `client.session.revert()` | +| Empty messages | Backend rejects `parts: []` | Check part count before delete | `packages/ui/src/stores/session-actions.ts` | + +## Schema Violation Details + +### Assistant Part Metadata (Fatal) + +**Behavior:** Assistant text part `metadata` is passed as `providerMetadata` to the underlying AI SDK. + +**Expected format:** +```typescript +providerMetadata?: Record> +``` + +**Violation examples:** +```typescript +// ❌ WRONG: Flat object +metadata: { compacted: true } + +// ❌ WRONG: Missing provider name wrapper +metadata: { key: "value" } + +// ✅ CORRECT: Nested by provider +metadata: { openai: { key: "value" } } +``` + +**Fix:** Do not store metadata on assistant text parts. Use client-side registry instead: +```typescript +// ✅ Use client-side registry +// packages/ui/src/stores/session-compaction.ts +const compactedParts = new Set() // part IDs +``` + +### Empty Messages After Part Deletion + +**Root Cause:** Backend validates messages have ≥1 part + +**Fix:** Check remaining part count before deleting last part +```typescript +// packages/ui/src/stores/session-actions.ts +if (record.partIds.length <= 1) { + // Delete entire message instead + await deleteMessage(sessionID, messageID) +} else { + await deleteMessagePart(sessionID, messageID, partID) +} +``` + +## `ignored` Flag Asymmetry + +| Part Type | `ignored: true` Effect | Notes | +|-----------|------------------------|-------| +| User text | ✅ Excluded from AI model context | Safe to use | +| Assistant text | ❌ No effect — still sent to model | Do not rely on this | +| Tool | ❌ No `ignored` field exists | N/A | +| Reasoning | ❌ No `ignored` field exists | N/A | + +**Implication:** Cannot "soft delete" assistant parts. Must delete or use client-side registry. + +## Decision Matrix: Context Modification + +| Goal | Strategy | SDK Support | Safe? | +|------|----------|-------------|-------| +| Update assistant text | `Part.update()` (if available) | ✅ | ✅ Yes (no metadata) | +| Update user text | `Part.update()` (if available) | ✅ | ✅ Yes | +| Hide user part from AI | `ignored: true` | ✅ | ✅ Yes | +| Hide assistant part from AI | `ignored: true` | ⚠️ No effect | ❌ No effect | +| Delete part | `client.part.delete()` | ✅ | ✅ Yes (check message parts) | +| Delete message | Raw DELETE via client | ✅ | ✅ Yes (irreversible) | +| Undo message deletion | Client-side restore | ⚠️ Manual | ⚠️ Must recreate | +| Revert code changes | `client.session.revert()` | ✅ | ✅ Only affects files | +| Store UI state | Client-side registry | N/A | ✅ localStorage/Set | + +## Race Conditions + +### Optimistic Updates + +**Symptom:** UI state desync after rapid operations + +**Cause:** `removeMessagePartV2()` and `removeMessageV2()` called optimistically before server confirmation + +**Mitigation:** SSE events eventually converge state. Do not rely on optimistic state for subsequent operations. + +### SSE Disconnection + +**Symptom:** Missed events during reconnection + +**Mitigation:** `serverEvents` reconnection triggers sync handlers (e.g., `syncPendingPermissions()`) to reconcile state. + +## Recommendations + +1. **Never store flat metadata on assistant text parts.** Always use client-side registries for UI state. +2. **Prefer user messages for metadata-heavy operations.** User text parts don't pass metadata to ai SDK. +3. **Implement client-side undo for destructive operations.** The SDK has no native message-level undo. +4. **Validate part payloads before sending.** Always spread existing part and override only specific fields. +5. **Handle `ignored` carefully.** It only works for user text parts. Don't rely on it for assistant parts. diff --git a/.opencode/skills/codenomad-architecture-guide/references/sdk-integration-patterns.md b/.opencode/skills/codenomad-architecture-guide/references/sdk-integration-patterns.md new file mode 100644 index 00000000..eda86515 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/sdk-integration-patterns.md @@ -0,0 +1,209 @@ +# SDK Integration Patterns + +## Client Lifecycle + +### SDK Manager + +CodeNomad creates and manages `OpencodeClient` instances through `SDKManager`: + +```typescript +// packages/ui/src/lib/sdk-manager.ts +class SDKManager { + private clients = new Map() + + createClient(instanceId: string, proxyPath: string): OpencodeClient { + const baseUrl = buildInstanceBaseUrl(proxyPath) + return createOpencodeClient({ baseUrl }) + } +} +``` + +### Worktree-Based Routing + +SDK clients are routed per worktree, not just per instance: + +```typescript +// packages/ui/src/stores/worktrees.ts +export function getOrCreateWorktreeClient( + instanceId: string, + worktreeSlug: string +): OpencodeClient { + const proxyPath = `/worktrees/${worktreeSlug}` + return sdkManager.createClient(instanceId, proxyPath) +} +``` + +**Rule:** Always use `getOrCreateWorktreeClient()` rather than creating clients directly. This ensures: +- Correct base URL with worktree proxy path +- Client caching and reuse +- Proper cleanup on instance disposal + +### Base URL Construction + +```typescript +// packages/ui/src/lib/sdk-manager.ts +export function buildInstanceBaseUrl(proxyPath: string): string { + const normalized = normalizeProxyPath(proxyPath) + const base = stripTrailingSlashes(CODENOMAD_API_BASE) + return `${base}${normalized}/` +} +``` + +## Error Handling + +### RequestData Wrapper + +Most SDK calls that return `{ data, error }` go through `requestData()` for consistent error handling: + +```typescript +// packages/ui/src/lib/opencode-api.ts +export async function requestData( + promise: Promise<{ data?: T; error? }>, + operation: string +): Promise { + const response = await promise + if (response.error) { + log.error(`API error in ${operation}`, response.error) + throw response.error + } + if (response.data === undefined) { + throw new Error(`No data returned from ${operation}`) + } + return response.data +} +``` + +### Pattern + +```typescript +// Always wrap SDK calls +const sessions = await requestData( + client.session.list(), + "session.list" +) + +// Direct SDK calls are also used when the method doesn't return { data, error } +// Example: const response = await rootClient.session.list() +``` + +## Optimistic Updates + +### Pattern + +1. Update local state immediately +2. Make API call +3. Handle success/error +4. SSE events eventually confirm/converge + +```typescript +// packages/ui/src/stores/message-v2/bridge.ts +export function removePermissionV2(instanceId: string, requestId: string) { + // 1. Optimistic: Remove from local store + updateMessageStore(instanceId, (store) => { + store.permissions.delete(requestId) + }) + + // 2. API call (may fail) + // 3. SSE event eventually confirms +} +``` + +### Reconciliation + +SSE events from the server eventually reconcile optimistic state: + +| Event | Handler | File | +|-------|---------|------| +| `message.part.updated` | `updateMessagePartV2()` | `bridge.ts` | +| `message.part.removed` | `removeMessagePartV2()` | `bridge.ts` | +| `permission.replied` | `removePermissionV2()` | `bridge.ts` | +| `question.replied` | `removeQuestionV2()` | `bridge.ts` | + +### Race Condition Warning + +Rapid successive operations can cause temporary desync: +- Delete part → quickly delete message → may error if part delete in flight +- Always check current state before optimistic updates + +## Permission Flow + +1. **Server emits** `permission.asked` or `permission.updated` SSE event + - Pushed through instance event stream +2. **UI Store receives** via `serverEvents` + - File: `packages/ui/src/stores/instances.ts` + - **Branch:** IF `isPermissionAutoAcceptEnabled()` + - Mechanism: `drainAutoAcceptPermissions()` in `packages/ui/src/stores/permission-auto-accept.ts` + - Action: Calls reply immediately, skips modal + - **Branch:** ELSE + - Mechanism: Queued in `permissionQueues` + - Action: Display modal +3. **UI Store:** `packages/ui/src/stores/message-v2/bridge.ts` calls `upsertPermissionV2()` +4. **UI Component:** `packages/ui/src/components/permission-approval-modal.tsx` displays +5. **User Action:** Calls `packages/ui/src/stores/instances.ts:sendPermissionResponse()` +6. **SDK Call:** `client.permission.reply()` via `packages/ui/src/lib/opencode-api.ts` +7. **Optimistic Update:** `removePermissionV2()` in bridge +8. **SSE Confirmation:** `permission.replied` event + - **Branch:** IF SSE disconnected → `syncPendingPermissions()` reconciles on reconnect + +## Session Event Handling + +### SSE Event Types + +| Event | Direction | Description | +|-------|-----------|-------------| +| `message.part.delta` | Server → UI | Streaming text update | +| `message.part.updated` | Server → UI | Part content changed | +| `message.part.removed` | Server → UI | Part deleted | +| `session.status` | Server → UI | Session status changed | +| `permission.asked` | Server → UI | New permission request | +| `permission.updated` | Server → UI | Permission updated | +| `permission.replied` | Server → UI | Permission resolved | +| `question.asked` | Server → UI | New question | +| `question.replied` | Server → UI | Question answered | +| `question.rejected` | Server → UI | Question rejected | + +### Event Source Setup + +```typescript +// packages/ui/src/lib/event-source-handlers.ts +export function attachEventSourceHandlers( + source: EventSource, + options: EventSourceHandlerOptions +) { + source.onmessage = (event) => { + const payload = JSON.parse(event.data) + options.onEvent(payload) + } + + source.onerror = () => { + options.onError?.() + } + + ;(source as EventSourceWithClose).onclose = () => { + options.onError?.() + } +} +``` + +## Worktree Client Pattern + +```typescript +// Always route through worktree +const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId) +const client = getOrCreateWorktreeClient(instanceId, worktreeSlug) + +// Then use client normally +const diff = await requestData( + client.session.diff({ sessionID: sessionId }), + "session.diff" +) +``` + +## Cleanup Pattern + +```typescript +// On instance disposal +sdkManager.destroyClientsForInstance(instanceId) +messageStoreBus.unregister(instanceId) +clearCacheForInstance(instanceId) +``` diff --git a/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md b/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md new file mode 100644 index 00000000..65b783d8 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/server-conventions.md @@ -0,0 +1,114 @@ +# Server Conventions + +## Framework: Fastify + +- Routes registered in `packages/server/src/server/routes/` +- Route handlers typed with Fastify generics +- Dependencies injected via `RouteDeps` interfaces + +### Route Registration Pattern + +```typescript +// packages/server/src/server/routes/example.ts +interface RouteDeps { + exampleManager: ExampleManager +} + +function registerExampleRoutes(app: FastifyInstance, deps: RouteDeps) { + app.get("/api/examples", async () => { + return deps.exampleManager.list() + }) +} +``` + +## API Types + +- **Shared types:** `packages/server/src/api-types.ts` +- **Consumed by UI:** `packages/ui/src/types/` +- **Breaking change rule:** Changing a type requires checking UI for matching interfaces +- **Preferred approach:** Additive changes (new optional fields) over breaking changes + +### Type Sharing Pattern + +```typescript +// Server defines in api-types.ts +export interface ExampleResponse { + id: string + name: string +} + +// UI may extend or mirror in packages/ui/src/types/ +export type { ExampleResponse } from "../../../server/src/api-types" +``` + +## Configuration + +- **Settings service:** `packages/server/src/settings/service.ts` +- **YAML document store:** `packages/server/src/settings/yaml-doc-store.ts` +- **Public config sanitization:** `packages/server/src/settings/public-config.ts` +- **Config location resolution:** `packages/server/src/config/location.ts` + +### Settings Documents + +| Document | Purpose | File | Notes | +|----------|---------|------|-------| +| Config | User preferences, binaries, models | `~/.config/codenomad/config.yaml` | Canonical format | +| State | Recent folders, session metadata | `~/.config/codenomad/state.yaml` | Canonical format | +| Config (legacy) | Migration fallback | `~/.config/codenomad/config.json` | Supported as input fallback | + +## Testing + +- **Route tests:** Fastify inject in `__tests__/` subdirectories +- **Example:** `packages/server/src/server/__tests__/network-addresses.test.ts` +- **No integration tests** for external services + +### Route Test Pattern + +```typescript +// packages/server/src/server/routes/__tests__/example.test.ts +import { createApp } from "./helpers" + +test("GET /api/examples", async () => { + const app = createApp() + const response = await app.inject({ + method: "GET", + url: "/api/examples" + }) + expect(response.statusCode).toBe(200) +}) +``` + +## Background Processes + +- **Manager:** `packages/server/src/background-processes/manager.ts` +- **Spawned via:** `spawn` with persistent output tracking +- **Output streaming:** SSE events for real-time UI updates +- **Process lifecycle:** start → running → stop/error + +## Workspaces + +- **Workspace manager:** `packages/server/src/workspaces/manager.ts` +- **Runtime:** `packages/server/src/workspaces/runtime.ts` +- **Git worktrees:** `packages/server/src/workspaces/git-worktrees.ts` +- **Spawn spec:** `packages/server/src/workspaces/spawn.ts` + +### Workspace Lifecycle + +1. Create workspace (folder path) +2. Spawn OpenCode server process +3. Manage via workspace runtime +4. Clean up on delete + +## Authentication + +- **Auth manager:** `packages/server/src/auth/manager.ts` +- **Session manager:** `packages/server/src/auth/session-manager.ts` +- **Token manager:** `packages/server/src/auth/token-manager.ts` +- **Password hashing:** `packages/server/src/auth/password-hash.ts` + +### Auth Flow + +1. Server generates bootstrap token on startup +2. UI exchanges token for session cookie +3. Subsequent requests use session cookie +4. Credentials stored in auth file (hashed with scrypt) diff --git a/.opencode/skills/codenomad-architecture-guide/references/ui-conventions.md b/.opencode/skills/codenomad-architecture-guide/references/ui-conventions.md new file mode 100644 index 00000000..d9a64e66 --- /dev/null +++ b/.opencode/skills/codenomad-architecture-guide/references/ui-conventions.md @@ -0,0 +1,130 @@ +# UI Conventions + +## Framework: SolidJS + +- Components use JSX (not React) +- State management via signals: `createSignal()`, `createMemo()` +- Hooks follow `use*` naming convention: `useSpeech()`, `useScrollCache()` +- Effects via `createEffect()` +- Cleanup via `onCleanup()` + +## i18n (Internationalization) + +### Runtime API + +- **In components:** `const { t } = useI18n()` +- **In stores/non-component code:** `tGlobal("key")` +- **Implementation:** `packages/ui/src/lib/i18n/index.tsx` + +### Message Files + +- **Location:** `packages/ui/src/lib/i18n/messages//` +- **Format:** TypeScript objects with flat dot keys: `"flat.dot.keys": "string"` +- **Merge helper:** `packages/ui/src/lib/i18n/messages/merge.ts` +- **Duplicate keys:** Throw at build time + +### Supported Locales (7) + +| Locale | Code | Direction | +|--------|------|-----------| +| English | `en` | LTR | +| Spanish | `es` | LTR | +| French | `fr` | LTR | +| Russian | `ru` | LTR | +| Japanese | `ja` | LTR | +| Simplified Chinese | `zh-Hans` | LTR | +| Hebrew | `he` | RTL | + +### Adding a New String + +1. Add to `packages/ui/src/lib/i18n/messages/en/*.ts` (appropriate part file) +2. Add same key to each other locale's corresponding file +3. Missing translations fall back to English (then to the key itself) + +### Anti-Pattern + +```typescript +// ❌ WRONG: Importing English messages directly +import { enMessages } from "../lib/i18n/messages/en" +const text = enMessages["key"] + +// ✅ CORRECT: Using the translation function +const { t } = useI18n() +const text = t("key") +``` + +## Stores + +### Pattern + +- Signal-based using SolidJS `createSignal()` +- Export signal accessor and setter: `export const [things, setThings] = createSignal(...)` +- Co-locate related stores (e.g., `session-*.ts` files for session management) + +### File Size Limits + +| Type | Warning | Target Limit | +|------|---------|-------------| +| Source files | >500 lines | <800 lines | +| Test files | >1000 lines | <1000 lines | + +### Example Store Structure + +```typescript +// packages/ui/src/stores/example.ts +import { createSignal } from "solid-js" + +const [items, setItems] = createSignal>(new Map()) + +export { items, setItems } + +export function addItem(item: Item): void { + setItems((prev) => { + const next = new Map(prev) + next.set(item.id, item) + return next + }) +} +``` + +## Components + +### Styling + +- Use existing token/utility CSS layers +- Tokens: `src/styles/tokens.css` +- Utilities: `src/styles/utilities.css` +- Co-locate reusable UI patterns under `src/styles/components/` +- New component styles: place in scoped subdirectory, import from aggregator file + +### Pattern + +```typescript +// packages/ui/src/components/example.tsx +import { useI18n } from "../lib/i18n" + +export function ExampleComponent(props: ExampleProps) { + const { t } = useI18n() + + return ( +
+ {t("example.key")} +
+ ) +} +``` + +## Testing + +- **Framework:** Vitest +- **UI stores/utilities:** `*.test.ts` alongside source files +- **Example:** `packages/ui/src/stores/session-status.test.ts` + +## Commit Messages + +- Use conventional style subject line +- Body paragraphs explain: + - User-visible behavior change + - Implementation approach + - Edge cases or platform considerations + - Validation or test coverage