main-patterns
Each domain in `src/main/core/` exposes a `controller.ts` that defines RPC handlers:
How it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
Each domain in `src/main/core/` exposes a `controller.ts` that defines RPC handlers:
Agent definition
main-patterns.mdMain Process Patterns
Controller Pattern
Each domain in `src/main/core/` exposes a `controller.ts` that defines RPC handlers:
// src/main/core/tasks/controller.ts
import { createRPCController } from '@shared/ipc/rpc';
import { createTask } from './createTask';
import { getTasks } from './getTasks';
export const taskController = createRPCController({
createTask,
getTasks,
deleteTask,
// ...
});Controllers are assembled into the router in `src/main/rpc.ts`:
export const rpcRouter = createRPCRouter({
tasks: taskController,
projects: projectController,
// ...
});**Rules:**
- Controller handlers are imported functions — keep logic in separate operation files, not inline
- Each controller becomes an RPC namespace (e.g., `rpc.tasks.createTask(...)` on the renderer)
- New domains need their controller added to `src/main/rpc.ts`
Service Pattern
For stateful concerns, use singleton classes:
export class AppService {
private cache = new Map();
async initialize() { /* ... */ }
async doSomething(id: string) { /* ... */ }
}
export const appService = new AppService();**Rules:**
- Module-level singleton export
- Initialization method called from `src/main/index.ts`
- Services hold long-lived state (caches, subscriptions, connections)
Provider Pattern
For domain logic with multiple backends (local vs SSH):
src/main/core/projects/
├── project-provider.ts # Interface
├── impl/
│ ├── local-project-provider.ts
│ └── _ssh-project-provider.ts # Prefixed with _ = not yet implemented
└── project-manager.ts # Orchestrates providers
Used in: projects, filesystem (`local-fs.ts` / `ssh-fs.ts`), terminals (`local-terminal-provider.ts` / `ssh-terminal-provider.ts`)
Result Type (`src/main/lib/result.ts`)
Explicit error handling via discriminated union:
import { ok, err, type Result } from '../lib/result';
async function doSomething(): Promise<Result<Data, SomeError>> {
if (problem) return err({ type: 'not_found' as const });
return ok(data);
}**Rules:**
- Prefer `Result<T, E>` over thrown exceptions for expected failure modes
- Controllers convert Result types to IPC-compatible responses
Event System (`src/main/lib/events.ts`)
Topic-based event emitter for main ↔ renderer communication:
import { events } from '../lib/events';
// Emit to a specific topic (e.g., session ID)
events.emit(ptyDataChannel, buffer, sessionId);
// Listen on a specific topic
const unsub = events.on(ptyDataChannel, (data) => {...}, sessionId);Channel naming: without topic → `eventName`, with topic → `eventName.{topic}`
Event type definitions live in `src/shared/events/`.
Read more
Main Process Patterns
Controller Pattern
Each domain in `src/main/core/` exposes a `controller.ts` that defines RPC handlers:
// src/main/core/tasks/controller.ts
import { createRPCController } from '@shared/ipc/rpc';
import { createTask } from './createTask';
import { getTasks } from './getTasks';
export const taskController = createRPCController({
createTask,
getTasks,
deleteTask,
// ...
});Controllers are assembled into the router in `src/main/rpc.ts`:
export const rpcRouter = createRPCRouter({
tasks: taskController,
projects: projectController,
// ...
});**Rules:**
- Controller handlers are imported functions — keep logic in separate operation files, not inline
- Each controller becomes an RPC namespace (e.g., `rpc.tasks.createTask(...)` on the renderer)
- New domains need their controller added to `src/main/rpc.ts`
Service Pattern
For stateful concerns, use singleton classes:
export class AppService {
private cache = new Map();
async initialize() { /* ... */ }
async doSomething(id: string) { /* ... */ }
}
export const appService = new AppService();**Rules:**
- Module-level singleton export
- Initialization method called from `src/main/index.ts`
- Services hold long-lived state (caches, subscriptions, connections)
Provider Pattern
For domain logic with multiple backends (local vs SSH):
src/main/core/projects/ ├── project-provider.ts # Interface ├── impl/ │ ├── local-project-provider.ts │ └── _ssh-project-provider.ts # Prefixed with _ = not yet implemented └── project-manager.ts # Orchestrates providers
Used in: projects, filesystem (`local-fs.ts` / `ssh-fs.ts`), terminals (`local-terminal-provider.ts` / `ssh-terminal-provider.ts`)
Result Type (`src/main/lib/result.ts`)
Explicit error handling via discriminated union:
import { ok, err, type Result } from '../lib/result';
async function doSomething(): Promise<Result<Data, SomeError>> {
if (problem) return err({ type: 'not_found' as const });
return ok(data);
}**Rules:**
- Prefer `Result<T, E>` over thrown exceptions for expected failure modes
- Controllers convert Result types to IPC-compatible responses
Event System (`src/main/lib/events.ts`)
Topic-based event emitter for main ↔ renderer communication:
import { events } from '../lib/events';
// Emit to a specific topic (e.g., session ID)
events.emit(ptyDataChannel, buffer, sessionId);
// Listen on a specific topic
const unsub = events.on(ptyDataChannel, (data) => {...}, sessionId);Channel naming: without topic → `eventName`, with topic → `eventName.{topic}`
Event type definitions live in `src/shared/events/`.
Emdash is the Open-Source Agentic Development Environment (🧡 YC W26). Run multiple coding agents in parallel. Use any provider.
Repo: generalaction/emdash
Other agents on emdash.
- acp-runtime
The ACP runtime is the domain service that serves the ACP API contract. It owns the host-scoped dependencies needed to run provider ACP sessions, but it should not mix cross-session routing with per-session state projection.
Open agent - main-process
The main process is organized into domain modules under `src/main/core/`. Each domain typically has a `controller.ts` (RPC handlers) and service/implementation files.
Open agent - overview
All paths are relative to `apps/emdash-desktop/`.
Open agent - renderer
All paths are relative to `apps/emdash-desktop/`.
Open agent - shared
- Agent/provider DTOs: - `src/shared/core/agents/agent-payload.ts` - provider metadata and capabilities are sourced from `packages/plugins/src/agents/registry.ts` - IPC primitives: - `src/shared/ipc/rpc.ts` — typed RPC router, controller, and client - `src/shared/ipc/events.ts`
Open agent - workspace-server
The Workspace Server (`apps/workspace-server/`) is a Node daemon that runs on a remote machine and exposes workspace runtimes (git, files, deps, ACP, …) to Emdash clients over the `@emdash/wire` protocol. Clients connect over an SSH-forwarded Unix socket; the daemon is
Open agent

