Development
Build LocalPilot from source, run the tests, and contribute.
Prerequisites
Node.js 20+ and VS Code 1.104+.
git clone https://github.com/isathish/localpilot.git
cd localpilot
npm install
Scripts
| Command | What it does |
|---|---|
npm run compile |
Type-check (tsc), lint (eslint) and bundle with esbuild → dist/extension.js |
npm run watch |
Rebuild on change |
npm run smoke |
Offline tests: mock Ollama & OpenAI servers, agent loop, context manager, parallel pool |
npm test |
Mocha tests inside a real Extension Development Host (downloads VS Code on first run). Includes end-to-end runs against a real local model when Ollama is reachable on :11434 — a real tool call writes a file, two sessions run in parallel, stop() aborts a run. Skipped otherwise; LOCALPILOT_E2E_MODEL=ollama/<tag> forces a model. |
npx @vscode/vsce package --no-dependencies |
Build the VSIX |
Press F5 to launch an Extension Development Host.
UI harnesses
The webviews can be opened in a plain browser with mock data for quick visual checks in light/dark and at any width — no Extension Host needed:
| File | What it shows |
|---|---|
scripts/ui-preview.html |
Chat view with a full sample transcript (steps, diff and terminal cards, approvals, tasks, changes). |
scripts/ui-preview-parallel.html |
Chat view with a stateful mock controller; window.sim.start/tool/text/end(sessionId, …) drives several sessions streaming at once to check tabs and background runs. |
scripts/settings-preview.html?section=models&light=1 |
Settings / setup wizard (?page=setup) with mocked providers, probe results, hardware profile, installed models, catalog and jobs. |
Extension API
activate() returns a small API for other extensions and scripts:
const lp = await vscode.extensions.getExtension('localpilot.localpilot-agent')!.activate();
const id = await lp.runPrompt('Add input validation to src/server.ts', 'agent'); // new session, streams in the background
const ids = await lp.runParallel(['Write tests for utils', 'Update the README']); // one agent per task, concurrently
lp.onDidChangeRuns(() => console.log(lp.runningSessionIds));
lp.stop(id);
send(sessionId, text) resolves with the final answer, getSession(id) returns the transcript, listModels() the discovered models and version the installed extension version.
Architecture
src/
providers/ Ollama + OpenAI-compatible clients, registry (provider/model ids)
agent/ agentLoop (model⇄tools), subagents, skills, prompts, parallel pool, changeTracker
context/ contextManager (budget → prune → compact), tokenizer, workspaceContext (@mentions, rules)
tools/ fs, search (ripgrep), terminal, editor/diagnostics, web search/fetch, registry
sessions/ persisted multi-session store
ui/ chatController, chatViewProvider (webview), agentsView, chatParticipant,
lmProvider (Language Model API), inlineCompletion, inlineEdit, images,
settingsPanel (Settings / provider manager / setup wizard webview)
mcp/ stdio MCP client
media/ chat.js / chat.css, settings.js / settings.css (webviews), walkthrough/*.md, icons,
localpilot.woff (brand icon font → $(localpilot-logo); rebuild with `npm run icons`)
docs/ this site (Jekyll, GitHub Pages)
Adding a tool
- Create
src/tools/<name>.tsexporting aToolwith a JSON-schemadefinition. - Set
parallelSafe: truefor read-only tools; setapprovalKindfor mutating ones. - Register it in
src/extension.tsand add a label intoolLabel()inmedia/chat.js.
Adding a provider type
Implement LLMProvider (src/types.ts), branch in createProvider() (src/providers/registry.ts), and extend the type enum in package.json.
Releases
Releases are driven by the version in package.json. Whenever main carries a version that has no vX.Y.Z tag yet, the Release workflow releases it. Nothing is tagged until the build is green:
- prepare checks that
package.json,package-lock.json,docs/_config.ymland theCHANGELOG.mdsection agree (npm run check:version) and decides whether the version is new. - verify type-checks, lints, runs the smoke and Extension Host tests, packages
localpilot-X.Y.Z.vsixand checks the version inside the VSIX. - publish creates the
vX.Y.Ztag, the GitHub Release (that version’s changelog section as notes, plus the VSIX, a stablelocalpilot.vsixandSHA256SUMS.txt) and publishes to the VS Code Marketplace and Open VSX when theVSCE_PAT/OVSX_PATsecrets exist. A failed publish fails the run; re-run Publish to Marketplace for that tag to retry without burning a version. Finally it starts the Docs workflow so the website shows the new version (commits pushed by the workflow itself do not trigger push-based runs, so this is done explicitly).
Pushes that do not change the version are checked for releasable commits: if the push contains anything other than docs:, chore:, ci:, build:, test:, style: or release: commits, a bump job runs scripts/release.js on main (patch by default; minor for a breaking type!: commit; or whatever [release patch|minor|major] in a commit message asks for), commits release: vX.Y.Z and starts a new Release run, which tags and publishes it. Put [skip release] in a commit message to hold a push back. Pushes with only housekeeping commits run CI and deploy the docs.
Cutting a release by hand
Only needed to pick a specific version (for example a minor or major bump). Two equivalent ways:
- From GitHub: Actions → Prepare release → Run workflow, pick
patch,minor,majoror a custom version. It bumps, updates the changelog, verifies, commitsrelease: vX.Y.Ztomainand starts the Release workflow. -
From your machine, on a clean
main:npm run release -- patch # or minor | major | 1.2.3 npm run release -- patch --dry-run # show the version and release notes without changing anything
Both run scripts/release.js, which bumps the four version locations, promotes the Unreleased section of CHANGELOG.md to ## X.Y.Z — date, runs compile + smoke + packaging and pushes the commit. Write changelog entries under ## Unreleased as you go; if there are none, the script generates them from the conventional commits since the previous tag (feat: → Added, fix: → Fixed, docs:/site: → Documentation, anything else → Changed; chore:, test:, ci: and release: commits are skipped unless they are all there is). If package.json was already bumped by hand and is untagged, patch/minor/major release that pending version instead of bumping again.
Editing package.json by hand and pushing still works, provided docs/_config.yml matches and the changelog has a ## X.Y.Z section with entries; CI and the Release workflow fail early otherwise. Pushing a tag manually or running Actions → Release → Run workflow for an existing tag is still supported.
Website
The site in docs/ is plain Jekyll (GitHub Pages). The version shown on the site (site.version) is taken from package.json when the Docs workflow builds, so it always matches the released extension; the version: key in docs/_config.yml is only a fallback for local jekyll serve and is kept in sync by npm run release / npm run check:version. Everything else site-wide lives in docs/_config.yml:
| Key | Used by |
|---|---|
google_analytics |
Google tag (gtag.js) in _includes/head.html, plus ask_ai, download_vsix, click_install, click_github and copy_markdown events. |
ask_ai |
The Ask AI buttons (_includes/ask-ai.html) shown under every docs title and in the footer. Each entry is an assistant whose public ?q= parameter receives a pre-filled, page-aware prompt — ChatGPT, Claude, Perplexity and Gemini (via Google AI Mode, since the Gemini app has no prefill parameter). |
topics, works_with |
Footer link columns and the keyword Topics strip (_includes/footer.html). Add a target keyword here so it becomes a crawlable link. |
nav, guides |
Sidebar, footer columns, pager and /llms.txt. |
Docs pages also get Copy page as Markdown (fetches the page source from GitHub and strips the front matter) and View as Markdown links, and /llms.txt lists every page with its description for LLM crawlers.
Contributing
Issues and pull requests are welcome at https://github.com/isathish/localpilot. Please run npm run compile && npm run smoke before opening a PR.