npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

copilot-runway

v0.2.1

Published

A local web dashboard for visualizing and orchestrating multiple GitHub Copilot CLI sessions across multiple projects

Readme

Copilot Runway

A local web dashboard for visualizing and orchestrating GitHub Copilot CLI sessions across all your projects.

If you work across multiple repos with several Copilot CLI sessions running at once, Runway gives you a single control tower to see what's happening, read conversation history, and send prompts without switching terminals.

Copilot Runway dashboard showing projects, sessions, and conversation detail

Features

  • Project sidebar: auto-discovers projects from the Copilot CLI data store; add custom folders on the fly
  • Session list: view all sessions for a project, filter by active/inactive, sorted by most recent activity
  • Live status: detects active sessions via PID lock files with process verification
  • Conversation viewer: read full session history with Markdown rendering (GFM, code blocks, tables) and syntax highlighting
  • Send prompts: send one-shot prompts to new or existing sessions, streamed back via SSE
  • Resizable detail panel: drag to resize, double-click to collapse, auto-expands on session select
  • Agent selection: choose custom agents for new or existing sessions; Runway remembers the last-used agent per session
  • Localhost only: binds to 127.0.0.1 with CORS protection

Prerequisites

  • Node.js 18+
  • GitHub Copilot CLI installed and on your PATH (copilot --version should work)

Install

Run it instantly with npx (no install required):

npx copilot-runway

Or install globally for a permanent command:

npm install -g copilot-runway
copilot-runway

The server starts and opens your browser automatically:

  Copilot Runway running at http://127.0.0.1:3847

From source

git clone https://github.com/jamesmcroft/copilot-runway.git
cd copilot-runway
npm install
npm start

Development mode

npm run dev

Uses node --watch to auto-restart the server on file changes.

How it works

Runway reads from the Copilot CLI's local data stores (read-only) to build the dashboard:

| Source | What it provides | | ------------------------------------------------ | -------------------------------------------------------------- | | ~/.copilot/session-store.db | Session history, conversation turns, checkpoints, file changes | | ~/.copilot/data.db | Project list (repos you've used with the CLI) | | ~/.copilot/session-state/<id>/workspace.yaml | Session name, working directory, branch, git root | | ~/.copilot/session-state/<id>/inuse.<pid>.lock | Active session detection (PID verified against OS) |

Custom projects and app settings are stored in ~/.runway/ to keep Runway's data separate from the CLI's databases:

| File | Purpose | | --------------------------------- | ----------------------------------------------------------------- | | ~/.runway/projects.json | Custom project folders added through the dashboard | | ~/.runway/session-agents.json | Last-used agent per session (auto-populated when sending prompts) | | ~/.runway/settings.json | Global Runway settings (worktrees root, default agent, launchers) | | ~/.runway/project-settings.json | Per-project setting overrides keyed by absolute project path |

When you send a prompt, Runway spawns a copilot process with -p "your prompt" --output-format json and streams the JSONL output back to the browser via Server-Sent Events. You can optionally select a custom agent (via the CLI's --agent flag), and Runway remembers your choice per session.

Settings

Open the settings page from the gear icon in the top bar, or with the Ctrl+, (Cmd+, on macOS) shortcut. The page is also reachable directly at /settings.

On-disk layout (under ~/.runway/):

  • settings.json holds the global document: { "schema_version": 1, "values": { ... } }. Read on demand, cached in memory, rewritten atomically (tempfile + rename) on every save.
  • project-settings.json holds per-project overrides as a single map keyed by the project's absolute path: { "schema_version": 1, "projects": { "/abs/path": { ... } } }.
  • launchers.json is the legacy editor-binary file from earlier releases. On first boot after upgrading, Runway folds its contents into settings.json under launchers.vscode and logs a deprecation warning. The old file is left in place for one release; a future version will remove it.

Resolution order (highest precedence last):

  1. Hardcoded default (declared in lib/runway/settings-schema.js).
  2. Global value from settings.json.
  3. Per-project override from project-settings.json (only for keys marked overridable; worktrees.root is global only).

Visual preferences (theme mode, palette family, detail-panel width) stay in browser localStorage so they apply without a server round-trip and never travel between machines. Theme mode (system / light / dark) is independent from palette; eight palettes ship today: default, solarized, monokai-inspired, high-contrast, material, tokyo-night, catppuccin, rose-pine. Each palette CSS file declares both light and dark variants so flipping theme mode keeps the active palette.

Reset to defaults: delete the relevant file under ~/.runway/ and reload. Runway will recreate it with schema defaults on the next save. For a partial reset, edit the JSON directly (Runway tolerates unknown keys and falls back to defaults if it cannot parse the file, logging a warning rather than crashing).

Removing a project

The settings page (per-project section) has a Remove project button that hard-purges the project from every Runway-owned state file under ~/.runway/. The confirmation modal includes:

  • A typed-name input. The destructive Remove button stays disabled until you type the project's display name (the last path segment) verbatim. Standard guard for irreversible actions.
  • An Also delete worktree directories on disk checkbox, default checked. When checked, every worktree directory bound to this project is removed through git worktree remove --force so git's worktree metadata stays consistent. When unchecked, the worktree bindings are still cleared from Runway state but the on-disk directories stay for manual recovery.
  • A summary line listing what will be removed, for example "Will remove 1 project entry, 2 setting override groups, 3 pins, 2 worktree bindings."

The sweep is implemented as a dynamic registry over per-project state files (projects.json, project-settings.json, pins.json, session-agents.json, worktree-bindings.json). New per-project stores can join the sweep without touching the route layer. The global launchers.json file is not project-scoped and is left untouched. Every per-file write is atomic (tempfile plus rename), and a missing or malformed individual store is treated as "nothing to remove" rather than a fatal error so a partially corrupt state directory still purges cleanly.

Active sessions block removal. If any live Copilot CLI session is attached to the project (an alive PID under ~/.copilot/session-state/<id>/inuse.<pid>.lock), the server returns HTTP 409 with a body of the form { "error": "active_sessions", "sessionIds": [...] } and performs no state mutations. The modal lists the blocking session ids and prompts you to stop them first; Runway never force-kills sessions on your behalf.

Idempotent re-delete returns 404. Once a project's Runway state is gone there is nothing to remove on the second attempt, so the server replies 404 Not Found. The UI translates a 404 into a "Project is already removed" toast. The 404 response shape is intentional: it is also returned for any other unknown project key so callers can rely on a single not-found path.

What is preserved. Historical session rows in the CLI's ~/.copilot/session-store.db are never touched. Session detail pages still load after the project is removed; their project label degrades to the raw cwd path. The user's repository on disk is never touched under any code path.

Live updates

The dashboard subscribes to /api/events (Server-Sent Events) for push-based updates so the UI reflects CLI activity within milliseconds instead of waiting for a periodic poll. The server watches two surfaces:

  • ~/.copilot/session-state/ (recursive fs.watch) for session directory and inuse.<pid>.lock changes
  • ~/.copilot/session-store.db and its -wal sidecar (hybrid fs.watch plus periodic fs.stat mtime heartbeat) for write activity, with a 250 ms debounce so the burst of WAL commits from a single CLI turn produces one event

The event channel publishes the following types:

| Event | Trigger | Data | | ------------------ | -------------------------------------------------------------------- | ------------------------------------- | | ready | Sent immediately on each successful connection | { at } | | session.created | New directory under ~/.copilot/session-state/ | { sessionId, at } | | session.active | inuse.<pid>.lock appears and the PID is alive | { sessionId, pid, at } | | session.inactive | inuse.<pid>.lock disappears or the PID fails a liveness probe | { sessionId, at } | | session.ended | Session directory removed | { sessionId, at } | | db.activity | Debounced write activity on session-store.db or -wal | { at } | | state.snapshot.end | Marks the end of the initial state snapshot on a new connection | { count, at } |

On every new connection the server sends ready, then one session.active or session.inactive frame per known session under ~/.copilot/session-state/ (classified by the same PID liveness check used for live emissions), then state.snapshot.end with the total snapshot count, and only after that the live delta stream. Snapshot frames are sent only to the new client's stream, never broadcast. Subscribers can therefore build full session state from the SSE stream alone, without a parallel REST call. Any events fired by the watcher during the snapshot read are captured in a per-connection buffer and drained immediately after state.snapshot.end, so no state change can be lost in the gap.

Frames are encoded as event: <type>\ndata: <json>\n\n and a comment heartbeat (: heartbeat\n\n) is sent every 25 seconds to defeat idle-proxy timeouts. EventSource clients ignore comment lines.

If the SSE connection drops, the dashboard reconnects with exponential backoff (1s, 2s, 4s, 8s, 16s, capped at 30s). After three consecutive failures, or if the browser does not support EventSource, the client falls back to a 30 second polling loop. The fallback is cancelled automatically the next time SSE reconnects and emits ready.

The session detail panel updates live off the same channel. When the CLI commits a new turn or checkpoint, a coalesced db.activity event triggers a re-fetch of the selected session and the conversation re-renders within roughly two seconds, with no manual reload. Refreshes are de-duplicated so at most one request is in flight at a time, and a stale-result guard discards any response that arrives after you have switched to a different session. Scroll position is preserved across re-renders: if you were scrolled to the bottom, the panel follows the conversation to the new bottom; if you had scrolled up to read earlier history, your position is kept exactly where it was.

Syntax highlighting

Fenced code blocks in the conversation view are highlighted with Prism (MIT). The eagerly loaded language set covers javascript, typescript, jsx, tsx, python, bash, shell-session, powershell, json, yaml, markdown, css, markup (HTML), sql, go, rust, csharp, java, and diff. Unknown or missing fence languages fall through to plain text, and blocks larger than 100,000 characters bypass the tokenizer so a pasted log dump cannot stall the renderer. Token colors are mapped to the existing theme variables so the same stylesheet works in both light and dark mode. Highlighting is render-time only via Prism.highlight(string, grammar, language), never Prism.highlightElement, so user content cannot escape the tokenizer.

Concurrency safety

Runway only ever reads the CLI's SQLite files via better-sqlite3 in read-only mode (WAL-safe) and subscribes to filesystem change notifications. It never locks or writes the CLI's files, so there is no interference with the CLI as the active writer. fs.watch events from SQLite WAL commits are debounced (250 ms) to avoid event storms.

Worktrees

Each session can opt in to a dedicated git worktree so parallel sessions on the same project never step on each other's files. The feature is fully opt in: new sessions get no worktree by default, and you bind one explicitly from the session detail panel.

How binding works

  1. Open a session and click Bind worktree in the Worktree section of the detail panel.
  2. Runway creates a new branch runway/<session-id-short> (the first eight alphanumeric characters of the session id) off the project's current HEAD.
  3. Runway runs git worktree add to materialize the branch under <worktrees.root>/<sanitized-project-name>/<session-id-short> on disk and persists the path-to-session binding in ~/.runway/worktree-bindings.json.

The sanitized project name is the project folder basename, lowercased, with non-alphanumeric characters folded to single hyphens, trimmed, and capped at 64 characters. The slug is purely a directory naming convenience; the canonical project key remains the absolute path.

Where worktrees live

The root directory is controlled by the worktrees.root global setting (see /settings). The default is ~/.runway/worktrees and a leading ~ is expanded to your home directory. The setting is global only: there is no per-project override, so every project's worktrees are siblings under a single tree.

Opening a worktree

Once bound, the panel offers Open worktree in VS Code which launches your editor at the worktree path via the vscode://file/... URL handler. The same handler is used by Code, Code-Insiders, Cursor, and Codium, so the action respects whatever editor binary your OS has configured.

Manual cleanup

Removal is always manual. The Remove worktree button opens a confirmation modal that shows the path and a clean or dirty badge based on git status --porcelain. The modal exposes two opt in checkboxes:

  • Force removal: passes --force to git worktree remove. Required when the worktree has uncommitted changes; the server rejects a non-force delete on a dirty tree with HTTP 409.
  • Also delete the branch: deletes the local runway/<id> branch after the worktree is gone. Disabled when the branch has commits beyond the branch point (Runway checks reachability with git merge-base --is-ancestor), so you can never lose work in flight by accident.

There is no automatic cleanup on session end or server restart. Bindings persist across restarts and only the explicit Remove action drops them.

Concurrency

A worktree path can be bound to at most one session at a time. If a second session resolves to the same path (for example, two session ids share the same eight-character prefix on the same project), the server returns HTTP 409 with the bound session id. The UI shows a "Focus the bound session" dialog whose button navigates straight to that session.

Diffing two worktrees

The compare-worktrees UI is out of scope for this iteration. Use git diff from either worktree directly for now.

Project structure

copilot-runway/
├── server.js          # Express app wiring: middleware, static assets, route mounting, listen
├── lib/
│   ├── paths.js               # Shared filesystem path constants; ensures ~/.runway exists
│   ├── cli/
│   │   ├── spawn.js           # `copilot` CLI spawn helper + running-process registry
│   │   └── agents.js          # List available custom agents (cached) via the CLI
│   ├── store/
│   │   ├── db.js              # Read-only openers for ~/.copilot/session-store.db and data.db
│   │   └── sessions.js        # workspace.yaml reader, lock-file/PID status, find-new-session-id
│   ├── runway/
│   │   ├── projects.js              # Load/save ~/.runway/projects.json (custom folders)
│   │   ├── session-agents.js        # Load/save ~/.runway/session-agents.json (per-session agent)
│   │   ├── settings.js              # Read/write/cache layer for settings.json and project-settings.json
│   │   ├── settings-schema.js       # Descriptor table + defaults (single source of truth)
│   │   └── settings-migrations.js   # Forward-only migration runner keyed by schema_version
│   ├── watchers/
│   │   ├── lifecycle.js       # fs.watch on ~/.copilot/session-state/, emits session.* events
│   │   └── db.js              # Hybrid fs.watch + mtime heartbeat on session-store.db, emits db.activity
│   └── routes/
│       ├── projects.js        # GET /api/projects, POST /api/projects/add
│       ├── sessions.js        # GET /api/sessions, /sessions/active, /sessions/:id
│       ├── send.js            # POST /api/sessions/send (SSE stream of CLI events)
│       ├── agents.js          # GET /api/agents
│       ├── stats.js           # GET /api/stats
│       ├── settings.js        # GET/PATCH/PUT /api/settings and /api/settings/projects/:projectKey
│       └── events.js          # GET /api/events (SSE stream of lifecycle and DB events)
├── bin/
│   └── copilot-runway.js  # CLI entry point (npx / global install)
├── public/
│   ├── index.html     # Single-page app shell
│   ├── settings.html  # Standalone settings page (loaded at /settings)
│   ├── settings.css   # Layout for the settings page
│   ├── settings.js    # Schema-driven settings form (UMD)
│   ├── palettes.js    # Palette manifest + apply helper (UMD)
│   ├── palettes/      # Per-palette CSS files, loaded on demand
│   ├── styles.css     # Light and dark themed styles
│   ├── app.js         # Frontend (rendering, themes, resize, markdown, API calls)
│   ├── logo.svg       # App logo
│   └── favicon.svg    # Browser tab icon
├── .github/
│   ├── workflows/ci.yml   # CI + npm publish pipeline
│   ├── dependabot.yml
│   └── ISSUE_TEMPLATE/
├── package.json
├── LICENSE
└── README.md

Module boundaries

The backend is split along three concerns so the upcoming WebSocket bridge has a clean place to live:

  • lib/cli/ owns every interaction with the copilot binary (spawn, agent discovery, the running-process registry).
  • lib/store/ owns read-only access to the Copilot CLI's own state under ~/.copilot/ (the two SQLite databases and per-session workspace.yaml + inuse.<pid>.lock files).
  • lib/runway/ owns Runway's own config files under ~/.runway/ (custom projects, per-session agent selection).
  • lib/routes/ contains one Express router per resource. Routers depend on the three layers above; they never reach into the filesystem or spawn processes directly.
  • server.js is wiring only: it sets up middleware (JSON body, CORS, static assets), mounts the routers under /api/..., and starts the listener.

API reference

All endpoints are localhost-only with CORS origin protection.

| Method | Path | Description | | ------ | ------------------------------------------------- | ------------------------------------------------------------------------------- | | GET | /api/projects | List all projects (CLI + custom), sorted by recent activity | | POST | /api/projects/add | Add a custom project folder ({ folderPath, name? }) | | GET | /api/projects/:projectKey/summary | Counts of Runway-owned state for the project ({ projects, projectSettings, pins, sessionAgents, worktreeBindings }) and the list of active session ids attached to it | | DELETE | /api/projects/:projectKey?removeWorktrees=true\|false | Hard-purge the project. 204 on success, 404 on unknown key (and idempotent re-delete), 409 with { error: "active_sessions", sessionIds } when live sessions are attached, 400 on a malformed key | | GET | /api/sessions?cwd=...&limit=50&active_only=true | List sessions, optionally filtered by directory | | GET | /api/sessions/active | List all active sessions across all projects | | GET | /api/sessions/:id | Session detail with paginated chronology (turns + inline file events) and checkpoints | | POST | /api/sessions/send | Send a prompt (SSE stream). Body: { prompt, sessionId?, cwd?, name?, agent? } | | GET | /api/agents | List available custom agents (cached 5 min) | | GET | /api/stats | Dashboard stats (total sessions, active count, recent activity) | | GET | /api/events | SSE stream of session lifecycle and DB activity events | | GET | /api/sessions/:id/worktree | Worktree binding state for a session ({ bound, worktreePath?, branchName?, dirty?, canDeleteBranch? }) | | POST | /api/sessions/:id/worktree | Bind a fresh worktree to the session. Returns 201 on create, 200 when already bound, 409 with boundSessionId on concurrency | | DELETE | /api/sessions/:id/worktree | Remove the bound worktree. Body: { force?, deleteBranch? }. 409 on dirty without force | | GET | /api/projects/:projectKey/worktrees | List worktrees for a project, enriched with the bound session id where applicable |

Configuration

| Environment variable | Default | Description | | -------------------- | ------- | ------------------------------------------- | | PORT | 3847 | Server port (change in server.js for now) |

Theme preference and panel width are stored in the browser's localStorage.

Contributing

Contributions are welcome. To get started:

  1. Fork the repo and clone your fork
  2. npm install
  3. npm run dev to start with auto-reload
  4. Make your changes and test locally
  5. Open a PR with a clear description of what you changed and why

License

MIT