Thanks for your interest in contributing. We favor small, focused PRs with clear intent. This guide covers the local development setup, the commands that matter, and the conventions contributors should follow before opening a PR.
- Git
- Node.js
24.14.0from.nvmrc pnpm@10.28.2- Optional, but useful for integration work:
- GitHub CLI (
gh) - At least one supported coding agent CLI
- Docker, when working on SSH development infrastructure
- GitHub CLI (
Use the pinned toolchain where possible:
nvm use
corepack enable
pnpm --versionFork the repository on GitHub, then clone your fork:
git clone https://github.com/<you>/emdash.git
cd emdashFrom the repo root:
pnpm installThis repository is a pnpm workspace. The Electron app is in
apps/emdash-desktop/, and shared workspace packages live in packages/.
For normal app development, run the full workspace dev command from the repo root:
pnpm run devThe root dev command now does two things:
- Builds all packages under
packages/. - Starts package watch builds and the Electron desktop app in parallel.
Use this command when you are changing code in packages/ or when you want the
same startup path a fresh contributor will use.
If you are only working inside apps/emdash-desktop/, you can run the Electron
dev server directly:
cd apps/emdash-desktop
pnpm run devFrom apps/emdash-desktop/, pnpm run d is a convenience command that runs
pnpm install and then starts pnpm run dev for the desktop app:
cd apps/emdash-desktop
pnpm run dImportant distinction:
pnpm run devfrom the repo root starts the workspace package watchers and the app together.pnpm run devfromapps/emdash-desktop/starts onlyelectron-vite devfor the desktop app.- If app code imports changed package output, prefer the root command so package
dist/files stay current.
Renderer changes usually hot reload. Main-process changes under
apps/emdash-desktop/src/main/ may require restarting the Electron dev app.
This is a pnpm workspace monorepo.
apps/emdash-desktop/- Electron desktop app packageapps/emdash-desktop/src/main/- Electron main process, RPC controllers, services, database, PTY, SSH, Git, GitHub, updates, and integrationsapps/emdash-desktop/src/preload/- typed Electron preload bridgeapps/emdash-desktop/src/renderer/- React renderer appapps/emdash-desktop/src/shared/- shared app IPC, provider, event, MCP, skills, and domain typesapps/emdash-desktop/drizzle/- generated Drizzle migrations and metadataapps/emdash-desktop/scripts/- release, verification, and build scriptspackages/core/- transport-agnostic core runtime primitivespackages/shared/- shared workspace primitivespackages/ui/- shared UI components and theme systempackages/plugins/- plugin interfaces and helpersagents/- architecture, workflow, convention, integration, and risk docs
Root scripts are aggregate workspace scripts. Most app-specific commands live in
apps/emdash-desktop/package.json.
Run these from the repo root unless noted. Root scripts run through Nx, which
builds projects in dependency order and caches results locally. A second run
of any cached target (build, test, typecheck, lint, format:check)
replays instantly if inputs have not changed.
pnpm run dev # build packages, watch packages, and start the Electron app
pnpm run build # build every workspace package
pnpm run format # format with oxfmt
pnpm run format:check # check formatting without writing
pnpm run lint # lint with oxlint
pnpm run typecheck # run TypeScript checks
pnpm run test # run workspace tests
pnpm run affected # lint, typecheck, and test only projects changed vs. main
pnpm run graph # open the Nx project graph in the browserIndividual project targets are addressable from the root without cd:
nx package:mac @emdash/emdash-desktop
nx db:reset @emdash/emdash-desktop
nx storybook @emdash/ui
nx theme:build @emdash/uiSee agents/workflows/nx.md for a full explanation of the Nx setup.
Useful app-local commands from apps/emdash-desktop/:
pnpm run d # install dependencies, then start the desktop app
pnpm run dev # start electron-vite dev for the desktop app only
pnpm run dev:debug # start with debug logging
pnpm run dev:main # watch the Electron main process
pnpm run dev:renderer # watch the renderer
pnpm run build # build the Electron app
pnpm run build:main # build main process only
pnpm run build:renderer # build renderer only
pnpm run package # build and package desktop artifacts
pnpm run rebuild # rebuild native Electron dependencies
pnpm run reset # clean app dependencies and reinstallUseful package-local commands from a package under packages/:
pnpm run dev # watch-build that package with tsdown
pnpm run build # build that package with tsdown
pnpm run test
pnpm run typecheckBefore opening or merging a PR, run the local merge gate:
pnpm run format
pnpm run lint
pnpm run typecheck
pnpm run testThere are no pre-commit hooks. CI enforces format:check, typecheck, and lint via
nx affected — only projects touched by the PR are checked. Tests are still
expected locally even when a specific CI workflow does not run the full test suite.
- Create a feature branch:
git checkout -b feat/<short-slug>- Keep PRs small and focused.
Update docs when behavior changes. Include screenshots or short recordings for UI changes where they help reviewers understand the result.
- Run validation locally.
Use the full merge gate above for broad changes. For narrow work, it is fine to run focused tests while iterating, then run the full gate before opening or merging the PR.
- Commit using Conventional Commits:
fix(opencode): change initialPromptFlag from -p to --prompt for TUI
feat(docs): add changelog tab with GitHub releases integration
- Open a pull request.
Describe the change, the reason for it, and the validation you ran. Link related issues when relevant.
- Use TypeScript strict mode.
- Use top-level
importstatements, notrequire(). - Do not introduce npm or yarn lockfiles.
- Use
pnpm. - Format with
oxfmt. - Lint with
oxlint. - Keep lines near the configured
printWidthof 100 characters. - Use 2 spaces, semicolons, single quotes in TypeScript, double quotes in JSX, LF endings, and trailing commas where valid in ES5.
- Avoid
any. If a boundary requires it, keep the escape local and document why. - Do not re-export as a shortcut. Import from the original source.
The app follows this high-level flow:
Renderer -> typed RPC client -> preload bridge -> Electron main -> controllers -> services
Main process:
- RPC handlers live in
src/main/core/*/controller.ts. - Controllers should delegate to imported operation or service functions.
- Expected failures should use the
Result<T, E>pattern fromsrc/main/lib/result.ts. - Prefer
execFileoverexec. - Treat shell escaping, PTY spawning, SSH commands, and worktree paths as security-sensitive.
- Preserve secret redaction in logging and telemetry code.
Renderer:
- Feature UI lives under
src/renderer/features/<feature>/. - Shared renderer primitives, stores, hooks, commands, PTY, Monaco, modal
infrastructure, and UI live under
src/renderer/lib/. - Renderer RPC calls go through
rpcfromsrc/renderer/lib/ipc.ts. - New modals must be registered in
src/renderer/app/modal-registry.ts. - New views must be registered in
src/renderer/app/view-registry.ts. - New commands should use
src/renderer/lib/commands/registry.tsand view-levelcommandProviderhooks when possible. - Components use
PascalCase; hooks useuseXcamelCase or an existing local pattern.
State and stores:
- Access task managers through
getTaskManagerStore(projectId), notproject.taskManager. - Access mounted projects through
asMounted(getProjectStore(id)). - Never use
asProvisioned(...)!orasMounted(...)!; use explicit null checks. - State guards should check
kind !== 'ready'rather than enumerate non-ready states. - Task selectors live in
src/renderer/features/tasks/stores/task-selectors.ts. - Project selectors live in
src/renderer/features/projects/stores/project-selectors.ts.
Development database paths use Electron app.getPath('userData').
- macOS:
~/Library/Application Support/emdash-dev/emdash4.db - Linux:
~/.config/emdash-dev/emdash4.db - Windows:
%APPDATA%\emdash-dev\emdash4.db
Use an isolated scratch database when working on schema or migration changes. From the repo root:
EMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run devFor app-only development, change into apps/emdash-desktop/ first so this starts
only electron-vite dev:
cd apps/emdash-desktop
EMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run devReset dev databases from apps/emdash-desktop/:
pnpm run db:resetDatabase rules:
- Do not hand-edit numbered Drizzle migrations or
drizzle/meta/. - Use
pnpm run db:generatefor new migrations. - Update fixtures and migration tests when schema behavior changes.
- Run focused database validation from
apps/emdash-desktop/when relevant:
pnpm run db:setup
pnpm run db:fixtures
pnpm run test:migrationsRead agents/risky-areas/database.md before changing database internals.
Emdash orchestrates coding agents in Git worktrees and PTY sessions. These areas are high impact.
- Do not delete worktree folders manually unless you know the matching Git state.
Prefer in-app cleanup or
git worktree prunefrom the main repository. - Do not weaken shell quoting, spawn behavior, environment allowlists, or secret redaction.
- PTY environment passthrough must use the allowlist in
src/main/core/pty/pty-env.ts. - Provider changes may need updates to shared provider metadata, dependency detection, PTY behavior, hooks/plugins, renderer assumptions, and tests.
Read the relevant risk or integration doc before touching these areas:
agents/risky-areas/pty.mdagents/risky-areas/ssh.mdagents/integrations/providers.mdagents/integrations/mcp.md
- Unit tests use Vitest.
- Main database integration tests run in the
main-dbVitest project. - Migration tests run in the
migrationsproject. - Fixture generation runs in the
fixturesproject. - Renderer browser tests use Playwright-backed
@vitest/browser-playwright. - Main-process tests are colocated under
src/main/core/**/*.test.ts. - Renderer unit tests live under
src/renderer/tests/. - Renderer browser tests live under
src/renderer/tests/browser/. - Integration-style tests create temporary repos and worktrees in
os.tmpdir().
From apps/emdash-desktop/, the app test command is:
pnpm run testIt runs the app Vitest projects:
node, main-db, migrations, browser, scripts
After native dependency changes, rebuild Electron native modules from
apps/emdash-desktop/:
pnpm run rebuildThis is especially relevant for better-sqlite3 and node-pty.
When working on Docker-backed SSH development infrastructure, start it from
apps/emdash-desktop/:
pnpm run run:docker-sshRead agents/workflows/remote-development.md and agents/risky-areas/ssh.md
before making SSH behavior changes.
Use GitHub Issues. Include:
- Operating system
- Emdash version or commit SHA
- Node and pnpm versions, if development-related
- Steps to reproduce
- Expected behavior
- Actual behavior
- Relevant logs, terminal output, or screenshots
Do not include secrets, tokens, private keys, local app databases, or private repository content in public issues.
Do not dispatch release workflows, publish packages, or upload artifacts unless you are explicitly doing release work.
The app version lives in apps/emdash-desktop/package.json. For release version
bumps, run these from apps/emdash-desktop/:
pnpm version patch
pnpm version minor
pnpm version majorThis updates package.json and pnpm-lock.yaml, creates a version commit, and
creates a tag.
Production releases are dispatched through GitHub Actions:
gh workflow run release-prod.yml --ref main -f arch=bothCanary releases are dispatched through:
gh workflow run release-canary.yml --ref main -f arch=bothProduction releases publish artifacts to GitHub Releases as the primary update feed and Cloudflare R2 as fallback. Canary releases currently publish to R2 only.
agents/README.mdagents/quickstart.mdagents/architecture/overview.mdagents/architecture/main-process.mdagents/architecture/renderer.mdagents/conventions/ipc.mdagents/conventions/main-patterns.mdagents/conventions/renderer-patterns.mdagents/conventions/typescript.mdagents/workflows/nx.mdagents/workflows/testing.mdagents/workflows/worktrees.md