renderer-patterns
All paths are relative to `apps/emdash-desktop/`.
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.
All paths are relative to `apps/emdash-desktop/`.
Agent definition
renderer-patterns.mdRenderer Patterns
All paths are relative to `apps/emdash-desktop/`.
Modal System
All modals use a registry-based system. Only one modal can be active at a time.
- `src/renderer/app/modal-registry.ts` — central registry mapping modal IDs to components
(`createModal`, `modalRegistry`)
- `src/renderer/lib/modal/modal-provider.tsx` — React context managing active modal state
(`useModalContext`, `showModal`, `BaseModalProps`)
- `src/renderer/lib/modal/modal-renderer.tsx` — renders the currently active modal
- `src/renderer/lib/modal/modal-store.ts` — modal state store
- `src/renderer/lib/modal/use-close-guard.ts` — close-guard hook
**Adding a modal:** 1. Create the component accepting `BaseModalProps<TResult>` (provides `onSuccess` and `onClose` callbacks) 2. Register it in `src/renderer/app/modal-registry.ts` 3. Open it via the hook:
const { showModal } = useModalContext();
showModal('myModal', { projectId: '123', onSuccess: (result) => {...} });**Rules:**
- All modals must be registered in `src/renderer/app/modal-registry.ts`
- `showModal` is type-safe — TypeScript infers required args from the registry
- `hasActiveCloseGuard` prevents dismissal during critical operations
View System
Views use a registry + parameterized navigation pattern.
- `src/renderer/app/view-registry.ts` — view definitions (required `MainPanel`, optional
`WrapView` and `TitlebarSlot`) plus navigation guards (`setupNavigationGuards`)
- `src/renderer/lib/layout/` — `provider.tsx`, `navigation-provider.tsx` (navigation and
param persistence), `layout-provider.tsx` (panel collapse/expand/drag state), `panel-drag-store.ts`
**Key behaviors:**
- `navigate(viewId, params?)` (from `useNavigate`) is type-safe; params are optional when all fields are optional
- Params persist per-view (navigating away and back preserves params)
- `updateViewParams(viewId, partial)` updates params without re-navigating
**Rules:**
- Views are singletons — one per ViewId
- Add new views to `src/renderer/app/view-registry.ts`
PTY Frontend (`src/renderer/lib/pty/`)
- `pty.ts` — `FrontendPty` class; subscribing fetches the main-process ring buffer and
registers the consumer in one synchronous tick, so there is no renderer-side buffer and no missed output
- `pty-session.ts` — session lifecycle
- `pty-pool-provider.tsx` — `TerminalPoolProvider` managing reusable xterm.js instances
- `pty-pane.tsx` — terminal pane component
- `prompt-injection.ts`, `pty-input-buffer.ts`, `pty-keybindings.ts`, `pty-clipboard.ts` — input handling
**Rules:**
- Historical output comes from the main-process ring buffer; do not add renderer-side buffering
- `sessionId` format: `makePtySessionId(projectId, scopeId, leafId)` from
`src/shared/core/pty/ptySessionId.ts` — deterministic
- Panel drag pauses resizing to avoid jank (`src/renderer/lib/layout/panel-drag-store.ts`)
React Query Context Pattern
Context providers use React Query for data fetching with optimistic updates:
// Pattern used in AppSettingsProvider, ProjectProvider, etc.
const { data } = useQuery({ queryKey: ['resource'], queryFn: () => rpc.ns.get() });
const mutation = useMutation({
mutationFn: (args) => rpc.ns.update(args),
onMutate: async (args) => {
// optimistic update via queryClient.setQueryData
},
onError: () => {
// rollback via queryClient.setQueryData with previous snapshot
},
});**Rules:**
- Contexts combine React Query + local state, not standalone useState
- Use `useAppSettingsKey(key)` for fine-grained per-setting hooks
- Optimistic updates must include rollback on error
State Outside React
For state that must survive React unmounts or be shared across unrelated components:
- **`useSyncExternalStore`-compatible stores** — e.g., `panelDragStore` in `src/renderer/lib/layout/`
- **Cross-feature stores** — `src/renderer/lib/stores/` (navigation, dependencies, resource monitor, ...)
- **MobX task and project stores** — `src/renderer/features/tasks/stores/` and
`src/renderer/features/projects/stores/`; access them through selectors (`task-selectors.ts`, `project-selectors.ts`) and task view hooks, never directly
Read more
Renderer Patterns
All paths are relative to `apps/emdash-desktop/`.
Modal System
All modals use a registry-based system. Only one modal can be active at a time.
- `src/renderer/app/modal-registry.ts` — central registry mapping modal IDs to components
(`createModal`, `modalRegistry`)
- `src/renderer/lib/modal/modal-provider.tsx` — React context managing active modal state
(`useModalContext`, `showModal`, `BaseModalProps`)
- `src/renderer/lib/modal/modal-renderer.tsx` — renders the currently active modal
- `src/renderer/lib/modal/modal-store.ts` — modal state store
- `src/renderer/lib/modal/use-close-guard.ts` — close-guard hook
**Adding a modal:** 1. Create the component accepting `BaseModalProps<TResult>` (provides `onSuccess` and `onClose` callbacks) 2. Register it in `src/renderer/app/modal-registry.ts` 3. Open it via the hook:
const { showModal } = useModalContext();
showModal('myModal', { projectId: '123', onSuccess: (result) => {...} });**Rules:**
- All modals must be registered in `src/renderer/app/modal-registry.ts`
- `showModal` is type-safe — TypeScript infers required args from the registry
- `hasActiveCloseGuard` prevents dismissal during critical operations
View System
Views use a registry + parameterized navigation pattern.
- `src/renderer/app/view-registry.ts` — view definitions (required `MainPanel`, optional
`WrapView` and `TitlebarSlot`) plus navigation guards (`setupNavigationGuards`)
- `src/renderer/lib/layout/` — `provider.tsx`, `navigation-provider.tsx` (navigation and
param persistence), `layout-provider.tsx` (panel collapse/expand/drag state), `panel-drag-store.ts`
**Key behaviors:**
- `navigate(viewId, params?)` (from `useNavigate`) is type-safe; params are optional when all fields are optional
- Params persist per-view (navigating away and back preserves params)
- `updateViewParams(viewId, partial)` updates params without re-navigating
**Rules:**
- Views are singletons — one per ViewId
- Add new views to `src/renderer/app/view-registry.ts`
PTY Frontend (`src/renderer/lib/pty/`)
- `pty.ts` — `FrontendPty` class; subscribing fetches the main-process ring buffer and
registers the consumer in one synchronous tick, so there is no renderer-side buffer and no missed output
- `pty-session.ts` — session lifecycle
- `pty-pool-provider.tsx` — `TerminalPoolProvider` managing reusable xterm.js instances
- `pty-pane.tsx` — terminal pane component
- `prompt-injection.ts`, `pty-input-buffer.ts`, `pty-keybindings.ts`, `pty-clipboard.ts` — input handling
**Rules:**
- Historical output comes from the main-process ring buffer; do not add renderer-side buffering
- `sessionId` format: `makePtySessionId(projectId, scopeId, leafId)` from
`src/shared/core/pty/ptySessionId.ts` — deterministic
- Panel drag pauses resizing to avoid jank (`src/renderer/lib/layout/panel-drag-store.ts`)
React Query Context Pattern
Context providers use React Query for data fetching with optimistic updates:
// Pattern used in AppSettingsProvider, ProjectProvider, etc.
const { data } = useQuery({ queryKey: ['resource'], queryFn: () => rpc.ns.get() });
const mutation = useMutation({
mutationFn: (args) => rpc.ns.update(args),
onMutate: async (args) => {
// optimistic update via queryClient.setQueryData
},
onError: () => {
// rollback via queryClient.setQueryData with previous snapshot
},
});**Rules:**
- Contexts combine React Query + local state, not standalone useState
- Use `useAppSettingsKey(key)` for fine-grained per-setting hooks
- Optimistic updates must include rollback on error
State Outside React
For state that must survive React unmounts or be shared across unrelated components:
- **`useSyncExternalStore`-compatible stores** — e.g., `panelDragStore` in `src/renderer/lib/layout/`
- **Cross-feature stores** — `src/renderer/lib/stores/` (navigation, dependencies, resource monitor, ...)
- **MobX task and project stores** — `src/renderer/features/tasks/stores/` and
`src/renderer/features/projects/stores/`; access them through selectors (`task-selectors.ts`, `project-selectors.ts`) and task view hooks, never directly
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

