# Orbit Code documentation > An open source, live 3D alternative to Claude Code in the terminal, for vibecoders and programmers. Synced from https://github.com/imshaikot/orbit-code/tree/97d91eb572fdb4e93f58b4c49af5f430691000f3 --- # Using Orbit This page reads the same whether Orbit runs in VS Code or as the desktop app: the panel is the same bundle in both. Where the two differ, the difference is called out. Installing and configuring each host is in [apps/vscode/README.md](https://orbit-code.imshaikot.com/docs/vscode/) and [apps/desktop/README.md](https://orbit-code.imshaikot.com/docs/desktop/). ## What you are looking at The graph is your workspace after indexing: - **A file is a sphere.** Its colour is its language (the legend at the top left lists the kinds present); its size follows the file's size. - **A directory is a bubble**, inside its parent's bubble. A bubble's colour is the kind most of its files are. A directory that holds nothing but one sub-directory is folded into it, so `src/main/java` shows as one bubble named `main/java`. - **An import is a line** between the two things that contain its ends at the level you are looking at: file to file inside one directory, file to bubble or bubble to bubble across directories. Imports that land on the same pair share one line. - **Claude is a star.** While a turn runs it moves to each file Claude reads or edits. A file being read lights cyan as a comet leaves it; a file being edited pulses amber; while Claude thinks, every import line on screen fires. The view opens in the root's source directory when it has one (`src`, `lib`, `app`, `packages`, …), else at the root. The breadcrumb at the top left says where you are, and each of its parts is a link back up. ## Navigating | To | Do | | --- | --- | | Look inside a directory | Scroll toward its bubble. It comes to the middle of the screen and its contents fade in; once it fills the view, you are inside it. Or click it | | Go back up | Scroll out, or press **Esc**, or click a part of the breadcrumb | | Orbit | Drag. The orbit is free all the way around, over the poles too | | Pan | Right-drag. Panning out of a directory backs out of it | | See what a file is | Hover it: the tooltip gives its path, kind and size, and how many files it imports and is imported by. A bubble's tooltip gives its file count, sub-directories and dominant kind | | Do something with a file | Click it: see [The file card](#the-file-card) | One level up, a directory's own files, lines and sub-bubbles show through its bubble, fainter; two levels up, fainter still, so the import network stays visible from a distance. The directories beside the one you are in stay as faint rims, so the view keeps its place among its neighbours. Only what the directory in view shows takes clicks. ## Views: Nested and Flat The tabs at the top middle switch views. - **Nested** is the default: bubbles inside bubbles, one directory to look into at a time. - **Flat** flies every file out of its directory onto an orbit round the workspace's core. Files are grouped by project (the deepest directory holding a manifest) or by top-level directory, each group along its own arc, in path order; each file is a solid sphere carrying its file type's icon, with its name under it once you are close enough. Imports rise between the spheres as arcs that fire while Claude thinks. Drag to orbit, scroll toward a file to close in on it, click a file for its card. Switching back to Nested returns to the directory you were in. ## Take a Tour **Take a Tour** at the top right hands the camera over. It flies from stop to stop (a directory, then usually one of its files, then a neighbour or another directory), backs out and swings in between stops rather than cutting, and pauses at each. Some stops get a card of what the graph knows about the place: the most imported file in its directory, the largest, an entry point, a file nothing else imports, the imports crossing a directory's boundary. Scrolling, dragging, Esc and the tabs are off until you press **Stop Tour**, which leaves you wherever the tour got to. A tour is random; starting it again begins a new route. ## Asking Claude The tab at the bottom middle opens the drawer. Type a prompt and press Enter, or run **Orbit Code: Ask Claude…** in VS Code. Orbit runs your own `claude` command line, so your Claude Code login, settings, `CLAUDE.md`, hooks and MCP servers all apply, exactly as in a terminal. Claude runs only in a trusted workspace: VS Code's workspace trust, or the desktop app's per-folder trust dialog. The composer's bar holds: - **Files** opens the host's file dialog; the files chosen become chips and go with the prompt as `@path` mentions, which Claude Code reads into the turn itself. - **Skills** opens a panel of the skills Claude Code offers (the workspace's `.claude/skills`, your own, and installed plugins'). Drag one onto the composer, click it, or type `/` in the composer to filter them; the first attached skill is invoked as a slash command, the rest are named for Claude to load. - **The model** picker lists the models Claude Code reports; blank is Claude Code's default. - **The effort meter** is a ring for the default and a bar per level the chosen model offers. Haiku offers none, so the meter dims for it. - **The permission mode**: default (your settings decide, and anything gated is asked in the panel), accept edits, or plan. Bypass can only be set in the host's settings, never here. - **History** opens a timeline of the workspace's earlier conversations, from Claude Code's own transcripts. Narrow the time range with the slider, click a conversation to read what it touched, and **Continue this conversation**: the next prompt resumes it. - **View conversation** and **New conversation**, above the composer, open the current conversation's transcript and make the next prompt start a fresh one. - **MCP** (top right of the drawer) opens the MCP view, described below. A sent prompt flies into a **Claude bubble** at the bottom left, one per running conversation. The bubble shows what Claude is doing; a click opens the conversation. ### Several conversations A prompt sent from the drawer while the current conversation is busy starts another one beside it, with its own process, bubble and star. Each bubble opens its own transcript. Up to eight conversations are kept; when another starts, the oldest idle one that is not current is let go. **Orbit Code: Stop Claude** (or Stop in a conversation's view) interrupts. ### The conversation view A click on a bubble opens the transcript: your prompts, Claude's replies as Markdown, each tool call with the file it touched linked (a link opens the file in an editor tab in VS Code, in the editor sheet in the desktop app), and a line per finished turn with its duration and list-price cost. The composer at the bottom has the same controls as the drawer, so a follow-up can switch model or bring skills and files. While the turn runs, **Stop** stands in for Send; you can write the next prompt meanwhile, and it goes once the turn ends. Esc closes the view. ### Permissions and questions When Claude asks to use a gated tool, a card appears in the conversation view with **Allow**, the "don't ask again" choice Claude Code offers for that request (allow all edits this session, always allow this command in this project), and **Deny**. The bubble turns to "waiting" meanwhile. When Claude asks you questions instead, the card lists them: pick the options (or several, when the question allows) or type your own answer, then **Answer**, or **Skip** them. With the panel out of sight, a request becomes a notification: in VS Code with the same buttons, in the desktop app with buttons on macOS and a click that reveals the window elsewhere. A question can't be answered from a notification, which offers to open Orbit or skip it. ### Following the star Click Claude's star and choose **Follow Spark**: the camera eases to it and keeps it in the middle of the screen as it moves, while your own drags and scrolls still work. Click the star again for **Stop Following**. Following pauses while a directory move animates, and drops once the star is gone. ### Subagents When Claude hands work to a subagent (a Task call), a smaller star comes out of Claude's, waits beside it joined by a faint line, and moves over the files the subagent reads or edits, with comets and MCP stations of its own. Click it for the subagent's output as it comes; that popup stays up after the subagent has finished and its star has gone back. The conversation view shows only the Task call itself. ### MCP servers A call to an MCP server's tool brings a station out beside Claude's star, joined to it by a beam along which the call and the answer run; stations leave once the turn ends. The MCP view (the drawer's MCP button, which wears the state most in need of attention) shows every MCP server Claude Code loads as a glyph coloured by its status: connected, connecting, needs sign-in, failed, disabled. A click offers what `/mcp` would: reconnect, enable or disable, sign in or out. A toggle is saved in your Claude Code settings; a sign-in opens the server's page in your browser. **Reload** asks every server again. Each conversation picks up a change at its next prompt, not during a turn; a claude.ai connector authorised on claude.ai needs Reconnect. ## The file card A click on a file opens a card beside it and rings the file: | Action | Does | | --- | --- | | **View diff** | The file against `HEAD`, offered only when git says it has changes. VS Code opens a diff editor; the desktop app opens the editor sheet's Changes mode | | **Open** | The editor sheet: a CodeMirror editor at the bottom of the panel with the file's language, Cmd/Ctrl+S to save, a Changes mode with the diff against `HEAD`, and edits made elsewhere taken in as long as you haven't changed anything. Files over 4 MB and binary files don't open there | | **Open in a tab** | An editor tab beside the panel. VS Code only: the desktop app has no tabs and hides this item | | **Attach to prompt** | Opens the drawer with the file as a chip | | **Rename…** | The name is selected without its extension. A file renamed within its directory keeps its place in the graph | | **Delete…** | The button arms after a moment; the file collapses inside a red ring and the next update removes it. VS Code deletes as a workspace edit (to the trash when `files.enableTrash` is on); the desktop app only ever moves to the trash | Esc closes the card without moving the camera. ## Live updates The graph follows your edits, Claude's edits and git checkouts within a few seconds, without laying anything out again: kept files keep their position, a new file is placed in its directory's bubble near what it imports, a new directory gets a bubble inside its parent, and a file Claude writes pulses when it arrives with the star moving to it. Because updates re-read only the files a change affects, a few imports can go stale; **Reindex** (in the legend at the top left, or **Orbit Code: Reindex Workspace**) indexes everything again and lays it out from scratch. ## Around the panel - **VS Code.** The Orbit icon in the activity bar opens the panel (and hands the side bar back to the Explorer). While the panel is hidden and a turn runs, the status bar shows how many run or wait for approval; a click opens the panel. Settings are the `orbit.*` keys. - **Desktop app.** One window per folder; File › Open Folder… and Open Recent. The tray menu lists open folders and stops running turns; the dock (macOS) and launcher (Linux) badge counts running turns. Settings are `settings.json` in the app's user data (File › Settings…), never read from a folder. ## Limits worth knowing - Only the first workspace folder is indexed, and it is the session's working directory. - Conversations live in the host process: reloading the VS Code window or quitting the desktop app starts new ones. History continues earlier ones, without replaying their transcript into the view. - View diff compares with `HEAD`, not with a branch's merge base. - A rename made in the Explorer or by git reaches Orbit as a delete and a create, so the file is placed again; only renames made from the card keep a file's place. - The Flat layout is computed in the page and not cached, so a reload can place files a live update added elsewhere. Page: https://orbit-code.imshaikot.com/docs/using-orbit/ --- # VS Code extension An open-source, live 3D visual alternative to Claude Code in the terminal, for programmers and vibe coders alike: your codebase as a dependency graph in VS Code, with Claude working through it as you watch. Directories are bubbles, files are nodes, imports are lines. Files Claude reads light up cyan, files it edits pulse amber, and the import network fires while it thinks. ## Features - **Dependency graph for most languages.** JS/TS, Python, Go, Rust, Java, C#, Swift, PHP, Dart and more. Imports resolve through each project's own config (`tsconfig`, `go.mod`, `Cargo.toml`, `pyproject.toml`, …), so monorepos work too. - **Live updates.** The graph follows your edits, Claude's edits and git checkouts within seconds, and keeps the layout in place. - **Nested or Flat.** The tabs at the top switch views. Nested shows directories as bubbles inside bubbles, one to look into at a time. Flat puts every file on an orbit around the workspace, each project or top-level directory along its own arc, each file a sphere with its file type's icon and its name, with the imports arching between them. - **Take a Tour.** The button at the top right flies the camera from place to place round the workspace on its own, directory to file to directory, backing out and swinging in between, and pauses a moment at each, now and then with a card of what the graph knows about it: the most imported file in its directory, the largest, an entry point, a directory nothing outside imports from. Navigation is off until you press Stop Tour. - **Claude in the panel.** Prompt from the bottom drawer, pick model, effort and permission mode, and run several conversations at once. A conversation's own window has the same controls, so a follow-up can switch model or bring skills and files, and you can write it while Claude is still working. - **Permissions.** Allow, Deny or "don't ask again" from a card in the panel, or from a notification when Orbit is hidden. When Claude asks you questions, pick its options or type your own answer on the same card, or skip them. - **Context.** Attach skills (type `/` or drag them in), attach files, or continue an earlier conversation. - **MCP servers.** The MCP button at the top right of the drawer shows every MCP server Claude Code loads, and whether it is connected, needs sign-in, failed or is disabled. Reload them, or sign in, reconnect, enable or disable one; the change is saved in your Claude Code settings, as `/mcp` saves it. - **File actions.** Click a file to view its diff, edit it inline, rename it, delete it or attach it to a prompt. - **Follow Spark.** Click Claude's star and the camera follows it. - **Subagents.** When Claude hands work to a subagent, a smaller star comes out of Claude's and moves over the files the subagent reads and edits. Click it to read the subagent's output as it comes. ## Requirements - VS Code 1.100+ - [Claude Code](https://docs.claude.com/en/docs/claude-code), installed and logged in. Without it, you still get the graph. ## Install ```sh yarn install yarn install-local ``` Reload VS Code, then click the Orbit icon in the activity bar or run **Orbit Code: Open**. **Navigating:** scroll over a bubble to zoom into it (it comes to the middle of the screen and opens once it fills the view), click a bubble to enter it, drag to orbit all the way around, right-drag to pan (panning out of a directory backs out of it), and press Esc to go back up. The directories beside the one you are in stay as faint rims. In the Flat view, drag to orbit, scroll toward any file to zoom, and click a file as in the Nested view; switching back returns to the directory you were in. **Take a Tour** (top right) hands the camera over: it flies from stop to stop until you press **Stop Tour**, which leaves you wherever it got to. ## Commands | Command | | | --- | --- | | Orbit Code: Open | Open the graph | | Orbit Code: Ask Claude… | Send a prompt | | Orbit Code: Stop Claude | Stop every running turn | | Orbit Code: New Claude Conversation | Start fresh | | Orbit Code: Reindex Workspace | Index everything again and lay it out from scratch | ## Settings | Setting | Default | | | --- | --- | --- | | `orbit.claude.path` | `""` | Path to `claude`. If empty, Orbit searches `PATH` and the usual install locations | | `orbit.claude.model` | `""` | Model, e.g. `opus` or `sonnet` | | `orbit.claude.effort` | `""` | `low`, `medium`, `high`, `xhigh` or `max` | | `orbit.claude.permissionMode` | `default` | `default`, `acceptEdits`, `plan` or `bypassPermissions` | | `orbit.claude.extraArgs` | `[]` | Extra arguments for every `claude` run | | `orbit.maxFiles` | `20000` | The most files Orbit indexes | Claude only runs in trusted workspaces. Your own Claude Code settings, `CLAUDE.md` and MCP servers apply. ## Development The extension is `apps/vscode` in the Orbit Code monorepo, built from its packages with Nx. From the repository root: ```sh yarn self # build and open the repository in an Extension Development Host (or press F5) yarn watch # rebuild on change yarn typecheck yarn harness # webview checks in headless Chrome yarn smoke # end-to-end in a real VS Code yarn package # a production .vsix in dist/apps/vscode/ ``` CI (`.github/workflows/ci.yml`) runs typecheck, build, the package boundary check and an index of the repository on every push to `main` and pull request, then packages a `.vsix` and runs smoke and harness; the harness report and screenshots are an artifact to read. Releases come from Nx Release: a `v` tag attaches the `.vsix` to a GitHub release, and publishes it to the Marketplace and Open VSX once their tokens are set. The longer documentation is in the repository's `docs/` directory: how to use the view, the architecture, the graph pipeline, how Claude Code is run, contributing, releasing and troubleshooting. ## Limitations - Only the first workspace folder is indexed. - Reloading the window ends your conversations. - Live updates are incremental, so a few imports can go stale. Reindex fixes them. - Enabling, disabling or signing in to an MCP server reaches each conversation at its next prompt, not during a turn. A claude.ai connector needs Reconnect after you connect it on claude.ai. ## License MIT Page: https://orbit-code.imshaikot.com/docs/vscode/ --- # Desktop app Orbit Code as a desktop app: an open-source, live 3D visual alternative to Claude Code in the terminal, for programmers and vibe coders alike, in a window of its own. It shows a folder as a 3D dependency graph and animates a live Claude Code session moving through it, the same way the VS Code extension does. It runs the engine the extension runs (`@orbit-code/core`, `@orbit-code/agent`) and the same UI bundle (`@orbit-code/webview`), hosted by Electron instead of VS Code. ## Requirements - Claude Code installed and signed in (`claude --version` works in a terminal). Orbit starts your own `claude` CLI, so your Claude Code settings, skills and MCP servers apply. - git, for the file card's View diff and the editor sheet's Changes. Without it the graph still indexes, by walking the folder. ## Run it from source ```sh yarn install # also downloads Electron yarn desktop --folder . # builds, then opens this repository yarn desktop --folder ../other --reindex ``` A relative `--folder` is taken from the workspace root. With no folder, the app reopens the folders that were open when it last quit, or shows an empty window. ## Using it - **Opening a folder.** File › Open Folder… (Cmd/Ctrl+O) or File › Open Recent. Each folder gets its own window. Opening a folder that is already open brings its window forward. - **Trust.** The first time a folder opens, Orbit asks whether you trust its authors, because Claude Code runs there with the settings, hooks and MCP servers the folder configures. The graph works either way. Claude sessions start once the folder is trusted (File › Trust Folder). - **Files.** A click on a file opens its card: View diff, Open, Attach to prompt, Rename, Delete. Open and View diff use the editor sheet at the bottom of the window. There are no editor tabs, so links in the transcript open there too. Delete moves the file to the system's trash, and fails rather than deleting a file for good. - **While the window is out of sight.** A permission request or a question from Claude becomes a system notification. On macOS its buttons answer it. Everywhere, a click brings the window forward. The tray icon's menu lists open folders and stops running turns. The dock (macOS) and launcher (Linux) show how many turns run, or how many wait for approval first. ## Settings `settings.json` in the app's user data (File › Settings… opens it). The keys match the extension's `orbit.*` settings, and changes apply as soon as the file is saved. | Key | Default | What it does | | --- | --- | --- | | `maxFiles` | `20000` | The most files indexed (at least 100) | | `claude.path` | `""` | The `claude` executable; empty looks in PATH and the usual install locations | | `claude.model` | `""` | Model alias or id; empty is Claude Code's default | | `claude.effort` | `""` | `low`, `medium`, `high`, `xhigh` or `max`; empty is Claude Code's default | | `claude.permissionMode` | `"default"` | `default`, `acceptEdits`, `plan` or `bypassPermissions` | | `claude.extraArgs` | `[]` | Appended to every `claude` invocation | Settings are never read from an opened folder, so a repository can't choose the executable, its arguments or the permission mode. `bypassPermissions` is accepted only from this file, never from the window. ## Where it keeps things The user data directory is `~/Library/Application Support/Orbit Code` on macOS, `%APPDATA%\Orbit Code` on Windows and `~/.config/Orbit Code` on Linux. A run from source uses `Orbit Code Dev` instead, so it never shares state with an installed copy. | Path | Holds | | --- | --- | | `settings.json` | Settings | | `recent.json` | Recent folders, and the ones open at the last quit | | `trust.json` | Trusted folders | | `workspaces//orbit-v1/` | Each folder's `graph.json` and up to 4 `layout-.bin`, as the extension keeps them | | `logs/orbit.log` | The log, with the lines the extension writes to Orbit.log (File › Show Log) | ## Development ```sh yarn nx run desktop:build # dist/: main.js, preload.js, and the indexer and webview builds copied in yarn nx run desktop:watch # rebuilds as sources change; restart the app, or View › Reload for the page yarn nx run desktop:typecheck yarn desktop:smoke # launches the app on a small folder under .harness/desktop-smoke ``` The main process is `src/main/`: | Module | Owns | | --- | --- | | `main.ts` | App lifecycle, a window per folder, the menu, trust, recent folders, launch arguments | | `workspace.ts` | One folder's engine: GraphService, SessionService and the shared OrbitController, with the desktop's dialogs and notifications as its `HostUi` | | `window.ts` | The BrowserWindow as the controller's transport: `ready`, visibility, and messages only from its own top frame | | `page.ts` | The `orbit://app/` scheme: the page with a fresh nonce and the panel's CSP, and `webview.js` | | `files.ts` | The file menu and editor sheet's requests, on the disk | | `watcher.ts` | A recursive `fs.watch` feeding ChangeBatcher, for live updates | | `settings.ts`, `recent.ts`, `trust.ts` | The JSON files in user data | | `notifications.ts`, `tray.ts` | Permission notifications, the tray and the dock count | | `shellPath.ts` | The login shell's PATH, for an app started from the Finder or a launcher | `src/preload/preload.ts` exposes `window.orbitHost.postMessage` to the page and delivers the host's messages as window `message` events, which is the contract `packages/webview/src/host.ts` has with any host but VS Code. The window is sandboxed, with context isolation and no Node integration. Environment variables: `ORBIT_DESKTOP_FOLDER` (a folder to open), `ORBIT_REINDEX=1`, `ORBIT_DESKTOP_USER_DATA`, `ORBIT_DESKTOP_LOG`, `ORBIT_DESKTOP_SOFTWARE_GL=1` (WebGL without a GPU). `--disable-workspace-trust` trusts every folder, as it does for VS Code. Everything the app runs is bundled into `dist/`, so `package.json` lists the workspace packages as devDependencies: electron-builder would otherwise copy them into the package. [docs/hosts.md](https://orbit-code.imshaikot.com/docs/hosts/) describes what the desktop app provides next to what the VS Code extension does, and [docs/using-orbit.md](https://orbit-code.imshaikot.com/docs/using-orbit/) how to use the view. ## Packaging and releasing ```sh yarn desktop:package # a production build, then electron-builder into dist/apps/desktop yarn desktop:package --linux AppImage # arguments go to electron-builder ``` This makes a dmg and zip on macOS, an NSIS installer on Windows and an AppImage on Linux (`electron-builder.yml`). electron-builder is pinned in `scripts/builder.mjs` and fetched with `yarn dlx`. Electron is pinned in `package.json` at 44.3.0, which runs Node 24.20.0. The desktop app is released with the VS Code extension: one version, one `v` tag. To release, add a version plan with the change (`yarn nx release plan minor --groups=apps -m "…"`), then run `yarn nx release --skip-publish` and push the tag. `.github/workflows/release.yml` builds a dmg for Apple silicon and one for Intel on macOS and puts them in the GitHub release beside the extension's `.vsix`, with that version's changelog. Windows and Linux installers aren't released yet; `yarn desktop:package` makes them on those platforms. [docs/releasing.md](https://orbit-code.imshaikot.com/docs/releasing/) has the whole procedure. The builds are not signed yet. macOS gets an ad hoc signature, so it runs on Apple silicon, but Gatekeeper asks before the first launch. Windows SmartScreen warns too. Signing needs, as follow-ups: - **macOS:** a Developer ID Application certificate (`CSC_LINK`, `CSC_KEY_PASSWORD`), `hardenedRuntime: true` and notarization (`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) in electron-builder.yml and the release job. Notifications from an unsigned build may not show on macOS. - **Windows:** a code signing certificate for electron-builder's `win` signing options. ## Limits - One folder per window; a multi-root workspace isn't supported. - Live updates watch the folder with a recursive `fs.watch`. On Linux that sets a watch on every directory, `node_modules` included, and a very large folder can run out of inotify watches. The log then warns, and changes made outside Orbit show after Reindex. - The editor sheet picks a file's language by its name; files over 4 MB, and binary files, don't open. - Notification buttons are macOS only. Windows shows no count on the taskbar. - Conversations live in the app, so quitting starts new ones; History continues earlier ones. Page: https://orbit-code.imshaikot.com/docs/desktop/ --- # Settings and commands Every command and setting the VS Code extension adds, read from its manifest (version 0.2.1). The desktop app reads the same settings from its own `settings.json`, written without the `orbit.` prefix. ## Commands Run these from the Command Palette. | Command | ID | | --- | --- | | Orbit Code: Open | `orbit.open` | | Orbit Code: Ask Claude… | `orbit.prompt` | | Orbit Code: Stop Claude | `orbit.interrupt` | | Orbit Code: New Claude Conversation | `orbit.newSession` | | Orbit Code: Reindex Workspace | `orbit.reindex` | ## Settings ### `orbit.maxFiles` Maximum number of source files to index. Larger workspaces are truncated. Type `number`, default `20000`, at least `100`. ### `orbit.claude.path` Path to the `claude` executable. Empty searches `PATH` and the default install locations. Type `string`, default `""`, set per machine, never by a workspace. ### `orbit.claude.model` Model alias or id passed to Claude Code as `--model`, for example `opus` or `sonnet`. Empty uses Claude Code's own default. The picker in the Orbit Code panel overrides this until the window reloads. Type `string`, default `""`. ### `orbit.claude.effort` Effort level passed to Claude Code as `--effort`: how much Claude thinks before answering. Empty uses Claude Code's own default. A model that takes no effort level, such as Haiku, ignores it. The effort meter in the Orbit Code panel overrides this until the window reloads. Type `string`, default `""`, one of ``, `low`, `medium`, `high`, `xhigh`, `max`. ### `orbit.claude.permissionMode` Permission mode for new Claude sessions. Bypass can only be chosen here, never from the panel. Type `string`, default `"default"`, one of `default`, `acceptEdits`, `plan`, `bypassPermissions`, set per machine, never by a workspace. ### `orbit.claude.extraArgs` Extra arguments appended to every `claude` invocation, for example `["--add-dir", "../shared"]`. Type `array`, default `[]`, set per machine, never by a workspace. Page: https://orbit-code.imshaikot.com/docs/settings/ --- # Indexer CLI Indexes a workspace into the dependency graph [Orbit Code](https://github.com/imshaikot/orbit-code) draws: files are nodes, imports are edges, and directories group them. - **JS and TS** through dependency-cruiser, once per `tsconfig`/`jsconfig` so each package's path aliases apply, with workspace packages, `exports` and `imports` maps resolved too. - **Other languages** (Python, Go, Rust, Java, C#, Swift, PHP, Dart and more) through import scans that resolve with each project's own manifests: `go.mod`, `Cargo.toml`, `pyproject.toml`, `composer.json`, `pubspec.yaml`, `.csproj`, `Package.swift`. - **Configuration files** linked to the files and projects they name. Imports resolve only against files in the graph, never against the disk. The bundle carries dependency-cruiser and TypeScript, so nothing else needs installing. ## CLI ```sh npx @orbit-code/indexer --out graph.json [--max N] npx @orbit-code/indexer --out next.json --previous graph.json # incremental, as a live update runs ``` Files come from `git ls-files` in a work tree (so `.gitignore` applies), otherwise from a walk of the directory. The graph's hash covers file ids, sizes and edges, so an incremental run and a full index of the same files agree. ## As a worker thread Orbit's editor hosts run `dist/indexer.mjs` on a `worker_threads` worker: `workerData` is an `IndexerRequest` and the messages back are `IndexerResponse`s, both defined in `@orbit-code/protocol`. ## License MIT Page: https://orbit-code.imshaikot.com/docs/indexer/ --- # Claude in Orbit Orbit embeds no model. It runs the user's own `claude` command line as a long-lived process that speaks stream-json, and turns what Claude does into three streams: **session state** (phase, permission request, cost), a **transcript**, and **per-file activity** on the graph. The page draws them as Claude's star and comets in the scene, and as bubbles, a conversation view and a permission card in the HUD. Two promises hold throughout: **what lights up is what Claude asked to touch, on the file it named**, and **every question Claude asks reaches the user**, even with Orbit hidden. The code is `packages/agent` (the session and the CLI) and `packages/core` (the projector and the controller); the shapes were last checked against Claude Code 2.1.267. ## The process ``` claude --print --input-format stream-json --output-format stream-json --verbose --permission-prompt-tool stdio [--model M] [--effort E] [--permission-mode P] [--resume ] [...claude.extraArgs] ``` - Started in the first workspace folder, with `ELECTRON_RUN_AS_NODE` removed from the environment, through no shell (except `.cmd`/`.bat` on Windows). - **One process serves many turns** of one conversation: each prompt is one `{"type":"user"}` line on stdin. - **Option changes apply to the next prompt.** Changing the model, effort or permission mode restarts the process with `--resume `, so the conversation continues. A process that died while idle restarts the same way. - **The probe.** Before anything, the service runs `claude --version` on the configured path, or on `claude` from `PATH` plus `~/.local/bin`, `~/.claude/local`, `/opt/homebrew/bin` and `/usr/local/bin`, because editors started from the GUI often lack the shell's `PATH`. It logs `agent: ` or `agent unavailable: `; without it, the graph still works. - **Trust.** Sessions stay unavailable until the workspace is trusted (VS Code's workspace trust; the desktop app's per-folder dialog). - **Without `--permission-prompt-tool stdio`** Claude Code denies gated tools outright instead of asking; the flag is what makes the permission card possible. ## Conversations `SessionService` keeps a `Conversation` per key (`c1`, `c2`, …), each with its own process, and one of them is **current**: the one the drawer and **Orbit Code: Ask Claude…** continue. - A prompt without a key goes to the current conversation if it is idle, else opens a new one beside it. A prompt with a key continues that conversation, and is refused while it is busy. Whichever takes the prompt becomes current, and so does one started by **New conversation** or continued from History. - Only the current conversation keeps its process warm between turns; a former current lets its idle process go and restarts with `--resume` when prompted again. - Past eight conversations, the oldest idle non-current one is let go. - **Stop** interrupts one; **Orbit Code: Stop Claude** interrupts every running one. The page gets a `sessions` snapshot (every conversation's state and the current key) whenever the set changes, and `session`, `transcript` and `activity` messages carrying the conversation's key. ## What Claude does, and what Orbit shows | Claude | Transcript | Scene | HUD | | --- | --- | --- | --- | | Starts a turn | A prompt entry opens a turn | Edges flow faster for the whole turn, and the frame loop stays awake | The text flies into a new bubble | | Thinks | | Every import line on screen fires violet | "Thinking" | | Writes | Markdown, rendered as DOM nodes; a link without a scheme opens a workspace file | | "Writing a reply" | | Reads a graph file (`Read`) | A tool entry linked to the file | The star moves over the file; a comet leaves it; the file and its bubbles light cyan once the comet lands | "Reading path" | | Edits a graph file (`Edit`, `MultiEdit`, `Write`, `NotebookEdit`) | Linked | The star moves over the file, which pulses amber; its neighbours at half strength | "Editing path" | | Creates a source file | Linked | Nothing until the live update adds it; then it glows like an edit with the star moving to it | | | Touches any other path | Unlinked | Nothing | | | Calls an MCP tool (`mcp__server__tool`) | An entry naming the server and tool | The server's station comes out beside the star; a pulse runs out along the beam and the answer back, red on error | "Asking server" | | Uses another tool (Bash, Grep, Glob, Task, WebFetch, …) | The tool and its command, pattern, URL or description | Nothing | The tool name and detail | | A tool fails | A warning notice | | | | Asks permission or questions | | | The bubble waits; a card in the conversation view; a notification when the page is out of sight | | Runs a subagent (`Task`) | The Task call; the subagent's own entries are kept apart | A smaller star comes out of Claude's, moves over the subagent's files, and goes back when it finishes | Its star's popup shows its output | | Finishes | A turn line with duration and list-price cost | Glows fade, the star goes home | The bubble says how it went and leaves | | Is stopped | "Stopped" | As a finish | "Stopping" | | Its process dies | An error notice and a failed turn, with the last stderr line | As a finish | A failed bubble, which stays until opened | Tool paths (absolute, or relative to the workspace) are resolved against the graph by `SessionProjector`; a path outside the graph animates nothing and appears in the transcript without a link. Activity is batched per 100 ms tick and carries the graph's hash. The projector keeps the last 400 transcript entries per conversation, and 400 more of its subagents'. ## Permissions A gated tool arrives as a `can_use_tool` control request, with the `permission_suggestions` Claude Code would apply for "don't ask again": switching the session to accept edits for an edit, an allow rule for a command. Orbit words them as the card's middle choice. - **Allow** answers with the request's own input echoed back. - The **always** choice sends those suggestions back as `updatedPermissions`, so Claude Code applies them itself; Orbit invents no rules of its own. - **Deny** carries a message. - An interrupt denies a pending request first. - Permission modes: `default` (the user's own settings decide), `acceptEdits`, `plan`, and `bypassPermissions`, which only a host setting can choose; the page never offers it and the controller refuses it. ### Questions An `AskUserQuestion` call arrives the same way. Its questions become a question card in place of the permission card: options as radios (or checkboxes when the question allows several) and a field for the user's own answer. The answer requires one per question and is sent as the tool's `answers` input, as Claude Code's own dialog does; **Skip** is a deny saying the questions were skipped. A notification can't hold the answers, so it offers to open Orbit or skip. ## Interrupt, cost, exits - An interrupt is an `interrupt` control request; the turn then ends with a result whose reason is an abort. If no result arrives within 5 s, the process is stopped (SIGTERM, then SIGKILL 3 s later) and the turn counts as interrupted. - `total_cost_usd` is cumulative per process, so the service subtracts the previous value and starts from zero with each new process. - A process that dies during a turn is logged as a warning (it isn't an Orbit bug), with a failed turn and a notice. Idle and non-zero: a warning, and the next prompt resumes the conversation. ## The catalog After a successful probe, in a trusted workspace with a folder open, the backend starts a short-lived process with the same arguments plus `--no-session-persistence`, sends it only the `initialize` and `mcp_status` control requests, and ends it. - `initialize` gives the models (with the default marked) and their effort levels (none for a model such as Haiku), and the commands; the skill catalog keeps the commands that are skills on disk (the workspace's `.claude/skills`, `~/.claude/skills`, installed plugins) with the skills each `SKILL.md` names. - `mcp_status` gives each server's name, status, scope, tool count and names, transport, version and a scrubbed error. The answer also carries the server's configuration, credentials included, and `initialize` the account: only the declared fields leave the parser and neither answer is logged. - Each session's `init` message updates the MCP statuses. ### The MCP view **Reload** replaces the service's control process (the catalog's arguments, no prompt, let go after five minutes unused) and asks `mcp_status` again while any server is pending. A click on a server offers reconnect, enable or disable, sign in or sign out; each is accepted only for a server the catalog lists and an action in the fixed set, and becomes the matching control request on that process. A toggle is saved in the user's Claude Code settings, as `/mcp` saves it. A sign-in's URL (http or https only) is opened by the host in the system browser and never reaches the page; for an OAuth server the process then waits for the callback while the service polls until it connects. Every action but reconnect makes each conversation start a new process at its next prompt, which loads the change. ## Skills, files, history - **Skills.** A prompt can carry up to eight catalog skill names; any other is dropped. The first is sent as a leading slash command (`/name text`) and the rest are named in the prompt for Claude to load. Built-in skills are left out of the panel, because `initialize` doesn't tell them from built-in commands. - **Files.** A prompt can carry up to twenty files as context: graph files, and absolute paths the host's own file dialog returned; anything else the page names is dropped. They are appended as `@path` mentions (`@"a b.md"` with whitespace), Claude Code's own syntax, so Claude reads them into the turn itself rather than through a tool call Orbit is asked to allow. - **History.** `ConversationHistory` summarises the workspace's earlier conversations from Claude Code's transcripts under its config directory (`projects/`), cached by size and mtime: title, prompts, model, branch, files touched, MCP servers and skills. Continuing one is accepted only for an id in the last list sent, because it becomes `--resume `: the conversation already holding that id becomes current, else a fresh one takes it. The earlier transcript isn't replayed into the view. ## Subagents A subagent's envelopes carry the id of the Task call running it. The conversation announces it on its first envelope (with the subagent's type and description) and ends it when that call's result arrives or the turn ends first. Its reads, edits and MCP calls are sent as activity with the subagent's id, and its transcript entries are kept in a history of their own, so a busy subagent doesn't push the conversation's entries out and the conversation view shows only the Task call. A subagent that sends nothing before its call returns gets no star. ## Settings that reach the process | VS Code | Desktop `settings.json` | Effect | | --- | --- | --- | | `orbit.claude.path` | `claude.path` | The executable; empty searches `PATH` and the usual locations. A change probes again | | `orbit.claude.model` | `claude.model` | `--model`; checked as a model name. Applies from the next prompt | | `orbit.claude.effort` | `claude.effort` | `--effort`, one of `low`, `medium`, `high`, `xhigh`, `max`; empty passes none. Applies from the next prompt | | `orbit.claude.permissionMode` | `claude.permissionMode` | `--permission-mode` unless `default`. Applies from the next prompt | | `orbit.claude.extraArgs` | `claude.extraArgs` | Appended to every invocation, for example `["--add-dir", "../shared"]`. A change probes again | The path, the extra arguments and the permission mode change what runs and with which permissions, so VS Code scopes them to the machine and restricts them in untrusted workspaces, and the desktop app reads them only from user data. ## Another backend `SessionBackend` and `AgentProcess` in `packages/agent/src/backend.ts` are the seam. Another backend (the Agent SDK, a remote runner) implements them, emits the same `AgentEvent`s, and calls the sink's exit exactly once, including after disposal; nothing above the service would change. Page: https://orbit-code.imshaikot.com/docs/claude-session/ --- # Troubleshooting ## Where to look **The log.** Both hosts write the same lines. | Host | Where | | --- | --- | | VS Code | View › Output › **Orbit**. On disk: macOS `~/Library/Application Support/Code/logs//window/exthost/imshaikot.orbit-code/Orbit.log`; Linux `~/.config/Code/logs/…`; Windows `%APPDATA%\Code\logs\…` | | Desktop app | File › Show Log, or `/logs/orbit.log` (rotated to `.1` past 5 MB); also stderr when started from a terminal | | The smoke test | `.harness/vscode-user/logs/` | Lines to know: `agent: ` or `agent unavailable: ` (the Claude Code probe), `scene ready for graph `, `[webview] layout of …` (a layout worker run), `graph updated : +A −R ~C files, E edges in N ms` (a live update), `[webview] graph update applied`, `turn done in …`. Webview errors arrive prefixed `[webview]`. **The storage.** `orbit-v1/` holds `graph.json` and up to four `layout-.bin` files. | Host | Where | | --- | --- | | VS Code (macOS) | `~/Library/Application Support/Code/User/workspaceStorage//imshaikot.orbit-code/orbit-v1/` | | Desktop app | `~/Library/Application Support/Orbit Code/workspaces//orbit-v1/` on macOS; `%APPDATA%\Orbit Code\…` on Windows; `~/.config/Orbit Code/…` on Linux. A run from source uses `Orbit Code Dev` | **Reindex** (in the legend, or **Orbit Code: Reindex Workspace**) rebuilds both; deleting the folder is the manual reset. **The page.** **Developer: Open Webview Developer Tools** in VS Code, View › Toggle Developer Tools in the desktop app. `window.__orbit` has `debug`, `world()`, `camera()` and the rest ([The webview](https://orbit-code.imshaikot.com/docs/webview/)). ## Claude - **"agent unavailable"** in the log, or the drawer says Claude isn't available. `claude --version` must work; Orbit looks on the configured path, then `PATH` plus `~/.local/bin`, `~/.claude/local`, `/opt/homebrew/bin` and `/usr/local/bin`. An editor started from the GUI often lacks the shell's `PATH`: set `orbit.claude.path` (or `claude.path` in the desktop settings), or start the editor from a terminal. The desktop app also asks the login shell for its `PATH` at start. - **Sessions stay unavailable in a folder.** The workspace isn't trusted: VS Code's trust prompt, or File › Trust Folder in the desktop app. The graph works either way. - **A gated tool is denied without asking.** Something removed `--permission-prompt-tool stdio`; check `claude.extraArgs` for arguments that change the output or input format. - **The process dies mid-turn.** It is logged as a warning with the last stderr line, the turn fails, and the next prompt resumes the conversation. A Claude Code upgrade can change the stream-json shapes; the parser was last checked against 2.1.267. - **An MCP change doesn't show in a running conversation.** Enabling, disabling or signing in reaches each conversation at its next prompt, not during a turn. A claude.ai connector authorised on claude.ai needs Reconnect. - **History is empty.** History reads Claude Code's transcripts under `/projects/`; a workspace whose transcripts Claude Code keeps under another name (very long paths) shows none. - **`ELECTRON_RUN_AS_NODE=1`** is inherited by shells that VS Code starts, including Claude Code running inside VS Code. Orbit strips it for Claude and the desktop scripts delete it, but starting VS Code or Electron any other way with it set gives plain Node. Delete it first. ## The graph - **An import is missing or stale.** Live updates are incremental and some importers aren't re-read; [The graph pipeline](https://orbit-code.imshaikot.com/docs/graph-pipeline/) lists the cases. Reindex. - **A file isn't in the graph.** It is ignored by git, beyond `maxFiles`, in an always-excluded directory (`node_modules`, `vendor`, …), a lock file, or of an extension the language table doesn't know. A file that newly enters the listing without being created (a `.gitignore` change) needs some file event to be noticed. - **The desktop app indexes a folder as empty** (`listed 0 files via git`). The folder is inside a repository whose `.gitignore` covers it, so `git ls-files` lists nothing. Make it a repository of its own, or move it. - **The layout looks old after a layout change.** A load with an unchanged hash reuses the cached layout. Reindex, or bump `LAYOUT_VERSION` in `packages/core/src/store.ts` to redo it everywhere. - **An extractor change doesn't show.** Live updates reuse the edges of unchanged files. Reindex. - **Live updates stop in the desktop app on Linux.** The recursive `fs.watch` sets a watch per directory and a very large folder can exhaust inotify's limit; the log warns. Raise `fs.inotify.max_user_watches`, or use Reindex. - **A directory that gained many files looks crowded.** New files are placed, not laid out, and sibling bubbles are packed close. Reindex gives a fresh layout. ## The page - **The scene is frozen.** Something moves without waking the frame loop; check the console for `[webview]` errors. While hidden, the page renders nothing on purpose. - **Low frame rate.** The stage steps the pixel ratio down (2, 1.5, 1) when frames keep missing 30 fps; the readout at the bottom right shows it. A graph with many thousands of imports draws each Flat arc with fewer segments. - **The webview build fails on a URL in a comment.** The constraint check is a plain regex over `packages/webview/src/`; no `http://` or `https://` anywhere, comments included. - **Styles don't apply from code.** The CSP blocks `style` attribute strings; set styles through `element.style`. ## Builds and checks - **A stale `dist/` after switching branches.** Nx restores a cached build whose inputs haven't changed; the environment isn't an input. `yarn nx reset` after upgrading Node or changing a build script's environment. - **`@orbit-code/` doesn't resolve** right after adding a package: `yarn install` links it. A target that "doesn't exist": `yarn nx reset`. - **A dependency-cruiser or TypeScript upgrade.** Three of dependency-cruiser's modules are swapped for shims by path regex in `packages/indexer/build.mjs`; if a regex stops matching, files fall back to the regex scanner. Compare `yarn index . --out before.json` and `after.json`: more `depcruiseFallbacks` or far fewer edges means a shim stopped matching. - **The harness can't find Chrome.** It expects `/Applications/Google Chrome.app/…`; pass `--chrome `. On Ubuntu a snap Chromium can't see the paths written for it; prefer a deb Chrome. - **A red harness check.** The harness never fails on a bad result. Rerun, and compare with a run on `main`: a few checks depend on the graph under test. - **The smoke test was killed mid-run.** Delete `packages/graph/src/__orbitSmoke.ts`, `__orbitSmokeMenu.ts` and `__orbitSmokeMoved.ts` yourself. - **`yarn install` downloads a 100 MB Electron.** `ELECTRON_SKIP_BINARY_DOWNLOAD=1` skips it where the desktop app won't run; both workflows set it except in the desktop jobs. - **A copy of the workspace builds the wrong sources.** Its `node_modules/@orbit-code/*` symlinks point into the original; run `yarn install` in the copy. ## Installing - **Gatekeeper or SmartScreen warns about the desktop app.** The builds are unsigned (macOS ad hoc); allow it once. Signing is a follow-up in `apps/desktop/README.md`. - **Notifications don't show from the desktop app on macOS.** An unsigned build's notifications may be suppressed; buttons on notifications are macOS only in any case. - **The installed extension doesn't update after `yarn install-local`.** Reload the open VS Code windows. The dev host (`yarn self`) never loads the installed copy. Page: https://orbit-code.imshaikot.com/docs/troubleshooting/ --- # Architecture Orbit is one engine, one UI and two hosts. The engine indexes a workspace into a graph and runs Claude Code conversations; the UI draws the graph and the conversations in a page; a host is an editor (VS Code) or an app (Electron) that gives the engine what it needs from the machine and gives the UI a page to show. ## Three runtimes, three bundles | Bundle | Runs in | Built from | May import | | --- | --- | --- | --- | | `apps/vscode/dist/extension.js` (CommonJS) | VS Code's extension host | `apps/vscode/src/`, with `@orbit-code/core`, `agent`, `graph`, `protocol` and `common` inlined | `vscode`, Node, those packages, and `@orbit-code/indexer/listFiles` | | `packages/indexer/dist/indexer.mjs` (ESM) | A `worker_threads` worker the host starts; also the `orbit-index` CLI | `packages/indexer/src/` | Node, dependency-cruiser, TypeScript, `@orbit-code/graph` and `protocol` | | `packages/webview/dist/webview.js` (IIFE) | The page, under a strict Content Security Policy | `packages/webview/src/` | three.js, CodeMirror, `@orbit-code/graph` and `protocol` | The desktop app replaces the first with two of its own: `apps/desktop/dist/main.js` for Electron's main process (the same packages inlined) and `dist/preload.js` for the window's sandboxed preload. Both hosts copy `indexer.mjs` and `webview.js` into their `dist/` and ship them. The indexer is its own bundle for three reasons: dependency-cruiser uses top-level `await`, so it can't be CommonJS; a large cruise would block the extension host; and TypeScript (about 9 MB) must not load at activation. Nothing the host bundles reach may import `dependency-cruiser` or `typescript`, directly or through any package, and the boundary check refuses any indexer module but `listFiles` outside the indexer. The packages tagged neutral (`protocol`, `graph`, `common`) compile without VS Code, Node or the DOM, because the webview, the hosts and the indexer all import them. [The monorepo](https://orbit-code.imshaikot.com/docs/monorepo/) has the full table of projects, tags and allowed imports. ## The engine Each service owns one concern and publishes events through `Emitter` from `@orbit-code/common`. Services don't know each other; the controller wires them to the page. | Service | Package | Owns | | --- | --- | --- | | `GraphService` | core | The cached `graph.json`, index runs and live updates on the worker, frozen and extended layouts, a path resolver per graph | | `Store` | core | `orbit-v1/` in the host's storage directory: `graph.json` and up to four `layout-.bin` files | | `ChangeBatcher` | core | File paths from the host's watcher, batched (1.5 s debounce, 5 s at most) into one refresh | | `SessionService` | agent | The workspace's conversations, the probe of the `claude` CLI, its availability, the catalog (models, skills, MCP servers), and which conversation is current | | `Conversation` | agent | One conversation and its process: prompts, turns, permission requests, interrupts, cost, restarts with `--resume` | | `ClaudeCliBackend` | agent | The one `SessionBackend`: spawns `claude` in stream-json mode and parses its output into `AgentEvent`s | | `ConversationHistory` | agent | The workspace's earlier conversations, summarised from Claude Code's own transcripts | | `SessionProjector` | core | One conversation's events plus the loaded graph, turned into per-file activity and transcript entries, batched per 100 ms tick | | `OrbitController` | core | The only module that knows all of the above: it checks the page's messages, routes them, forwards events, and sends a full snapshot whenever the page says `ready` | The services take what they need from the editor as options: a logger, a storage directory, the indexer's path, the first folder, a file lister, whether the workspace is trusted. That is what lets [a host](https://orbit-code.imshaikot.com/docs/hosts/) be small. The controller takes three things from its host: a `HostUi` (capabilities, the folder, the file dialog, opening a URL or a file, permission notifications), a `FileHost` (what the file card and the editor sheet ask of a file) and, once there is one, a `WebviewTransport` (post a message, whether the page is in sight, and its `ready`, message and dispose events). ## The page `packages/webview/src/main.ts` wires the page's modules together with callbacks; none of them imports another's instance. `HostBridge` posts through `acquireVsCodeApi()` when the page has it and through `window.orbitHost` otherwise, and takes host messages as window `message` events either way, so the same bundle runs in VS Code, in the desktop window and in the harness. [The webview](https://orbit-code.imshaikot.com/docs/webview/) describes the scene and the HUD. ## The protocol `packages/protocol/src/protocol.ts` defines every message that crosses a boundary: host to page and page to host, host to indexer worker, and page to layout worker. Host messages are coarse: the graph arrives as one `reset` or one `update` carrying the whole graph (never per-edge deltas), and activity and transcript entries come batched per host tick. Page messages are untrusted input: the controller checks each one's shape and values before a service sees it. A few rules run through the protocol: - **Generations.** Work a reindex can supersede carries a generation counter, and stale results are dropped. Activity carries the graph's hash and is dropped for any other graph. - **Snapshots.** Every `ready` (first load, reload, reopen) gets a full snapshot: the host's capabilities, the graph or the last indexing status, every conversation's state and the current one, the catalog, and each conversation's transcript (the last 400 entries). Host state the page displays has to be part of it, or a reload loses it. - **Versions.** `PROTOCOL_VERSION` (13 at the time of writing) is sent with `ready`; a mismatch is logged as a hint about stale bundles, not a gate. ## Data flow 1. **Startup.** The host creates the panel or window and attaches it to the controller. The page posts `ready`; the controller sends the snapshot. 2. **Graph load.** `GraphService.load()` reads the cached `graph.json`, or lists the workspace's files (`git ls-files` in a work tree, else the editor's search or a walk of the folder) and runs the indexer worker on them. A cached graph is followed by a background refresh, so edits made while the editor was closed show up. **Reindex** starts over: a full index, the cached layout ignored, a new scene. 3. **Graph reset.** The controller sends one columnar `graph` message: node index equals array position, edges as a flat `Uint32Array`, and the cached layout when there is one for that hash. 4. **Layout.** Without a cached layout, the page runs the layout worker: one force simulation per directory, bottom-up, each directory a bubble enclosing its files and its sub-directories' bubbles. The result goes back to the host as `layoutComputed`, which saves it as `layout-.bin`; the page then posts `sceneReady`. 5. **A turn.** A prompt from the page goes to `SessionService`, which hands it to a conversation's process as one stream-json line. Claude's output becomes session state (phase, permission request, cost), transcript entries and per-file activity, which the page draws. [Claude in Orbit](https://orbit-code.imshaikot.com/docs/claude-session/) has the details. 6. **Live updates.** The host's watcher feeds changed paths into `ChangeBatcher`; `GraphService.refresh()` re-lists the files, runs the worker on the previous graph plus the touched files, extends the current layout in place, writes the new graph and layout, and the controller sends an `update` delta (or a reset, if the page holds an older graph). The page builds a new World that adopts the old one's animation state through an index remap. [The graph pipeline](https://orbit-code.imshaikot.com/docs/graph-pipeline/) has the details. ## Storage and logs | | VS Code | Desktop app | | --- | --- | --- | | Graph and layouts | `/imshaikot.orbit-code/orbit-v1/` | `/workspaces//orbit-v1/` | | Log | The **Orbit** output channel (`Orbit.log` on disk) | `/logs/orbit.log`, also stderr | | Settings | The `orbit.*` settings | `/settings.json` | Both hosts write the same log lines, which the end-to-end checks read. [Troubleshooting](https://orbit-code.imshaikot.com/docs/troubleshooting/) lists the paths per platform. ## Security constraints These hold in both hosts and in the harness, and the checks compare them: - The page's CSP is `default-src 'none'` with a nonce'd script and style and `worker-src blob:` (the layout worker starts from a Blob URL). No fonts, no remote images, no network: assets are bundled as text or data URIs, and the build refuses `fetch`, `XMLHttpRequest`, `importScripts` and any `http://` or `https://` in the webview's sources. - The page has no file access. VS Code's `localResourceRoots` is `dist/` only; the desktop scheme serves only the page, `webview.js` and its source map, and its window is sandboxed, context-isolated and without Node. - The page never touches files. The file card and the editor sheet send requests that the controller accepts only for a normalised id of a file in the graph; VS Code applies them as workspace edits, the desktop app writes in place behind a revision check and deletes only to the trash. - Text goes into the DOM as `textContent`, never HTML. Markdown replies are rendered as DOM nodes. - Claude runs only in trusted workspaces. Settings that change what runs (`claude.path`, `claude.extraArgs`, `claude.permissionMode`) are machine-scoped in VS Code and read only from user data in the desktop app, so a repository can't set them. `bypassPermissions` is accepted only from settings, never from the page. Model names and effort levels are checked because they become command-line arguments, and a resumed conversation's id must be one the host listed. - MCP status answers carry server configurations with credentials, and `initialize` the account: only the fields the catalog declares leave the parser, and neither answer is logged. A sign-in URL goes from the host to the system browser and never reaches the page or the log. Page: https://orbit-code.imshaikot.com/docs/architecture/ --- # The graph pipeline A full index builds the workspace graph once; live updates keep it current. The promise the pipeline keeps: **after a live update, the picture looks as if nothing was laid out again, and the graph is what a full index would produce.** The pipeline spans five projects: `packages/indexer` (the worker and CLI), `packages/graph` (file kinds, the columnar graph, the directory tree, layout extension), `packages/core` (`GraphService`, `Store`, `ChangeBatcher`), the host (its file lister and watcher) and `packages/webview` (the layout worker and the `World`). ## Listing files The host lists the files. In a git work tree that is `git ls-files`, so `.gitignore` applies; otherwise VS Code's `findFiles` with a fallback exclude list, or, in the desktop app, a walk of the folder with the same list. `ALWAYS_EXCLUDED_DIRS` (`node_modules`, `.git`, `bower_components`, `vendor`, `.harness`) are skipped even when tracked. The `maxFiles` setting (default 20,000, at least 100) truncates larger workspaces. Which files become nodes is decided by `classify()` in `packages/graph/src/languages.ts`, by basename, then basename pattern, then extension. Each class names the file's **kind** (its colour and legend entry) and its **extractor**: `depcruise` for JavaScript and TypeScript, a regex rule set for another language, `config` for configuration files, or `none` for a node without edges. Lock files and other generated output are left out. The same tables build the host's watcher glob, so a new extension is also watched. ## Language coverage | Kind | Extensions and files | Imports read by | | --- | --- | --- | | TypeScript, JavaScript | `.ts .tsx .mts .cts .js .jsx .mjs .cjs`; `.vue .svelte .astro` | dependency-cruiser, once per `tsconfig`/`jsconfig` so each package's path aliases apply; the regex scanner as its fallback | | Python | `.py .pyi .pyw .pyx .pxd`, `.ipynb` | Regex, resolved through `pyproject.toml` source roots | | Go | `.go` | Regex, through `go.mod` and `go.work` | | Rust | `.rs` | Regex, through `Cargo.toml` crates | | Java & Kotlin | `.java .kt .kts .scala .sc .groovy .gvy`, `.clj .cljs .cljc` | Regex | | C & C++ | `.c .h .cc .cpp .cxx .hpp .hh .m .mm .cu` and more | Regex | | C# & .NET | `.cs .csx` (`.fs .vb`: nodes without edges) | Regex, through `.csproj` root namespaces | | Swift | `.swift` | Regex, through SwiftPM targets | | Ruby | `.rb .rake .ru`, `Rakefile`, `Gemfile` and friends | Regex | | PHP | `.php .phtml`, `*.blade.php` | Regex, through `composer.json` PSR-4 prefixes | | Dart | `.dart` | Regex, through `pubspec.yaml` names | | Elixir & Erlang | `.ex .exs .erl .hrl` (`.gleam`: no edges) | Regex, Elixir modules under `lib/` | | HTML & templates | `.html .htm`, Nunjucks, Jinja, Twig, Liquid, EJS, Pug, Razor | Regex; some template languages are nodes without edges | | Styles | `.css .scss .sass .less .styl .pcss` | Regex | | Shell | `.sh .bash .zsh .fish`, PowerShell | Regex | | SQL & schemas | `.proto .graphql .gql` (`.sql .prisma .thrift`: no edges) | Regex | | Infrastructure | Terraform, HCL, Nix, `Dockerfile`, Compose files, `Vagrantfile` | Regex, or the config scan | | Other code | Haskell, Elm, Lua, Perl, R, Julia, Zig, Nim, Crystal, Solidity; OCaml, ReScript, Fortran, assembly, Verilog, Lisp and shader languages as nodes without edges | Regex | | Project config | `.toml .yaml .yml .ini .gradle .cmake .csproj .sln` and the manifests of every ecosystem (`package.json`, `tsconfig.json`, `go.mod`, `pom.xml`, `CMakeLists.txt`, `Makefile`, `Procfile`, …) | The config scan: entry points, `extends` and references, workspace members, modules, subdirectories, Dockerfiles | Twenty kinds share eleven colours; kinds that seldom share a repository share one, and the legend lists them together. ## Indexing The worker (`packages/indexer/dist/indexer.mjs`, started by `IndexClient` on a `worker_threads` thread, or run as the `orbit-index` CLI): 1. Stats every file and reads the project manifests wherever they sit: package names, entry points and `exports`/`imports` maps, the tsconfig governing each file, Go modules, Cargo crates, Python source roots, PSR-4 prefixes, pubspec names, `.csproj` root namespaces and SwiftPM targets. 2. Runs dependency-cruiser over JavaScript and TypeScript in batches of 400, once per tsconfig or jsconfig. Bare specifiers it can't follow into the graph (a workspace package without `node_modules`, an `exports` map, `@/`) go through Orbit's own resolver. 3. Runs the regex scanner over every other language, resolving through the same manifests, plus any batch dependency-cruiser threw on. The config scan links configuration files to what they name. 4. Resolves imports only against files in the graph, never against the disk. The graph (`graph.json`) holds the nodes (id, size, mtime, a count of unresolved local imports), the edges, the stats, `projects` (a fingerprint of what resolution took from the manifests) and a **hash**: sha1 over file ids, sizes and edges. The hash is computed the same way by a full index and an incremental run, so an unchanged hash means nothing drawn changed, and it keys the cached layout. **Unresolved and external are different.** An import a resolver can't match is either local but unresolved (counted on the node, and re-read when files are added) or external (never re-read). Which is which is decided by the manifests: a dotted Python import of a package under a source root is local, `numpy` is external. ## The cache `Store` keeps `orbit-v1/` in the host's storage directory: `graph.json` and up to four `layout-.bin` files, keyed only by the graph hash. Consequences: - After changing an extractor, run **Reindex**: live updates reuse the edges of files that didn't change, so they won't pick up an extractor change. - After changing the layout's output, Reindex lays out again, but a plain load with an unchanged hash reuses the old layout; bumping `LAYOUT_VERSION` in `packages/core/src/store.ts` (2 at the time of writing) redoes it everywhere. - A `layout-.bin` may be an extended layout written by a live update rather than a worker run; the format is the same. - `GraphService` keeps the loaded graph for the life of the host and updates it in place, so reopening the panel shows the current graph without re-reading the cache. ## Layout With no cached layout, the page runs the layout worker (`packages/webview/src/layout/nested.ts`, inlined into the bundle and started from a Blob URL): - Every directory with files under it is a bubble inside its parent's bubble. Labels are in canonical order (`.` first, parents before children, subtrees contiguous), and the hierarchy is derived from the labels alone. Past 65,535 bubbles the deepest levels are left out and their files sit in the nearest kept ancestor. - Bottom-up, one d3-force-3d simulation per directory places its own files and its already laid out sub-directory bubbles; the bubble's radius encloses them plus a margin. An import pulls at the one level where it is drawn. Bubbles sit on their parent's plane, a fixed gap apart; files in a directory without sub-directories spread in 3D. - Deterministic: fixed tick counts and a seeded random generator. The result goes back to the host as `layoutComputed` and is saved. The Flat view's layout (`flatLayout.ts`) is computed in the page when Flat is first shown and never cached. ## Live updates ``` file created / changed / deleted / renamed │ the host's watcher (VS Code file system watchers; a recursive fs.watch in the desktop app) ▼ ChangeBatcher workspace ids, excluded directories dropped, 1.5 s debounce, 5 s at most; flushed when a turn ends ▼ GraphService refresh(): one update at a time, later requests merged, superseded by load() │ returns early when no path can matter; re-lists the files; stops if the ids are unchanged and no graph file was touched ▼ indexer worker gets the previous graph and the touched files; stats everything; re-extracts only what the change affects ▼ extendLayout kept files keep their position; bubbles keep centre and radius; new files and directories are placed ▼ GraphService writes graph.json and layout-.bin; logs `graph updated : +A −R ~C files, E edges in N ms` ▼ controller an `update` delta if the page holds the base graph, else a reset carrying the layout ▼ the page a new World adopts the old one's animation state through the index remap; logs `graph update applied` ``` **What is re-read.** New files; files whose size or mtime changed, or that the watcher reported; importers of removed files; files with unresolved imports when something was added; and everything when `projects` differs from the previous graph's (a package name, an `exports` map, a tsconfig or a source root changed) or when more than 30% would be read anyway. Other edges are reused. When the hash comes out unchanged, only the new mtimes and fingerprint are written and nothing is sent. **Placing new things.** `extendLayout` in `packages/graph/src/layoutExtend.ts` is pure and shared with the harness. A new file goes into its directory's bubble, near its placed import neighbours, else near that bubble's other files, else at its centre, at the nearest free spot. A directory without a bubble gets one inside its deepest existing ancestor, in a spot a sibling emptied by this update when that fits, else at the free spot nearest what its files import; short of room, it shrinks rather than overlap its siblings. A bubble with no room inside grows to take a spot just outside, and its parents with it as far as they must, unless that runs into anything beside them. Emptied bubbles are dropped. **Renames.** A rename or delete from the file card doesn't wait for the watcher: the controller calls `refresh()` as soon as the edit lands and passes the rename along, so a file renamed within its directory keeps its position and its animation state. A rename made in the editor or by git arrives as a delete and a create, and the file is placed like a new one. **In the page.** `SceneController` applies an update only on top of the graph it holds; otherwise it treats it as a reset (with the layout carried, so no worker run). The new `World` adopts shared uniforms, per-file state, comets in flight, Claude's stars and MCP stations, the directory the camera is inside of and any camera move under way. If that directory is gone, the camera backs out to its nearest remaining ancestor. Added files pulse: with an edit glow and the star moving to the last of them during a turn, with one self-fading pulse otherwise. ## Known staleness Live updates are incremental, so some imports go stale until Reindex: - An added file isn't linked from importers that treated the import as external rather than unresolved: single-name Python imports, JVM imports, Ruby `require`, C# namespaces, Swift modules outside a SwiftPM package, Haskell, Elm, Lua, Perl and Clojure modules, and path strings in configuration files. - It isn't linked from importers whose import already resolved elsewhere (a closer candidate, the first files of a Go package, C# namespace or Swift target). - A file that newly enters the listing without being created (a `.gitignore` or `maxFiles` change) needs some file event to be noticed. - dependency-cruiser returns no dependencies for some files depending on what else is in the same batch, so an incremental run can find edges a full index misses, or the reverse. - New files and directories are placed, not laid out. Sibling bubbles are packed close, so a directory that gains many files crowds them until Reindex. ## Running the indexer alone ```sh yarn index --out graph.json [--max N] # a full index; needs a prior build yarn index --out next.json --previous graph.json # an incremental run, as a live update does it npx @orbit-code/indexer --out graph.json # the published package ``` Comparing an incremental run's hash with a full index of the same files is the check that the planner missed nothing. Page: https://orbit-code.imshaikot.com/docs/graph-pipeline/ --- # The webview `packages/webview` is the UI: a three.js scene and a DOM HUD, built into one `dist/webview.js` that VS Code's panel, the desktop window and the harness all load under the same Content Security Policy. It knows nothing about VS Code or Node: it talks to its host through `HostBridge` (`acquireVsCodeApi()` when the page has it, else `window.orbitHost`), and gets everything by message. ## Composition `main.ts` wires the modules together with callbacks; none of them imports another's instance. | Module | Owns | | --- | --- | | `host.ts` | `HostBridge`: typed `post` and `on(type, handler)`; `keep`/`kept` for the little state a host persists (the chosen view) | | `frameLoop.ts` | The visibility-aware, parking requestAnimationFrame loop at two paces: 30 fps for the ambient animation, up to 60 fps while the camera or the pointer moves | | `scene.ts` | `SceneController`: a graph reset into a layout into a `World`; activity into the World; live updates | | `stage.ts` | The renderer, camera and controls (rotate and pan; the wheel is `SmoothZoom`'s). Steps the pixel ratio down (2, 1.5, 1) if frames keep missing 30 fps | | `interaction.ts` | Hover and click through GPU picking; the tooltip; the `grabbing` cursor during a drag | | `zoom.ts` | `SmoothZoom`: the wheel, eased over frames toward the point under the pointer, pulling a sub-directory bubble to the middle of the screen so that zooming into it opens it | | `focus.ts` | What is on screen, derived from the camera alone: which directory is looked into, and the crossfade between a directory and the sub-directory being zoomed into | | `dirView.ts` | What the view navigates: skipped directories (one holding nothing but one sub-directory), the root, and where the view starts | | `world.ts` and `world/` | Every layer for one graph and its frozen layout, and the state that must survive a live update | | `layout/` | The nested layout worker (inlined into the bundle as a string and started from a Blob URL) and its client | | `flatLayout.ts`, `neurons.ts`, `orbits.ts` | The Flat view: files on orbits by project, the import arcs, the orbit rings and the core | | `tour.ts` | Take a Tour | | `hud/` | The DOM: identity and breadcrumb, the view tabs, the tooltip, the drawer and composer, the Claude bubbles, the conversation view, the constellation (skills, history, MCP), the history panel, the file card, the editor sheet, the spark popup, the tour button and card, the status overlay and the performance readout | | `constellation/` | The skills, history and MCP panel's own WebGL canvas: a small force layout, instanced glow glyphs, a tesseract per skill, a gyroscope per conversation, a 16-cell per MCP server | | `picking.ts`, `uniforms.ts`, `nodeState.ts`, `palette.ts` | GPU picking, the uniforms every material shares, the per-file state texture, the colours | ## Layers `World` owns, for one graph: - `NodeLayer`: every file, one instanced mesh. - `Bubbles`: every directory. - `EdgeLayer`: every import, one draw object, with visibility and highlighting decided in the shader. - `ParticleLayer`: the comets of reads. - `ClaudeLayer`: a star per conversation with a turn under way (the first is always there; further ones fade in on a ring around its home), and a smaller star per subagent that comes out of its conversation's star and goes back into it. - `McpLayer`: a station per MCP server a conversation calls, orbiting that conversation's star on a beam. Everything keyed by node index (state texels, instance attributes, pick ids, label keys) is rebuilt for each World. State that must survive a live update is copied in `World.adopt()` through the index remap: the shared uniforms, per-file state, touched files, pending events, comets in flight, Claude's stars and their positions, MCP stations, the directory the camera is inside of, and which star is followed. Anything new that holds animation state needs an adopt too. ## Focus and levels The camera decides what is drawn. A directory is looked into while the middle of the screen is inside its bubble; zooming from framing the open directory toward framing a sub-directory crossfades from the one's contents to the other's on a log scale, and at the end the sub-directory opens. Zooming back out retraces the same curve, and so does panning until the middle of the screen leaves the bubble, so no way in or out jumps. A click, Esc or the breadcrumb eases the camera over 600 ms and opens only the directories on the way. In the shaders, a file shows while its own directory's contents are on screen (and wherever Claude is working on it), a bubble while its view parent's are, and the shown directory's own bubble is a faint frame. One level up, a directory's files, lines and sub-bubbles show through its bubble at reduced strength; two levels up, fainter still; so the import network stays visible and fires while Claude thinks, and the nesting reads while zoomed out. The directories beside the shown one keep a faint rim each. Each import is drawn in exactly one directory, the deepest holding both files, as a segment between the two things shown there that contain them. ## GPU picking There is no raycasting. Each pickable mesh has a visible material and a pick material built from the same shader source, and a click or hover reads the id under the pointer from a render target, asynchronously. Ids are 24-bit: 0 is nothing, node `i` is `i + 1`, cluster `c` is a base plus `c`, and a Claude star is another base plus its stable id. Only what the directory dominating the screen shows is pickable: its own files and its sub-directory bubbles; Claude's stars are pickable regardless. Hover picks run one per frame while the pointer moves, none during a drag. ## GPU animation Every material shares the uniforms from `uniforms.ts` by reference. Per-file activity lives in one float data texture: when it was read, when edited, its weight, when it was added. Shaders derive glow, pulses and fades from the current time and the time of rest, so the CPU writes a few texels per event and nothing per frame. A read's texel is written when its comet lands, not at launch, or each read would put out the glow already there; landings still in flight when the turn ends are dropped. A thought writes only a burst window, and the edge shader fires every segment it doesn't clip with a rhythm seeded by the source end's position, so all lines out of one file fire together and keep their rhythm across a live update. ## The frame loop `frame()` renders one frame and returns whether another is needed and at which pace: smooth while the camera moves or a hover pick is wanted, ambient for everything else, parked when nothing moves. While a turn runs the edges keep flowing, so the loop runs for the whole turn; while the page is hidden it renders nothing at all. Anything that starts moving without going through an input or a host message has to wake the loop, or the scene freezes. ## Colours `palette.ts` holds sRGB triples that the shaders write straight to the framebuffer, never routed through `THREE.Color`, which converts to linear. Colour is file type: each file's kind, and each directory's dominant kind, come from the node names, and the legend in the HUD is built from the same table. The kind colours sit at mid lightness, clear of the bright read, edit and violet activity colours, and were checked in OKLab for the kinds that meet in one repository. ## Views and the tour `World.setMode` animates a shared mix from Nested (0) to Flat (1). The file mesh morphs rather than a second mesh appearing: the vertex shader flies each file on an arc once its delay has passed and prints its icon from an atlas drawn once on a canvas; bubbles and the Nested lines fade out as the mix rises, and the neurons and orbits fade in. In Flat, focus is frozen from above the orbits' plane, the breadcrumb is the root alone, and the wheel closes in on the file under the pointer. The tour picks each stop at random (hubs and large files weighted up, nowhere twice until every candidate has been) and flies there through `Focus`: out to the directory holding both places on the bearing the camera already has, then in to the stop on a new one. Some stops get a card with facts worked out from the graph alone. While it runs, hover, clicks, Esc, the breadcrumb, the wheel and the tabs are off; a live update re-finds the stop by id, and a reset ends the tour. `__orbit.startTour({ seed, cards })` makes a run repeatable for the harness. ## The HUD - **Composer.** The drawer and the conversation view each hold one: skill and file chips, the input, and a bar of Files, Skills, the model, the effort meter, the permission mode and Send (Stop, in the view, while the conversation works). The drawer adds History and the MCP button. A slash typed alone opens the Skills constellation filtered to the text after it; Enter attaches the pick in place of the command. - **Constellation.** Draws on its own transparent canvas over the whole viewport, only while open. Its glyphs take input through transparent buttons kept over them (a skill is dragged with pointer capture and dropped on the composer that opened the panel); nothing in it is a raycaster either. Under History, a two-thumb time range narrows the conversations shown. - **Markdown** replies are rendered as DOM nodes with text nodes, never HTML. - **File card and editor sheet.** The card is placed beside its file on every rendered frame; Rename and Delete morph the card into a prompt in place. The sheet is CodeMirror 6 with the page's nonce for its styles; Changes is a unified merge view against the base the host sent. A Mod shortcut the editor handled stops propagating, so VS Code's webview host doesn't undo or save a second time. - **Esc order.** One capturing listener on the window cancels a skill drag, else closes the constellation, the conversation view, the history panel or the drawer; the file card's listener runs before it, and the editor sheet's (on the document) after, before `Interaction` sees the key. ## Hard constraints The constraint check in `packages/webview/build.mjs` runs before every build and fails it if any source under `packages/webview/src/`: - mentions `Raycaster` (picking is GPU only); - calls the synchronous `readRenderTargetPixels(`; - contains `http://` or `https://`, comments included (the check is a plain regex); - uses `fetch(`, `XMLHttpRequest` or `importScripts(` (data arrives only by message). It also requires exactly one instanced mesh in `nodes.ts` and exactly one line, mesh or points object in `edges.ts`: all files are one draw call, and so are all lines. Also required, though no build checks them: three.js is bundled locally; styles are injected with the script's nonce (set styles from code through `element.style`, never a `style` attribute string, which the CSP blocks); the page never touches files; and the constellation is no exception to GPU picking of the graph. ## Debugging **Developer: Open Webview Developer Tools** in VS Code, View › Toggle Developer Tools in the desktop app, or the harness's Chrome. `window.__orbit` exposes `debug` (frames, fps, CPU time per frame, draw calls, live updates applied), `world()`, `project(x, y, z)` (world to screen), `camera()`, `view()`, `constellation()`, `editor()`, `fileMenu()`, `tour()`, `startTour()` and `endTour()`. The performance readout at the bottom right shows the frame rate, CPU time, draw calls and pixel ratio. The harness (`yarn harness`) runs this bundle in headless Chrome against a simulated host and writes a report and screenshots; [Contributing](https://orbit-code.imshaikot.com/docs/contributing/) says how to read it. Page: https://orbit-code.imshaikot.com/docs/webview/ --- # Hosts A host is the program that runs the engine and shows the page. There are two: the VS Code extension (`apps/vscode`) and the Electron desktop app (`apps/desktop`). Everything else is shared: the services in `packages/core` and `packages/agent`, the controller, the protocol, the indexer and the UI bundle. What is host code is only what the editor or the app does that the other can't. ## What every host provides | Duty | VS Code | Desktop app | Shared piece it stands on | | --- | --- | --- | --- | | Storage directory, first folder, trust, a logger | `extension.ts` | `workspace.ts`, `trust.ts`, `logger.ts` | The options of `GraphService` and `SessionService` | | The file list | `graph/files.ts`: `git ls-files` in a work tree, else `workspace.findFiles` | `listFilesOnDisk`: git, else a walk | `@orbit-code/indexer/listFiles` | | File events | `graph/watcher.ts`: VS Code's file system watchers | `watcher.ts`: one recursive `fs.watch` | `ChangeBatcher` in core | | The indexer worker | `build.mjs` copies `dist/indexer.mjs` | The same, unpacked from the asar | `@orbit-code/indexer` | | A page for the UI under the panel's CSP, with a transport | `panel/orbitPanel.ts`: a webview panel | `page.ts` (the `orbit://app/` scheme), `window.ts`, the preload | `webview.js`; `HostBridge` in the page | | Dialogs, notifications, the browser, editor tabs | `controller.ts` (`PanelController`) | `workspace.ts` (`Workspace`), `notifications.ts` | `HostUi` in `packages/core/src/controller.ts` | | File requests from the card and the sheet | `fileActions.ts`: workspace edits, documents, diff editors | `files.ts`: the disk, behind the same revision check | `FileHost`, `fileHelpers.ts` in core | | Settings under the same trust rules | `config.ts` and `package.json` | `settings.ts`: `settings.json` in user data | `normalizeSettings` and `applySettings` in core | | Status while the page is out of sight | `statusBar.ts`, permission notifications | `tray.ts` (tray, dock badge), `notifications.ts` | `summarizeSessions` in agent | The controller is `OrbitController` in `packages/core/src/controller.ts`. A host gives it a `HostUi`, a `FileHost` and, once there is one, a `WebviewTransport`, and sends `{ type: 'host', capabilities }` for what it can't do: the desktop app sets `tabs: false`, so the file card hides "Open in a tab" and transcript links open the editor sheet. ## VS Code `apps/vscode` is the extension `imshaikot.orbit-code`. Its `package.json` is the extension manifest. - **Activation** on `onStartupFinished`. `activate()` builds `GraphService` (storage from `storageUri`, `dist/indexer.mjs`, the first folder, the file lister), `SessionService` over `ClaudeCliBackend`, the controller, the watcher, the status bar item and the activity bar launcher, pushes them all into the context's subscriptions, and registers the commands. In development mode it opens the panel by itself, and `ORBIT_REINDEX=1` reindexes. - **Commands** are the `orbit.*` entries in `contributes.commands`: Open, Ask Claude…, Stop Claude, New Claude Conversation, Reindex Workspace. `orbit.prompt` takes the prompt text as an argument and returns whether it was accepted, which the smoke test relies on. - **Settings** are `orbit.maxFiles` and `orbit.claude.*`, read once by `readSettings()`, checked by `normalizeSettings`, and applied by `applySettings` on change (a new path or extra arguments probe the CLI again; a new model, effort or mode applies from the next prompt). `claude.path`, `claude.extraArgs` and `claude.permissionMode` are machine-scoped and listed in `restrictedConfigurations`, so a repository's `.vscode/settings.json` can't set them. - **Workspace trust** is `limited`: the graph works in an untrusted workspace, and sessions stay unavailable until it is trusted. - **The panel** is a webview panel with scripts enabled, `retainContextWhenHidden` (so the WebGL context and layout survive tab switches; the frame loop parks instead) and `localResourceRoots` of `dist/` only. Its HTML carries the nonce'd CSP and one script. Creating the panel collapses the side bar, and so do **Orbit Code: Open** and the activity bar icon for a panel already open. - **The activity bar** entry is a view container with one empty view whose becoming visible hands the side bar back to the Explorer and opens the panel. - **The status bar** shows how many turns run, or wait for approval, while the panel is hidden or closed; a click opens it. A permission request while the panel is hidden becomes a warning notification with Allow, the request's "don't ask again" choice and Deny. - **Logs** go to the **Orbit** output channel, on disk under VS Code's logs directory as `Orbit.log`. Webview errors and log calls arrive prefixed `[webview]`. - **Storage** is `/imshaikot.orbit-code/orbit-v1/`. ## The desktop app `apps/desktop` composes the same services for each open folder in Electron's main process, one `Workspace` per folder in its own `BrowserWindow`. | Module (`src/main/`) | Owns | | --- | --- | | `main.ts` | App lifecycle: launch arguments (`--folder`, `--reindex`, `--disable-workspace-trust`), the single-instance lock, a window per folder (an empty window takes the next folder opened), the menu, the trust dialog, recent folders reopened at launch, the indexer's path under `app.asar.unpacked` | | `workspace.ts` | `Workspace`, the desktop's `HostUi`: the services, the controller attached to its window, the watcher flushed when a turn ends; `pickFiles` is the open dialog, `openExternal` the system browser for http(s) only | | `window.ts` | `OrbitWindow`, the `WebviewTransport`: messages accepted only from its own top frame on `orbit://app/`, `ready` re-armed on every load, visibility on show, hide, minimise and restore, no navigation or new windows | | `page.ts` | The privileged `orbit` scheme: the page with a fresh nonce and the panel's CSP (as a header and a meta tag), `webview.js` and its map, and nothing else | | `../preload/preload.ts` | Exposes one object with one function, `window.orbitHost.postMessage`, and delivers host messages as window `message` events | | `files.ts` | `DesktopFileHost`: the same file requests on the disk, with the same revisions; delete through the system trash and refused otherwise; no tabs | | `watcher.ts` | A recursive `fs.watch` into `ChangeBatcher` | | `settings.ts`, `trust.ts`, `recent.ts` | JSON files in user data; the settings file is watched and applied with `applySettings` | | `notifications.ts`, `tray.ts`, `shellPath.ts` | Permission notifications (with buttons on macOS), the tray and badge count, the login shell's `PATH` for an app started from the GUI | | `logger.ts` | `/logs/orbit.log`, rotated past 5 MB, with the same lines the extension writes | Invariants the desktop keeps: the window is sandboxed, context-isolated and without Node; the scheme serves the page and its script only; settings come from user data only, never from a folder; trust gates Claude per folder; delete is to the trash or not at all; typed arrays cross the IPC intact (the smoke's relaunch step proves a saved layout is reused). Electron 44.3.0 and electron-builder 26.15.3 are pinned; both CI workflows skip Electron's binary download except in the desktop jobs. `apps/desktop/README.md` covers running it, its settings, where it keeps things and packaging. ## Adding an editor An editor app hands the shared host its facts and the shared UI a page; nothing else is rewritten. The checklist: 1. Scaffold `apps/` (see [The monorepo](https://orbit-code.imshaikot.com/docs/monorepo/)) with a runtime tag of its own, and add that runtime to the boundary check's table. 2. Supply each duty in the table above. `apps/vscode` is the reference for every one, and the desktop app for a host that embeds Chromium with Node beside it. 3. Serve `webview.js` in a page under the same CSP, define `window.orbitHost.postMessage` before the script runs, and deliver host messages as window `message` events. 4. Implement `HostUi`, `FileHost` and `WebviewTransport`, and send the `host` capabilities message for what the editor can't do. 5. Targets: `build` (depending on `^build`, so the webview and indexer are built), `package`, an end-to-end check, and `nx-release-publish`; a `publish:` tag and a release group; jobs in `ci.yml` and `release.yml`. By editor: VS Code's family (Cursor, Windsurf, VSCodium, Positron) needs no new app, since the same `.vsix` is published to Open VSX. An editor that can't host a page (JetBrains through a JCEF tool window, Zed, Neovim) would start a local server app running the engine in one Node process, speaking the protocol over a WebSocket or stdio, and open its page. Page: https://orbit-code.imshaikot.com/docs/hosts/ --- # The monorepo An Nx workspace (package-based, a `project.json` per project) over Yarn 4 workspaces, cut along one line: **what a module needs from where it runs**. Code that needs nothing but JavaScript is neutral and goes everywhere; code that needs Node runs in a host or the indexer worker; code that needs the DOM is the UI; only an editor app touches an editor's API. ## Projects | Project | Package | Tags | Holds | | --- | --- | --- | --- | | `packages/protocol` | `@orbit-code/protocol` | lib, neutral | Every message that crosses a boundary; workspace ids; MCP tool names | | `packages/graph` | `@orbit-code/graph` | lib, neutral | File kinds, the columnar graph, the directory tree, layout extension, node radius | | `packages/common` | `@orbit-code/common` | lib, neutral | `Emitter`, `Event`, `Disposable`, `Logger`, `TickBatcher` | | `packages/indexer` | `@orbit-code/indexer` | lib, node, publish:npm | The indexer worker and the `orbit-index` CLI; `listFiles` for hosts | | `packages/agent` | `@orbit-code/agent` | lib, node | Claude Code conversations, the CLI backend, stream-json, permissions, the catalog, history | | `packages/core` | `@orbit-code/core` | lib, node | The editor-agnostic host: `GraphService`, `Store`, the indexer client, `ChangeBatcher`, `SessionProjector`, the controller, settings | | `packages/webview` | `@orbit-code/webview` | lib, browser | The UI, built to `dist/webview.js` | | `apps/vscode` | `orbit-code` | app, vscode, publish:vscode | The VS Code extension | | `apps/desktop` | `@orbit-code/desktop` | app, electron, publish:desktop | The Electron desktop app | | `tools/harness` | `@orbit-code/harness` | tool, browser | The simulated host and the headless Chrome harness | ``` neutral protocol ◄──── graph common ▲ ▲ ▲ node indexer ─────────┘ (+ protocol) │ agent ── protocol ─────────────────┤ core ─── agent, graph, protocol, common, indexer/listFiles browser webview ─ graph, protocol harness ─ graph, protocol, common vscode apps/vscode ─ core, agent, graph, protocol, common, indexer/listFiles; ships the indexer and webview builds electron apps/desktop ─ the same ``` ## Where code goes Ask, in order: 1. **Does it call an editor's API?** That editor's app. 2. **Does it need Node?** If it extracts imports or needs dependency-cruiser or TypeScript: `packages/indexer`, from which only `listFiles` is imported outside. If it is about Claude sessions and knows nothing of graphs: `packages/agent`. If it joins graphs, sessions and files on behalf of a host: `packages/core`. 3. **Does it need the DOM?** `packages/webview`, or `tools/harness` for test pages. 4. **Otherwise it is neutral.** Part of a message's shape: `packages/protocol`. Computing on graphs, paths or layouts: `packages/graph`. Plumbing any service needs: `packages/common`. A service in core or agent takes editor facts as options and never asks an editor. A neutral package doesn't gain a Node or DOM import to save a copy: if two runtimes need the same logic, the logic is neutral and each runtime wraps it. A new package is worth its manifest only when it holds a different runtime or a different reason to change. ## The boundary check `yarn boundaries` (`tools/scripts/check-boundaries.mjs`, also in CI) reads each project's `src/` imports and `package.json` and fails on: - **Tags.** Anything but exactly one `type:` (lib, app, tool) and one `runtime:` (neutral, browser, node, vscode, electron) per project. - **Type.** A lib depending on anything but libs; an app or tool depending on an app or tool. - **Runtime.** Neutral imports neutral; browser imports neutral and browser; node imports neutral and node; vscode and electron import neutral and node too. Only `runtime:vscode` imports `vscode`. Node builtins are refused in neutral and browser code. - **Declared.** An import of a package the project's `package.json` doesn't list, or a relative import leaving the project's `src/`. - **Indexer.** Any `@orbit-code/indexer` import but `listFiles` outside the indexer, so dependency-cruiser and TypeScript stay in the worker. It can't see what a bundle pulls in transitively (the extension's wiring check follows the extension's imports into every package for that) or globals: a DOM or Node global in the wrong package fails that package's own typecheck instead, because its tsconfig doesn't declare it. ## Packages - A workspace package exports its sources module by module: `"./*": "./src/*.ts"`. Consumers import `@orbit-code/graph/languages`, and typecheck and bundle each other's sources directly; there is no build step between libraries and no barrel. - `dependencies` are what the code imports (workspace packages as `workspace:*`, npm packages pinned exactly, the same version everywhere). `devDependencies` are types and the builds a project only ships (`@orbit-code/webview` for the extension and the harness). The desktop app lists every workspace package as a devDependency, because electron-builder copies an app's `dependencies` into the package and everything is bundled already. - Workspace-wide tools (`nx`, `@nx/js`, `typescript`, `esbuild`, `@types/node`) are root devDependencies. - `.yarnrc.yml` keeps a `node_modules` tree, because the build scripts and tools run under plain Node, which can't resolve Plug'n'Play packages. Workspace packages are symlinked into `node_modules/@orbit-code/`. - Names are `@orbit-code/`; the extension keeps `orbit-code`, its Marketplace identity. Manifests are `private: true` with a one-sentence description and `license: MIT`. ## TypeScript per runtime `tsconfig.base.json` holds the compiler options (`noEmit`, `moduleResolution: bundler`, strict). Each project's `tsconfig.json` extends it and adds only its runtime: | Runtime | Adds | | --- | --- | | neutral | ES2023 and `tools/typescript/neutral-globals.d.ts` (timers and `console`, nothing else) | | node | `"types": ["node"]` | | browser | the DOM libs | | vscode | `"types": ["node", "vscode"]` | | electron | `"types": ["node"]` and the DOM (the preload sees the page's window); Electron's types come with its package | A project's typecheck also checks the workspace sources it imports, under its own settings. esbuild doesn't typecheck, so `yarn typecheck` is a separate step. The `build.mjs` files, `scripts/`, `test/` and `tools/**/*.mjs` are plain JavaScript and aren't typechecked; `node --check` catches syntax errors in them. ## Targets | Target | Projects | Does | | --- | --- | --- | | `build` | indexer, webview, vscode, desktop, harness | `node build.mjs [--production]` in the project, writing its `dist/`. Depends on `^build`, so `-c production` reaches the dependencies. The webview's runs its constraint check first | | `typecheck` | every project | `tsc -p tsconfig.json`, from the target defaults | | `watch` | the same as build | `build.mjs --watch`, continuous; `vscode:watch` starts the indexer and webview watches and copies their builds in as they change | | `package` | vscode, desktop | A production build, then vsce or electron-builder. Uncached for the desktop, whose output is per platform | | `smoke`, `e2e`, `start` | vscode, desktop, harness | Uncached, after a build | | `nx-release-publish` | indexer, vscode, desktop | npm, the Marketplace and Open VSX, or GitHub release assets | Each build writes nothing outside its own `dist/`; an app copies what it ships from its dependencies' `dist/`. Nx caches `build` and `typecheck` in `.nx/cache` from each project's files, `tsconfig.base.json`, `tools/typescript` and the sources of what it depends on, but not the environment: after upgrading Node or changing a build script's environment, `yarn nx reset`. Nx passes a configuration along `^build` but not along a `dependsOn` on the same project, which is why `vscode:package` runs `nx run vscode:build:production` itself, and why `package.mjs` refuses a `dist/` still holding source maps. ```sh yarn nx show projects # every project yarn nx show project vscode --json # its targets yarn nx graph # the project graph in a browser yarn nx run-many -t typecheck build -p webview indexer yarn nx affected -t typecheck build --base=main yarn nx reset # clear the cache and stop the daemon ``` Arguments after a root script reach the script Nx runs: `yarn harness --graph g.json` runs the harness on that graph after Nx built what it needs. ## Adding a library 1. Create `packages/` with a `package.json` (source exports, `private: true`), a `project.json` (name, `projectType`, `sourceRoot`, tags, `"typecheck": {}`), a `tsconfig.json` for its runtime and `src/.ts`. 2. `yarn install`, which links it into `node_modules/@orbit-code/`. 3. Add `"@orbit-code/": "workspace:*"` to the `dependencies` of each project that imports it, then `yarn install` again: the lockfile records workspace dependencies, and CI installs with `--immutable`. 4. `yarn nx show project `, `yarn typecheck`, `yarn boundaries`. 5. Add its row to the table above and to the root README's layout table. ## Moving code between packages 1. `git mv` the file, so history follows it. 2. Imports inside the new package become relative; imports of it from elsewhere become `@orbit-code//`. 3. Update the `dependencies` of the package that lost it, the one that gained it, and every importer; `yarn install`. 4. `yarn typecheck && yarn boundaries && yarn build`. 5. Grep the old path: the smoke suite names graph files, the harness names sources, and `ci.yml` checks that `packages/webview/src/main.ts` is in the indexed graph. ## Gotchas - `node_modules/@orbit-code/*` are relative symlinks. A copy of the workspace whose `node_modules` is a symlink to this one builds *this* workspace's sources; run `yarn install` in the copy. - A new project shows up in Nx from its `project.json`, but nothing resolves `@orbit-code/` until `yarn install` has linked it. A target that "doesn't exist" right after adding it: `yarn nx reset`. - Neutral means neutral: `TextEncoder`, `URL`, `structuredClone` and `performance` aren't ECMAScript, so neutral packages don't have them unless `neutral-globals.d.ts` declares them. - The test runners write into the workspace: the smoke creates `packages/graph/src/__orbitSmoke*.ts` for a few seconds, and the harness writes `.harness/` at the root. Page: https://orbit-code.imshaikot.com/docs/monorepo/ --- # Contributing ## Setting up ```sh 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](https://orbit-code.imshaikot.com/docs/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](https://orbit-code.imshaikot.com/docs/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: ```sh 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 ` 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](https://orbit-code.imshaikot.com/docs/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: `: ` 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. Page: https://orbit-code.imshaikot.com/docs/contributing/ --- # Releasing Releases are cut with Nx Release using **version plans**, because commit subjects here aren't conventional commits. A plan is a small Markdown file that says what gets which bump and why; `nx release` applies the plans, writes the changelogs, commits and tags; pushing the tag makes `.github/workflows/release.yml` build and publish. The VS Code extension and the desktop app are released together: one version, one `v` tag, one GitHub release holding the `.vsix` and the macOS dmgs, with that version's changelog as its notes. ## Release groups `nx.json` defines two groups, by tag: | Group | Projects | Tag | Publishes to | | --- | --- | --- | --- | | `apps` | `apps/vscode` (`publish:vscode`) and `apps/desktop` (`publish:desktop`), fixed: one version for both | `v` | A GitHub release with the `.vsix` and a dmg each for Apple silicon and Intel; the Visual Studio Marketplace and Open VSX | | `npm` | `packages/indexer`, `apps/server` and any other `publish:npm` project, versioned independently | `-v` | npm, with provenance | A dependent is never bumped for a dependency's release (`updateDependents: never`), and a minor bump below 1.0 is applied as a minor, not a patch (`adjustSemverBumpsForZeroMajorVersion: false`). Each project gets its own `CHANGELOG.md`, without author lines; there is no workspace changelog. ## Cutting a release 1. **With the change worth releasing**, write a plan and commit it in the same commit: ```sh yarn nx release plan patch --groups=apps -m "Fix the file card's position after a live update" yarn nx release plan minor --projects=indexer -m "…" ``` This writes `.nx/version-plans/version-plan-