Contributing

Synced from docs/contributing.mdEdit on GitHub

Setting up

yarn install          # Yarn 4 (the version is pinned in package.json); also downloads Electron for the desktop app
yarn build            # every project through Nx, dependencies first
yarn typecheck
yarn boundaries

Node 22 is what CI runs. ELECTRON_SKIP_BINARY_DOWNLOAD=1 yarn install skips the Electron binary on a machine that won't run the desktop app.

The dev loop

yarn self (or F5, "Orbit Code: this repository") Builds, then opens this repository in an Extension Development Host with apps/vscode as the extension and other extensions disabled. The panel opens by itself
yarn self --reindex The same, ignoring the cached graph. If VS Code is already running, the new window may not see the variable; use Orbit Code: Reindex Workspace
yarn watch Rebuilds the extension, the indexer and the webview on change and copies the builds into apps/vscode/dist/
yarn desktop --folder . Builds and opens this repository in the desktop app (--reindex, --disable-workspace-trust)
yarn nx run desktop:watch The desktop equivalent of yarn watch
yarn harness The webview in headless Chrome against a simulated host; a report and screenshots in .harness/out/
yarn index . --out graph.json The indexer alone

After a rebuild with a watch running: Developer: Restart Extension Host (or a window reload) for the extension bundle; close and reopen the panel, or Developer: Reload Webviews, for the webview bundle (the host resends its snapshot); nothing for the indexer, since each index run starts a new worker, though an extractor change still needs Reindex to show. In the desktop app: restart it for the main process, View › Reload for the page. yarn watch doesn't reload a build.mjs or the indexer's shims; restart it after editing them.

ORBIT_CODE points yarn self at another code CLI, for Cursor or VSCodium.

Where a change goes

To change Edit Then also
A command contributes.commands in apps/vscode/package.json; registerCommand in extension.ts, delegating to the controller or a service The command list in apps/vscode/README.md
A setting contributes.configuration; readSettings() in apps/vscode/src/config.ts; normalizeSettings and applySettings in packages/core/src/settings.ts (both hosts) Machine scope and restrictedConfigurations if it changes what runs; both READMEs' settings tables
A host → page message HostToWebview in packages/protocol/src/protocol.ts; post it from packages/core/src/controller.ts, and in sendSnapshot() if it is state; handle it in main.ts and wake the loop if it changes what is drawn tools/harness/src/hostSim.ts; PROTOCOL_VERSION
A page → host message WebviewToHost; a callback in main.ts; a validated case in OrbitController.onMessage(). What only an editor can do goes through HostUi, which both apps implement receive() in hostSim.ts; PROTOCOL_VERSION
The page's CSP apps/vscode/src/panel/orbitPanel.ts Identically in page() of tools/harness/harness.mjs and contentSecurityPolicy in apps/desktop/src/main/page.ts
A HUD control packages/webview/src/hud/, styles.css, wiring in main.ts The harness's selectors, if a class is renamed
Claude CLI arguments or process handling packages/agent/src/claudeCli.ts; AgentStartOptions in backend.ts sameOptions() in conversation.ts, if a change must restart the process; a real smoke turn
Stream-json parsing packages/agent/src/streamJson.ts (pure; unknown shapes return nothing); onAgentEvent() in conversation.ts A real smoke turn
What a tool call animates or shows packages/agent/src/tools.ts, packages/core/src/sessionProjector.ts The host simulator's scripted turn, if the page must react to something new
A file operation apps/vscode/src/fileActions.ts and apps/desktop/src/main/files.ts together; shapes in packages/core/src/fileHelpers.ts
Status bar, notifications, tray apps/vscode/src/statusBar.ts, apps/desktop/src/main/tray.ts and notifications.ts; the wording in packages/agent/src/sessionSummary.ts is shared
An extractor, live updates, the layout The graph pipeline Reindex to see an extractor change; a full index compared with an incremental run
Anything keyed by node or cluster index in the page World and its layers An adopt for any new animation state
A build, loaders, shims The project's build.mjs; packages/indexer/shims/ Restart the watch

Some rules that follow from the constraints in Architecture: page messages are checked in the controller before a service sees them; text goes into the DOM as textContent; anything that ends up on a command line is validated; a new service is wired in extension.ts and workspace.ts rather than made a dependency of another service; and a service that needs no editor API belongs in packages/core or packages/agent.

The checks

There is no unit-test runner and no linter. The checks are:

Change Run
Anything yarn typecheck, yarn build, yarn boundaries
The manifest, commands, settings, views, activation also yarn smoke
The panel, CSP, HUD, frame loop, protocol, the shared controller also yarn harness (then read the report), yarn smoke and yarn desktop:smoke
The session, CLI arguments, stream-json, tools also ORBIT_SMOKE_PROMPT="Read package.json and reply with its name" ORBIT_SMOKE_MODEL=haiku yarn smoke, a real and paid turn
A build.mjs, the shims, a dependency upgrade also yarn index . --out .harness/check.json, the harness, the smoke, and yarn package
The indexer, live updates, the layout an incremental run's hash against a full index's; the harness's liveUpdate check; the smoke's live-update step
apps/desktop also yarn nx run desktop:build and yarn desktop:smoke; yarn desktop:package after touching packaging

The harness

yarn harness builds the webview, the indexer and the host simulator, indexes this repository (or takes --graph), serves the page under the real CSP, and drives headless Chrome over the DevTools protocol with real mouse and keyboard events. It covers where the view opens, camera motion, zooming into a bubble, drilling in and out, a thought, a live update, the Claude bubble and transcript, the spark, a subagent, two conversations at once, the drawer and the conversation view, the composer's toggles, the file card and the editor sheet, the Flat view, the tour, the frame rate cap, and the loop parking when hidden and idle.

It does not fail on a bad result: it only throws if the scene never loads. Read .harness/out/report.json and the screenshots next to it. A quick summary:

node -e 'const c = require("./.harness/out/report.json").checks, l = c.liveUpdate ?? {}, p = c.promptRoundTrip;
console.log({ fps: c.frameLoop.measuredFps, drill: c.drill?.matchesHover, fileHover: !!c.hoverFile?.tooltip, esc: c.escapeToOverview,
  live: l.appliedInPlace && l.stillDrilledIn && l.newLayoutComputed === 0, thinkingPixels: c.thinking?.litPixels,
  prompt: p.hostReceived && p.inputCleared && p.actionWhileWorking === "Stop",
  hiddenFrames: c.hiddenStopsLoop.framesWhileHidden, idleFrames: c.idleParks.framesWhileIdle, consoleClean: c.consoleClean })'

Expect a frame rate of about 30, every boolean true, zero frames while hidden and while idle, and lit pixels above zero. Chrome is expected at /Applications/Google Chrome.app/…; pass --chrome <path> otherwise. Other options: --graph, --out, --size 1440x900, --scale, --skip-build. Compare a red check with a run on main before calling it a regression: a few checks depend on the graph under test.

The smoke tests

yarn smoke builds the extension, downloads VS Code into .vscode-test/ (ORBIT_VSCODE_VERSION pins a version), opens a real window on this workspace for about a minute with a fresh user-data directory, and exits non-zero on failure. It waits for the graph, the saved layout, the scene and the Claude Code probe; with ORBIT_SMOKE_PROMPT it runs one real turn (keep the prompt read-only, since nobody answers permission requests); it reopens the panel from the activity bar, writes and deletes a file to see the live update, and drives the file requests (read, write, rename, delete) through the API the extension exports. It fails on any error line in Orbit.log, so failures that aren't Orbit bugs are logged as warnings. If it is killed mid-run, delete packages/graph/src/__orbitSmoke*.ts yourself.

yarn desktop:smoke starts the desktop app on a small folder it writes under .harness/desktop-smoke (a git repository of its own, since this one ignores .harness), with its own user data, waits for the scene and the probe, checks the saved layout, writes a file and waits for the watcher's update, then starts the app again and checks the layout is reused. About 5 s.

Both smokes grep specific log lines (scene ready for graph, agent: / agent unavailable:, [webview] layout of, turn done in, graph updated, [webview] graph update applied); don't reword them.

CI

.github/workflows/ci.yml runs on every push to main and pull request, on Ubuntu with Node 22:

  1. check: yarn typecheck, yarn build, yarn boundaries, and an index of this repository that must hold at least 40 files and edges, dependency-cruiser results, and packages/webview/src/main.ts. Its Nx cache is saved for the jobs after it.
  2. In parallel: package (yarn package, the .vsix uploaded as an artifact), smoke under xvfb-run (no claude on the runner, so the probe logs agent unavailable), harness with the runner's Chrome (the report and screenshots uploaded), and desktop (yarn desktop:smoke, then yarn desktop:package --linux AppImage, uploaded).

Only the desktop job downloads Electron. Releasing covers release.yml.

Commits

Commits read as a sequence of small, self-contained steps:

  • One commit per module or subtask. A change touching several areas is split, in the order a reader would want (a shared type before the code that uses it, the host before the page that talks to it). A one-line fix or a rename touching everything is one commit; the rule is about not bundling distinct work.
  • The subject line is the message, under 100 characters, in the repository's own shape: <Area>: <description> when the change sits inside one module (Webview:, HUD:, Indexer:, Session service:, Graph service:, Extension host:, Desktop:, Harness:, README:), or a plain sentence when it doesn't. Lowercase description, no trailing period. Add a body only when the change genuinely needs explaining.
  • No attribution trailers. No Co-Authored-By, no "Generated with" footer.
  • Stage files by name, so an unrelated in-progress file never rides along.

git log --oneline is the reference for the convention.