Development Guide
Prerequisites
- Bun (latest) – Electrobun’s runtime; provides native TypeScript execution and fast package management
- Electrobun installed globally or via the project
Commands
Prefer the bun run script wrappers over calling bunx vitest / bunx vite directly – the wrappers use the workspace-pinned tool versions and avoid silent version drift. Run bun run verify before opening a PR.
bun install # Install all dependencies
bun run dev:hmr # Recommended: Vite HMR + Electrobun running concurrently
bun run dev # Alternative: Electrobun with file watching (restarts on changes)
bun run start # One-shot: Vite build then Electrobun dev (no watching)
bun run hmr # Vite dev server only, port 5173 (desktop package; used internally by dev:hmr)
bun run build:canary # Production build (canary channel)
Tests
bun run test # Full test suite across all workspaces
bun run test:desktop # Desktop package only (faster)
bun run test:file path/to/file.test.ts # Run a single test file
bun run test:typecheck # tsc --noEmit
bun run test:build # Production build (catches import/CSS issues unit tests miss)
bun run test:lint # Biome lint (matches CI)
bun run verify # test + typecheck + build + lint -- the PR-readiness check
The desktop package also provides test:bun (Bun-native event-server tests), test:e2e
(Playwright), and test:a11y (Playwright accessibility). Run these from
packages/desktop (or via bun run --cwd packages/desktop <script>).
Why dev:hmr is Preferred
bun run dev:hmr runs the Vite dev server (port 5173, with Hot Module Replacement) and
Electrobun concurrently. On startup the Bun main process probes the dev server: if it is
up, the app loads from http://localhost:5173; otherwise it falls back to the bundled
views://mainview/index.html. This gives instant CSS/component feedback without
restarting the app.
bun run dev (no HMR) watches files and restarts the Bun process, but does not provide
instant CSS/component updates. Use it when testing main-process changes.
Test Environment
Tests use Vitest with environment-specific configuration (see
packages/desktop/vitest.config.ts):
- Unit tests (
tests/unit/) run in thenodeenvironment by default. - UI tests (
tests/unit/ui/) run injsdomfor DOM access (matched viaenvironmentMatchGlobs). - Benchmarks (
tests/bench/) run innodeviabunx vitest bench(run from the desktop package). @testing-library/reactand@testing-library/jest-domare available for component testing.
The benchmark suite (tests/bench/perf.bench.ts) uses a schema generator
(generateLargeSchema(300)) to build deterministic 300+ node trees and asserts that
updateNodeStatus never changes the dataKey value – the invariant that status
updates must not trigger tree re-layout.
Sample Schemas
samples/hello-world.json– Minimal schema (4 nodes, all 4 statuses)samples/getting-started.json– Rich schema (15 nodes, 4 depth levels, markdown notes, metadata)
In the Vite dev server (HMR mode without Electrobun), the Open button loads
getting-started.json as a fallback. The WelcomeScreen offers links to load both samples directly.
The electrobun/view Unavailability Issue
electrobun/view is injected by the Electrobun runtime and is not a regular npm
package – it does not exist in the Vite dev server (standalone HMR) or in the test
environment (vitest with jsdom). Any code that imports it eagerly will crash there. Use a
lazy import wrapped in try/catch so the app still works in dev/test:
try {
const { electroview } = await import("../rpc");
rpcSend = (payload) => electroview.rpc.request.logMessage(payload);
} catch {
// electrobun/view unavailable outside the runtime -- fall back gracefully
}
Follow this pattern whenever new code uses electroview.
How to Add a New Component
-
Create the component file in
packages/desktop/src/mainview/components/. -
Use only
--rv-*tokens via Tailwind classes – no hardcoded colors (a CI grep check enforces this):// Good <div className="bg-rv-bg-surface text-rv-text-primary border border-rv-border">Content</div> // Bad -- hardcoded colors fail the CI grep check <div className="bg-[#1b1b1c] text-[#e0e0e0]">Content</div> -
If you need a new token, follow Design System – How to Add a New Token.
-
Add the component to the app shell in
App.tsxor the appropriate parent. -
Write tests in
tests/unit/ui/using@testing-library/react(these get thejsdomenvironment automatically).
How to Add a New RPC Endpoint
A three-file change; TypeScript enforces consistency across all three.
-
shared/types.ts– Define the contract:// Inside RoadmapRPCType.bun.requests: myEndpoint: { params: { input: string }; response: { output: number }; }; -
packages/desktop/src/bun/index.ts– Add the handler:// Inside BrowserView.defineRPC handlers.requests: myEndpoint: ({ input }) => ({ output: input.length }), -
Call from webview code:
import { electroview } from "../rpc"; const result = await electroview.rpc.request.myEndpoint({ input: "hello" });
See RPC and IPC for the full contract reference and data flow diagrams.
How to Add a New Logger Category
Create a logger with getLogger() using an array-based category. No extra configuration
is needed – loggers inherit from their parent category.
// Webview side (packages/desktop/src/mainview/logging/logger.ts)
export const myFeatureLogger = getLogger(["webview", "my-feature"]);
// Bun side (packages/desktop/src/bun/logging.ts)
export const myFeatureLogger = getLogger(["bun", "my-feature"]);
See Logging for details.
Keyboard Shortcuts
The canvas keyboard layer lives in
hooks/useKeyboardRouter.ts.
The router runs in capture phase and stands down when a Radix dialog or context menu is
open, or when a text input / CodeMirror editor is focused. File verbs come from
lib/fileCommands.ts — the single
registry (label, shortcut, enablement, action) that the File menu, the keyboard router
and the sidebar all read, so a verb cannot drift between surfaces. Node search matches
titles by default; the notes toggle inside the search box widens it to notes and is
persisted (AppSettings.searchInNotes).
Global / file
| Shortcut | Action |
|---|---|
Ctrl+N |
New roadmap |
Ctrl+O |
Open… |
Ctrl+S |
Save (flush now) |
Ctrl+Shift+S |
Save As… |
Ctrl+B |
Toggle sidebar |
Ctrl+, |
Preferences |
Ctrl+F |
Focus node search |
Ctrl+Shift+L |
Toggle event log |
Ctrl is ⌘ on macOS. Ctrl+S, Ctrl+Shift+S, Ctrl+, and Ctrl+F also fire while
a text input or CodeMirror editor has the caret; Ctrl+N, Ctrl+O, Ctrl+B and
Ctrl+Shift+L respect the input-focused guard.
Canvas (focused node)
| Shortcut | Action |
|---|---|
Arrow ← / Arrow → (TB) |
Move focus between siblings |
Arrow ↑ / Arrow ↓ (LR) |
Move focus between siblings |
Arrow ↓ (TB) / Arrow → (LR) |
Enter first child (descend) |
Arrow ↑ (TB) / Arrow ← (LR) |
Return to parent |
Space |
Select focused node (open / refresh side panel) |
Enter |
Add child + open inline rename on the new node |
Tab |
Add sibling below + open inline rename |
Shift+Enter |
Add sibling above + open inline rename |
F2 |
Inline rename on focused node |
Del / Backspace |
Delete focused node (confirmation dialog if it has children) |
Ctrl+D |
Duplicate focused node + subtree, then open rename on the copy |
Ctrl+C |
Copy node + subtree to clipboard as JSON |
Ctrl+V |
Paste clipboard subtree under focused node |
Ctrl+Arrow ↑ |
Move focused node up among its siblings |
Ctrl+Arrow ↓ |
Move focused node down among its siblings |
Escape |
Cancel inline rename, or deselect node |
F6 |
Toggle focus between canvas and side panel |
Arrow-key axes follow the layout orientation: in TB (top-bottom) layout siblings are
horizontal and children flow downward; in LR (left-right) layout siblings are vertical and
children flow rightward. Ctrl+C / Ctrl+V are context-aware – when a text input,
<textarea>, contentEditable, or CodeMirror (.cm-editor) is focused, they fall through
to the browser’s native text copy/paste instead of the node clipboard.
Side panel
| Shortcut | Action | Context |
|---|---|---|
e |
Enter edit mode | Side panel open, no text input focused, not already editing |
Escape |
Cancel title edit / exit edit mode | Side panel in edit mode |
Edit mode can also be entered by clicking the title field or the [E] pencil button in
the panel header. Node cards are keyboard-accessible (role="button", tabIndex={0},
Enter / Space handlers); the dashed focus ring uses a keyboard-nav-active class on
<body> so it shows only during keyboard navigation, not after a mouse click.
Outline (sidebar)
| Shortcut | Action |
|---|---|
Arrow ↑ / Arrow ↓ |
Move between rows |
Arrow → |
Expand the row, or move to its first child if already expanded |
Arrow ← |
Collapse the row, or move to its parent if already collapsed |
Home / End |
First / last row |
Enter / Space |
Select the node and reveal it on the canvas |
While an outline row holds DOM focus ([data-outline-tree]) the canvas router stands
down for node-navigation keys, so the arrows above act on the outline rather than the
canvas; the Ctrl shortcuts in the Global / file table keep working from there.
Project Conventions
| Convention | Rule | Why |
|---|---|---|
| Token prefix | All CSS custom properties start with --rv- |
Namespace isolation from Tailwind internals and third-party CSS |
| Hardcoded colors | Not allowed in components; CI grep enforces this | Ensures theme switching works for every component |
| RPC contract | Defined once in shared/types.ts, imported by both processes |
Compile-time safety across the process boundary |
| Logging | Use LogTape loggers, not console.log |
Structured output, file persistence, category filtering |
| Test location | tests/unit/ for unit tests, tests/unit/ui/ for component tests |
Environment matching: node for logic, jsdom for components |
| Formatter | Biome (not Prettier) | Faster; linting + formatting in one tool |
| Package scope | @roadraven/ |
Consistent npm namespace for publishable packages |
| App version | Single source is packages/desktop/package.json (read by electrobun.config.ts, src/bun/appVersion.ts and the renderer’s __APP_VERSION__ define); bun scripts/bump-version.ts X.Y.Z propagates it to the other package.json files and the plugin/marketplace pins |
One literal to bump; footer, About and the Setup Wizard mismatch check cannot drift |
Related Documentation
- Architecture Overview – process model, package structure
- Design System – token system, theming, adding themes
- RPC and IPC – typed RPC contract details
- Logging – two-process logging architecture