Local-first AI coding agent for VS Code & Cursor. Ollama, LM Studio & your inference fleet. Cursor-grade agent UX — offline, private, zero token cost.
Feels like Cursor. Runs entirely on your hardware. Costs nothing per token.
Nyx in 30 seconds — click for the MP4 version
Open agentic coding models have caught up. DeepSeek V4, GLM 5.2, Qwen Coder —
open weights now deliver flagship-class agentic performance, and quantized
builds run on hardware you can put under your desk: a DGX Spark, a
Mac Studio, a Blackwell workstation, even a 128 GB MacBook. What’s been
missing is the front end: an agent panel that treats those models as
first-class citizens instead of a dropdown afterthought.
Nyx is that panel. A local-first coding agent in its own editor sidebar that
looks and behaves like a native agent panel — file-edit diff cards, checkpoints,
approvals, @-mentions, semantic search, MCP tools, a job queue, project memory —
but every request goes to your machines. No account, no cloud, no telemetry,
no per-token bill.
| Review every change before it lands | Manage your whole inference fleet |
|---|---|
![]() |
![]() |
Status: v0.29.0. Local-first and fully offline-capable (the only optional
network use is web fetching/search, the one-time OCR language-data download,
and the first-time download of a vision/embedding model, all under your control).
The recipes for running flagship-class open models locally are public and
surprisingly convenient. Point any of these at Nyx’s machine manager and you
have a Cursor-grade agent that never leaves your network:
feat/v4-port-cuda fork +ollama pull qwen2.5-coder:32b),llama-server -m model.gguf),The result: agentic coding with zero marginal cost, full privacy, and
hardware you already own doing the work. Nyx handles the part the serving
stacks don’t — the agent UX and the quirks of local models.
There are excellent AI coding extensions — Cline, Roo Code, Continue.dev, Twinny. For all of them, local models are one provider entry in a dropdown: they are designed around cloud frontier models, and running them against Ollama means hand-crafted Modelfiles, brittle tool calling, and agents that derail when a 7B model gets the JSON slightly wrong.
Nyx inverts that: local inference is the product, not the fallback.
/api/show → 🔧 tools / 👁 vision / 🧠 thinking badges), auto-detected context lengths, per-machine temperature/num_ctx — and automatic failover to another machine serving the same model. No other extension has a concept of machines.tool_calls, raw/fenced JSON, function-style text, or DeepSeek DSML markup; arguments get JSON repair; edit_file matches whitespace-tolerantly; small models get a reduced tool set; the prompt prefix stays byte-stable so the server’s KV cache keeps working. The failure modes that break other agents on local models are our core engineering target.If you have serious local hardware and want an agent that treats it seriously, that’s the gap Nyx fills.
ollama pull <model> → it shows up in the picker.qwen2.5-coder:7b pull.git_diff / git_log tools answer “what did I just change?” without run_command approval friction.Cmd/Ctrl+Alt+K → describe the change → review the diff → apply. No chat roundtrip; checkpoints and backups apply as usual.nyx.benchmarkRouting, the judgment-best benchmarked model plans and the edit-precision winner executes.nyx.includeActiveFile.Cmd/Ctrl+Alt+N — both directions). A violet brand identity — ✦ NYX badge, accent line, tabs, meter (configurable via nyx.accentColor) — keeps the two worlds visually distinct; clicking the badge opens About (version, GitHub, update check).read_terminal tool — “fix the error in my terminal” just works.30m), so the model stays loaded and follow-up turns skip the reload pause.~/.cursor/mcp.json / .cursor/mcp.json (stdio + HTTP) and offers their tools to the agent — e.g. codebase-memory-mcp for graph-based code memory. Governed by the same per-tool permissions (mcp:<server>/<tool>).nomic-embed-text, int8-quantized, structure-aware chunking, live file-watcher updates) powers the semantic_search tool: find code by meaning, not just by regex. Works with Ollama and OpenAI-compatible hosts (LM Studio, llama.cpp, vLLM) via a /v1/embeddings fallback; coverage limits are configurable (nyx.indexMaxFiles / nyx.indexMaxChunks) and warn when hit.qwen2.5-coder:7b) — the piece that used to require a second extension. Opt-in via the Nyx Tab status-bar toggle.node .harness/eval.mjs)./api/show: 🔧 tools, 👁 vision, 🧠 thinking — and their true context length is detected automatically.edit_file / write_file approvals show the proposed diff before anything is applied; destructive-looking commands are flagged; Always allow skips future prompts for that tool in the current chat.path +N −M cards with a colored diff preview; click the filename to open it in the editor. Edits go through WorkspaceEdit, so open editors and undo history stay intact.@ in the composer to fuzzy-search workspace files and inline them as context.run_command streams stdout/stderr into the tool card while it runs; background: true starts dev servers & long jobs the agent can poll (check_process) and stop (kill_process) — and Stop actually kills running processes.web_search and fetch_url on its own.edit_file, automatic backups, and a shrink guard so files are never silently destroyed.run_script writes and runs a throwaway bash/python/node script (in a temp dir) to test or verify things..cursor/rules/*.mdc, AGENTS.md, .cursorrules, and SKILL.md files.allow / ask / deny policy. Reads are allowed; edits and shell commands ask first.recall_memory ranks matches with the local embedding index — with keyword search as the offline fallback.reasoning_content, <think>, …) show a collapsible Thought for Ns block above the answer.$…$, $$…$$, \(…\), \[…\]) render as real typeset math via a bundled KaTeX — fully offline.edit_file matching, JSON repair for sloppy tool arguments, tool calls detected even when embedded in prose, automatic retry on transient network errors, and failover to another machine serving the same model.search_files uses the ripgrep binary shipped with the editor (with a JS fallback).ask_user(...)) in the model output are detected automatically.ollama pull qwen2.5-coder:32b # great on a high-memory Mac Studio
# lighter option:
ollama pull qwen2.5-coder:7b
One-line install (macOS / Linux) — detects Cursor and VS Code, downloads the
latest release (SHA-256 verified), installs, done:
curl -fsSL https://raw.githubusercontent.com/sthamann/nyx-local-ai/main/install.sh | bash
Windows (PowerShell):
irm https://raw.githubusercontent.com/sthamann/nyx-local-ai/main/install.ps1 | iex
Options (append after bash -s --): --editor=cursor|code|all,
--version=vX.Y.Z, --vsix=<local file>, --from-source. If no release
exists yet, the script automatically falls back to cloning and building from
source (needs Node ≥ 18).
npm install
npm run build # bundles the extension + webview
npm run package # produces nyx-local-ai-0.29.0.vsix
Install into Cursor:
cursor --install-extension nyx-local-ai-0.29.0.vsix --force
Or VS Code:
code --install-extension nyx-local-ai-0.29.0.vsix --force
If the cursor CLI is not on your PATH, use the full binary path, e.g. on macOS:
"/Applications/Cursor.app/Contents/Resources/app/bin/cursor" \
--install-extension nyx-local-ai-0.29.0.vsix --force
Releases are produced by CI (.github/workflows/release.yml): pushing a v*
tag builds the .vsix and attaches it as nyx-local-ai.vsix together with
checksums.txt — exactly what the installers download and verify.
Staying up to date: Nyx checks GitHub releases once a day (a single
anonymous API call — disable with nyx.updateCheck) and offers a one-click
in-editor update; Nyx: Check for Updates runs the check on demand.
Then reload the window (Cmd/Ctrl+Shift+P → Developer: Reload Window) and click the Nyx icon in the Activity Bar (or the Nyx entry in the status bar).
When no model is found, the empty state doesn’t just point at the docs — it
diagnoses the setup and offers one-click fixes: (1) is Ollama reachable at
nyx.ollamaUrl? (2) is a coding model installed? If not, Pull
qwen2.5-coder:7b downloads one straight from the panel (one-time, a few GB).
(3) once models exist, it offers to build the semantic index. All checks
re-run live as you fix things; the ↻ Re-scan models button re-probes
everything.
Open it via the ⚙️ button in the sidebar.
ollama / lmstudio / openai), and its base URL. Use Test to probe it and discover the models it serves (per-host; tries both native and OpenAI-compatible listing endpoints).num_ctx for Ollama).Machines are stored in the nyx.machines setting and managed entirely from the UI. API keys are kept in the editor’s SecretStorage (OS keychain) — never in settings; existing plaintext keys are migrated automatically on first use.
Nyx can live in the left Activity Bar (default), the secondary side bar on the right — or as a full editor tab next to Cursor’s own agent window.
Ctrl+Tab). Sidebar and tab show the same live session.Cmd/Ctrl+Alt+N jumps into Nyx — and pressed inside Nyx, jumps back to your code. One key, both directions (Nyx: Toggle Nyx Focus).nyx.accentColor.nyx.maxAgentSteps.For multi-step tasks the agent maintains a visible plan: set_plan renders
a pinned card above the transcript with ○ pending / ▸ active / ✓ done steps and
a progress counter, updated as the agent works. The plan is persisted with the
chat and restored when you reopen it — so you always know where a long-running
task stands.
The agent can drive a headless browser — using the Chrome/Edge you already
have installed (via playwright-core, no browser download): browser_navigate
opens a page and returns its text plus numbered interactive elements;
browser_click(ref) / browser_type(ref, text, submit?) interact with them;
browser_snapshot re-reads the page and browser_screenshot runs the shot
through the local vision toolchain so the agent can see the result. Perfect
for “start the dev server and check the page renders” loops. Navigation,
clicks, and typing require approval by default; page content is wrapped as
untrusted data. Custom binary via nyx.browserExecutable.
Inline ghost-text completions from a small local model — the missing piece
that previously required a second extension (Twinny/Continue). Opt-in via
nyx.autocompleteEnabled or the Nyx Tab status-bar toggle:
/api/generate with suffix), so any FIM-capableqwen2.5-coder:7b (default, ~200–300 ms), codellama,starcoder2, codegemma.nyx.helperOllamaUrl, or thenyx.autocompleteOllamaUrl override; defaults to your main Ollama), soollama pull qwen2.5-coder:7b # then: Nyx: Toggle Tab Autocomplete
Which of your machines/models should be the daily driver? .harness/eval.mjs
benchmarks any OpenAI-compatible endpoint on the skills Nyx actually needs:
node .harness/eval.mjs --url http://localhost:11434/v1 --model qwen2.5-coder:32b
node .harness/eval.mjs --url http://192.168.1.77:8888/v1 --model deepseek-v4-flash --rounds 3
It scores tool-call success (5 tasks incl. an ask-user trap), edit
precision (edit_file calls are actually applied via Nyx’s fuzzy matcher and
verified against expected output), and bug-judgment accuracy incl. the
false-positive rate on correct-but-suspicious-looking snippets (closure
timing, delta indentation — the classic LLM traps). Uses the real Nyx system
prompt, tool schemas, and tool-call parser, so scores reflect in-product
behavior. Example (DeepSeek V4 Flash on a DGX Spark cluster): 80% tool calls,
67% edits, 83% judgment, 33% FP rate, ~1.3 s/request.
Or benchmark from the UI: ⚙ Manage models → edit a machine → Benchmark
next to any model runs a compact 9-request version in-product and pins the
scores as a chip (🔧 tools ✏ edits 🧠 judgment; hover for FP rate and latency).
Results are stored, so you can compare machines side by side.
Setup recommendation: after each benchmark, Nyx turns the stored scores
into a concrete proposal — “model X as daily driver, Y as utility model, Z
for autocomplete” — shown in the machine editor with an Apply setup
button that sets all three at once (selection, nyx.utilityModel,
nyx.autocompleteModel) instead of three manual settings.
Nyx’s system prompt distills the battle-tested conventions from production
agents (Cursor, Claude Code, Windsurf — see the public
leaked-system-prompts
collection) into a ~1.3k-token prompt tuned for local models: communication
style (markdown, direct, no tool-name leakage, no prompt disclosure), working
rules (read before edit, no unsolicited files/docs, preserve indentation,
3-attempt linter cap, non-interactive shell flags, no hardcoded secrets),
verify-before-report, plan discipline, and the JSON tool-call contract.
Deliberately compact: big-agent prompts run 5–10× larger, which wastes context
and slows every request on local hardware. Extend it with your own
instructions via nyx.systemPromptAppend (global) or .cursor/rules/*.mdc
(per project) — both stay prompt-cache-stable within a session.
Local models love to “find” bugs by pattern-matching code they’ve only read.
Nyx’s system prompt enforces an evidence discipline: before reporting a bug or
any runtime-behavior claim, the agent must reproduce it with a minimal
run_script/run_command test and quote the actual output; claims that can’t
be executed must be labeled unverified hypothesis, and observations are kept
separate from inferences. In our own dogfooding this discipline would have
eliminated 4 of 7 false positives in a model-written bug report (see BUG.md).
Nyx exposes a broad, Cursor-like tool set. Default permissions:
| Tool | Purpose | Default |
|---|---|---|
read_file |
Read a file (images/PDFs auto-converted to text) | allow |
list_dir |
List a directory | allow |
search_files |
Regex content search | allow |
semantic_search |
Meaning-based code search (local embedding index) | allow |
find_files |
Fuzzy filename / glob search | allow |
file_outline |
Class/function outline with line ranges (language server) | allow |
find_symbol |
Workspace-wide symbol search (language server) | allow |
find_references |
All references to a symbol (language server) | allow |
format_file |
Run the configured formatter on a file | allow |
http_request |
GET/POST/… against local APIs & dev servers | ask |
wait |
Pause up to 30 s (dev-server boot etc.) | allow |
get_diagnostics |
Linter/compiler errors & warnings | allow |
git_diff |
Uncommitted git changes + short status (read-only) | allow |
git_log |
Recent commit history (read-only) | allow |
read_terminal |
Visible output of the active integrated terminal (read-only; ask on Safe) | allow |
fetch_url |
Fetch text of an http(s) URL | allow |
web_search |
Search the web (DuckDuckGo) | allow |
recall_memory |
Search project memory of past sessions | allow |
save_memory |
Record a durable outcome | allow |
read_rule |
Load a project rule by name | allow |
use_skill |
Load a skill’s full instructions | allow |
ask_user |
Ask a clarifying question | allow |
set_plan |
Show/update the visible task plan | allow |
write_file |
Create/overwrite a file (with diff + backup) | ask |
edit_file |
Targeted search/replace edit (with diff + backup) | ask |
delete_file |
Delete a file (to trash + backup) | ask |
rename_file |
Rename/move a file | ask |
run_command |
Run a shell command (streams output; background: true for long-running jobs) |
ask |
run_script |
Write & run a throwaway test script (bash/sh/zsh/python/node) | ask |
check_process |
Poll a background process (status + output) | allow |
kill_process |
Stop a background process | allow |
browser_navigate |
Open a URL in the headless browser | ask |
browser_snapshot |
Read the current page (text + elements) | allow |
browser_click / browser_type |
Interact with page elements | ask |
browser_screenshot |
Screenshot + vision description | allow |
browser_close |
Close the headless browser | allow |
mcp_<server>_<tool> |
Any tool from a connected MCP server | ask |
“What did I just change?” shouldn’t need a shell approval: git_diff
(working tree vs. HEAD, optional staged/path filters, plus a short
git status) and git_log (recent commits) are read-only tools with
allow-default — the agent sees your uncommitted work instantly, while
mutating git operations (commit, push, reset) still go through
run_command with the usual approval.
Cmd/Ctrl+Alt+K)Select code → Nyx: Quick Edit Selection (keybinding Cmd/Ctrl+Alt+K, or
right-click → Quick Edit Selection) → type an instruction (“add error
handling”, “convert to async/await”) → the active model rewrites exactly the
selection. You review the +/− diff in a native confirmation before
anything is applied; the edit then runs through the standard machinery —
checkpoint, backup, WorkspaceEdit, shrink guard — so it shows up in the
review view and is revertible like any agent edit. No chat roundtrip, no
context ceremony.
One switch instead of a JSON file: the autonomy selector next to the mode
toggle picks how much the agent may do without asking —
fetch_url, web_search, browser reads) asks first.For fine-tuning, nyx.toolPermissions maps any tool name to allow, ask, or
deny and always wins over the preset. Use * as a fallback. In an
approval card, Always allow whitelists that tool for the rest of the
current chat. MCP tools are governed per-tool (mcp:<server>/<tool>) or
per-server (mcp:<server>). Example:
"nyx.toolPermissions": {
"run_command": "deny",
"write_file": "allow",
"mcp:shopware": "ask",
"*": "ask"
}
Nyx is an MCP client. On every model refresh it reads:
~/.cursor/mcp.json (your global Cursor MCP config),<workspace>/.cursor/mcp.json (workspace overrides), andnyx.mcpServers setting (same shape),connects over stdio (command/args/env) or streamable HTTP (url/headers), and offers every discovered tool to the agent as mcp_<server>_<tool>. Tool calls stream through the same approval flow; permissions default to ask and are configured via mcp:<server>/<tool> or mcp:<server> keys. Special-token sanitization and output truncation apply to MCP results too. Toggle with nyx.mcpEnabled.
Works out of the box with servers like codebase-memory-mcp (graph-based code memory: index_repository, search_code, query_graph, trace_path, …) — if it’s in your Cursor config, Nyx picks it up automatically.
semantic_search(query) finds code by meaning — “where is authentication handled?” — even when no keyword matches. Fully local:
nyx.embeddingModel, default nomic-embed-text, auto-pulled on first use via Ollama); vectors are int8-quantized and persisted in workspace storage (a 2,500-file repo stays in the tens of MB)./api/embed, Nyx falls back to the OpenAI-compatible /v1/embeddings endpoint — so LM Studio, llama.cpp server, and vLLM users get semantic search as well.nyx.indexMaxFiles files / nyx.indexMaxChunks chunks (defaults 2,500 / 12,000). When a limit is hit, the status line says so explicitly — raise the settings to cover a bigger repo instead of silently searching half the codebase.semantic_search for conceptual questions and search_files (ripgrep) for exact strings.Disable with nyx.semanticIndexEnabled. For graph-aware retrieval (call paths, architecture queries), pair it with codebase-memory-mcp via MCP.
On every request Nyx injects:
.cursor/rules/*.mdc (with frontmatter), plus AGENTS.md and .cursorrules at the workspace root.SKILL.md) discovered from the workspace and from global roots (~/.cursor/skills-cursor, ~/.claude/skills, ~/.agents/skills, ~/.cursor/plugins/cache).The agent can pull a full rule or skill on demand via read_rule / use_skill.
Put an http(s) link in your message and Nyx fetches it before the agent runs (up to 3 URLs). It extracts readable page text and describes up to two images per page through the vision/OCR toolchain, then adds everything to context. Direct image URLs are described too. Toggle with nyx.autoFetchUrls. The agent can also fetch on its own via fetch_url.
web_search(query) runs a keyless DuckDuckGo search and returns titles, URLs, and snippets; the agent then reads a result with fetch_url. No API key or account.
Several layers protect your files:
read_file shows large files partially and reports total size; the agent pages through with { offset, limit } instead of relying on a truncated read.edit_file — targeted search/replace edits operate on the full on-disk content, so the rest of a big file is preserved.run_script(language, code) writes a script to a temp directory, runs it in the workspace root, returns stdout/stderr, and deletes it — so quick tests and verifications never clutter your project. Supports bash, sh, zsh, python, node.
A Context % meter shows how full the model’s context window is — using the machine’s configured context length, the model’s advertised size (a 1M model shows a 1M budget), or nyx.contextTokens as fallback. Click the meter for a breakdown (system prompt vs. conversation vs. tool results) with a Compact now action. Space is reclaimed in two stages: at 60% of the budget, old tool outputs are trimmed (the last few stay verbatim — usually avoids the expensive step entirely); at nyx.compactThreshold (default 75%), older turns are summarized by the model. nyx.maxOutputTokens (default 8192) caps each single generation so a runaway model can’t flood the session. On top of that, Nyx watches the live stream and cuts off a degenerate repetition loop (a word or phrase repeated over and over — common on small local models) the moment it detects one, keeping the partial reply and posting a status note instead of spending minutes on garbage.
Attach context for the next message via:
@-mentions — type @ plus a filename fragment in the composer; pick a suggestion with ↑/↓ + Enter,
the 📎 button in the composer,
right-click a file/folder in the Explorer → Add to Nyx context,
select code in the editor → right-click → Add Selection to Nyx (or Cmd/Ctrl+Alt+L),
the editor tab context menu → Add to Nyx context,
drag files from Finder / the OS file manager directly onto the chat — the webview can’t see OS paths, so Nyx captures the bytes (up to 10 files, 25 MB each), stores them in extension storage, and attaches from there,
drag files onto the Attach files (drop here) strip below the chat (no Shift needed),
or drag from the editor’s Explorer onto the chat panel while holding Shift (editor-internal drags only reach webviews with Shift — a VS Code limitation).
paste an image from the clipboard (screenshot → Cmd/Ctrl+V in the composer).
Text files are inlined (truncated if large), folders are listed, and media files run through the vision/PDF toolchain. If the active model advertises vision support, attached images are passed to it natively (base64) instead of being described by the helper vision model.
The composer also keeps a prompt history: press ↑ (with the caret at the start or an empty input) to recall earlier prompts shell-style, ↓ to walk back to your draft.
The file you are editing rides along automatically: each message includes the
path plus ±30 lines around your cursor of the last active editor — so
“fix this” just works without attaching anything. Deliberately tiny (local
models have tight context budgets), skipped when you already @-mentioned
the file, and disabled entirely via nyx.includeActiveFile.
“Fix the error in my terminal” — two ways to make that work:
read_terminal tool returns the sameBoth capture via the editor’s select-all/copy commands (VS Code has no stable
buffer API), restore your clipboard afterwards, and tail-truncate long output.
Each agent request carries a compact map of the workspace — directories
with their files, shallow levels first, hard-capped at roughly 400 tokens.
Especially small local models stop guessing paths and jump straight to
read_file/edit_file with correct locations, saving whole tool roundtrips.
Complementary to the semantic index (meaning vs. structure); cached for a few
minutes and rebuilt automatically. Disable with nyx.repoMap.
So even non-vision models can work with images and PDFs:
unpdf.tesseract.js) and a description from a local vision model (default moondream, auto-pulled via Ollama on first use).Configurable via nyx.visionModel, nyx.autoInstallVisionModel, nyx.enableOcr.
Vision, embeddings, and autocomplete are “helper” workloads that usually live
on one box. Instead of three URLs, set nyx.helperOllamaUrl once (empty =
your main nyx.ollamaUrl) and all helpers use it. The legacy per-feature
settings (nyx.visionOllamaUrl, nyx.embeddingOllamaUrl,
nyx.autocompleteOllamaUrl) keep working as explicit overrides for
existing setups — no breaking change, just one knob instead of three.
Ollama unloads models after ~5 minutes of idle — so the first message after a
pause pays the full model-load cost again. After every turn Nyx re-arms the
keep-alive timer of the model it just used (default 30m, via the
documented preload call), keeping weights and KV cache hot between turns.
Configure the duration with nyx.keepAlive (-1 = keep loaded forever,
empty = off). Ollama machines only; the call appears in the privacy report as
model keep-alive.
While generating, a tok/s indicator shows a live estimate and then an accurate figure from the server’s usage data (when provided).
When a model streams reasoning separately — via a reasoning_content / reasoning field (DeepSeek, etc.) or <think>…</think> tags in the content — Nyx shows a collapsible Thinking… block above the answer. When generation finishes, it collapses to Thought for Ns.
LaTeX formulas in answers render as real typeset math: $$…$$ and \[…\] as display blocks, \(…\) and $…$ inline. Rendering uses a bundled KaTeX (stylesheet + fonts ship inside the extension — no CDN, fully offline) and is tokenized before markdown parsing, so underscores and backslashes inside formulas survive. Dollar amounts like “$5 and $10” are left alone, and invalid TeX falls back to plain text instead of erroring.
When a request is ambiguous, the model calls ask_user and you answer inline via single-choice, multiple-choice (with an optional “Other…” field), or free text. The Q&A is saved in the transcript.
While the agent is busy, pressing Enter queues your message instead of sending it. The queue panel above the composer lets you reorder, edit, and remove items, or clear them. Jobs run one after another.
Queue up N tasks, hit ▶ Run all (or Nyx: Run Queued Jobs (Batch)), and
walk away: the jobs run sequentially in the extension host — the panel
doesn’t need to stay visible — and when the queue drains, Nyx posts a
session report into the chat: per-job status and duration, the net
diff (files, +/− lines), and a pointer to the review view for per-file
revert or one-click commit. A native notification fires when the batch ends.
nyx.batchVerifyCommand (e.g. npm test) and every job is verifiednyx.grindMaxIterations);write_file / edit_file first show the proposed diff in the approval card — you approve what you can see, then it is applied via WorkspaceEdit (open editors and undo history stay intact). Applied edits render as cards showing the path, a +added −removed badge, and a colored diff preview; click the filename to open the file.
Approval cards and every file in the review view carry an Open diff link
that opens the editor’s real side-by-side diff — the checkpoint original
(session start) on the left, the current disk state on the right, served
through a virtual-document provider. Full syntax highlighting, word-level
diffing, and familiar navigation instead of a text preview.
Utility work shouldn’t occupy your big model: chat titles, commit messages,
and the project-memory distillation are automatically routed to the
smallest reachable ≤8B model (e.g. a local qwen2.5-coder:7b) while the
heavyweight on the DGX keeps thinking. Pin a specific model with
nyx.utilityModel — when set explicitly, context compaction routes there
too. Without a small model everything falls back to the active one.
Benchmark-based multi-model routing (opt-in): enable
nyx.benchmarkRouting and Nyx uses your stored benchmark scores to split a
run across machines — the judgment-best model handles the first
plan/reasoning turn, the edit-precision winner executes the tool steps.
Requires at least two benchmarked, reachable models; a status line shows the
chosen route, and the machine-failover mechanism keeps applying per turn.
No benchmarks, no magic — your selected model is used as-is.
The killer economics of local inference: iteration costs nothing. Type
/grind npm test
and Nyx enters a fix-until-green loop: it captures the failing output,
lets the agent fix the code, then runs the command itself after every
agent turn — feeding fresh failures back (with a “don’t repeat what didn’t
help” instruction) until the command passes or nyx.grindMaxIterations
(default 8) is reached. Every iteration gets its own checkpoint, Stop
aborts the loop, and if the command already passes Nyx tells you instead of
burning cycles. Combine with 🚀 Autopilot autonomy and let your own hardware
grind while you get coffee — no per-token bill.
The ✎ N changed chip next to the context meter opens the review view: the
net diff of the whole chat (checkpoint originals vs. disk), one card per
file with +/− badge, new/deleted markers, diff preview, and a
Revert button per file (plus Revert all). Commit stages exactly the
session’s files, generates a conventional commit message from the staged diff
with a local model, and commits (git commit -F, no shell-escaping issues) —
the status line reports the short hash.
Each user message starts a checkpoint. Hovering a message reveals ↩ Edit & rerun: Nyx restores every file the agent changed after that point, rewinds the conversation, and places the original message in the composer for editing. After an error, a Retry button does the same and re-sends automatically. Checkpoints are persisted with the chat.
Per project, Nyx distills each session’s key outcomes (goal, result, changed files) and stores them locally. The distillation is a real utility-model call (goal + outcome + gotchas in 2–4 sentences, routed to the small model) with the last assistant answer as offline fallback. New sessions automatically receive a compact digest, and the agent can:
recall_memory(query?) — search past work; matches are ranked by the local embedding model (same host as the semantic index), so “auth work” finds the “login flow refactor” — with keyword ranking as offline fallback,save_memory(title, summary) — record a durable outcome.Browse, delete, or clear memories from the 🧠 view in the sidebar. Toggle with nyx.memoryEnabled; control the injected count with nyx.memoryInject.
Recent chats live in an always-visible tab strip under the title bar — click to switch instantly (browser-tab style, with a + for a new chat, × on hover to delete, and an overflow counter that opens the full history). Right-click a tab for a context menu with Close chat and Close other chats — closing deletes the chats, so Close other chats asks for confirmation first. Toggle the strip with the ⧉ button; the preference is remembered. Switching is blocked while a job runs so you never abort one by accident.
For the full archive, open History (☰): chats grouped by date (Today, Yesterday, …), searchable by title/model/machine/mode, with edit stats (+/− lines, files changed) and the model shown as a subtle chip. Switching chats restores the full transcript, tool cards, and task plan. Up to 50 sessions are kept per workspace.
After the first exchange, Nyx asks the model for a short 3–6 word title and names the chat automatically (shown in the top bar and in History). Falls back to the first message if generation is unavailable. Toggle with nyx.autoTitle.
Hover a finished answer and a ⧉ Copy action appears under it, opening a
three-format menu:
Code blocks additionally keep their own per-block Copy button.
Two ways out of a session: Nyx: Export Chat as Markdown saves the full
transcript (messages, collapsible tool cards, Q&A) as a .md file, and
Nyx: Copy Chat as Markdown (Handoff) puts the same Markdown straight on the
clipboard — paste Nyx’s local groundwork into Cursor’s cloud agent, a PR
description, or an issue without touching the filesystem.
And one way in: copy a conversation (e.g. from Cursor’s agent) and run
Nyx: Import Handoff from Clipboard — the text is attached as a 📥 chip and
rides along as context with your next message, so Nyx continues the work
locally. Typical split: the cloud agent plans or reviews, Nyx executes
privately on your own hardware — or the other way around.
The 🛡 button next to the context meter opens the network log of the current
session: every contacted host with its purpose (model inference,
embeddings, vision/OCR, web search, URL fetches, model downloads) and request
count. On a fully local setup you’ll see nothing but your own machines —
which is exactly the point: the “no cloud calls” promise becomes something
you can verify, not just believe. The log lives in memory per session and
is never persisted or transmitted.
Many local models emit tool calls as text in the assistant content instead of the OpenAI tool_calls field. Nyx detects and converts these automatically:
{"name": "read_file", …}), even embedded in prose,ask_user(question="…", type="single", options=[…]),function_calls / V4 tool_calls blocks, <|DSML|invoke …>), including gateways that pass the raw markup through — special tokens are stripped from the visible answer.Prefer a coding/tool-tuned model for best results.
| Setting | Default | Purpose |
|---|---|---|
nyx.ollamaUrl |
http://localhost:11434 |
Ollama server base URL |
nyx.lmStudioUrl |
http://localhost:1234 |
LM Studio server base URL |
nyx.machines |
[] |
Labeled endpoints (managed via the UI) |
nyx.customEndpoints |
[] |
Legacy extra OpenAI-compatible endpoints (prefer nyx.machines) |
nyx.autonomy |
balanced |
safe / balanced / autopilot — how much runs without asking |
nyx.toolPermissions |
{} |
Per-tool allow / ask / deny overrides; MCP tools via mcp:<server>/<tool> or mcp:<server> |
nyx.maxAgentSteps |
25 |
Max tool iterations per task (a Continue button appears at the limit) |
nyx.maxOutputTokens |
8192 |
Hard max_tokens cap per generation (0 = off) |
nyx.toolProfile |
auto |
full, reduced (core tools only — better for ~7B models), or auto by model size |
nyx.systemPromptAppend |
"" |
Extra instructions appended to the agent’s system prompt |
nyx.allowPrivateNetworkFetch |
false |
Allow fetch_url to reach localhost / private-network hosts (SSRF guard) |
nyx.contextTokens |
16384 |
Assumed context window when a machine has none set |
nyx.compactThreshold |
0.75 |
Fraction of context that triggers auto-compaction |
nyx.visionModel |
moondream |
Local vision model for image descriptions |
nyx.visionOllamaUrl |
http://localhost:11434 |
Override: Ollama host for the vision model (default = helper host) |
nyx.autoInstallVisionModel |
true |
Auto-download the vision model on first use |
nyx.enableOcr |
true |
Run offline OCR on images (in addition to the vision model) |
nyx.autoTitle |
true |
Auto-name chats after the first exchange |
nyx.autoFetchUrls |
true |
Fetch URLs in your message (text + described images) |
nyx.memoryEnabled |
true |
Remember key outcomes per project |
nyx.memoryInject |
5 |
Recent memories injected into a new session |
nyx.includeActiveFile |
true |
Auto-include the active file (±30 lines around the cursor) as context |
nyx.repoMap |
true |
Inject a compact workspace map (~400 tokens) into the agent’s system context |
nyx.keepAlive |
30m |
Keep the model loaded on Ollama between turns (warm KV cache); empty disables |
nyx.accentColor |
"" |
Brand accent color of the Nyx panel (any CSS color; empty = Nyx violet) |
nyx.semanticIndexEnabled |
true |
Local embedding index for semantic_search |
nyx.embeddingModel |
nomic-embed-text |
Embedding model (auto-pulled via Ollama) |
nyx.embeddingOllamaUrl |
http://localhost:11434 |
Override: host for embeddings (default = helper host) |
nyx.indexMaxFiles |
2500 |
Max files in the semantic index (status warning when hit) |
nyx.indexMaxChunks |
12000 |
Max chunks in the semantic index (status warning when hit) |
nyx.helperOllamaUrl |
"" |
Shared host for helper workloads — vision, embeddings, autocomplete (empty = nyx.ollamaUrl) |
nyx.batchVerifyCommand |
"" |
Command run after every batch-queue job, fix-until-green (empty = off) |
nyx.benchmarkRouting |
false |
Opt-in multi-model routing from benchmark scores (plan vs. edit turns) |
nyx.mcpEnabled |
true |
Connect to configured MCP servers |
nyx.mcpServers |
{} |
Extra MCP servers (same shape as Cursor’s mcp.json) |
nyx.autocompleteEnabled |
false |
Tab autocomplete via a local FIM model (opt-in) |
nyx.autocompleteModel |
qwen2.5-coder:7b |
FIM-capable Ollama model for completions |
nyx.autocompleteOllamaUrl |
"" |
Override: Ollama host for autocomplete (empty = helper host) |
nyx.autocompleteMaxTokens |
160 |
Max tokens per inline completion |
nyx.browserExecutable |
"" |
Chromium binary for browser tools (empty = auto-detect Chrome/Edge) |
A full manual test pass. Assumes Ollama is running with a coding model such as
qwen2.5-coder:32b.
npm install && npm run build && npm run package
cursor --install-extension nyx-local-ai-0.29.0.vsix --force
Then Developer: Reload Window and open the Nyx icon.
Faster dev loop: run
npm run watchand use Run → Start Debugging (F5)
to launch an Extension Development Host if you add a launch config; otherwise
the build-install-reload loop above is the reliable path.
write_file when prompted.hello.md +N −0. Click the filename → it opens in the editor.edit_file produces a small diff.list_dir), “Search for TODO” (search_files), “Find files named provider” (find_files), “Any diagnostics in this file?” (get_diagnostics).node -v” → you get an approval prompt (default ask)."nyx.toolPermissions": { "run_command": "deny" } and retry → it’s blocked.moondream download)..cursor/rules/style.mdc with a rule (e.g. “Always add a top-of-file comment”). Ask Nyx to create a file and confirm it follows the rule.read_rule.ask_user and present choices. Answer and confirm it proceeds accordingly.recall_memory.save_memory entry appears (badge: saved)."nyx.autoFetchUrls": false to compare.web_search, then fetch_url on a result, and answers with a link.run_script; you get the script’s stdout, and no file is left in the project.read_file with paging and edit_file (a small diff), leaving the rest intact.backups/ folder (recoverable). A drastic shrink shows a ⚠ warning in the tool result.@sidebar (or any filename fragment) → pick a suggestion with ↑/↓ + Enter → ask about the file; its content is included automatically.python3 -m http.server 8765 in the background, check it, then stop it.” → run_command(background), check_process, kill_process; a Stop button also appears on the card.sleep 60, approve, then press Stop → the process dies immediately (no 2-minute hang), and any pending approval card is resolved."nyx.maxAgentSteps": 2, give a bigger task → a Continue button appears when the limit is hit.nomic-embed-text and indexes the workspace; watch the status line).semantic_search and lands in permissions.ts without any keyword match.~/.cursor/mcp.json (e.g. codebase-memory-mcp). Open Nyx → the status line reports MCP: N tool(s) from M server(s).mcp:codebase-memory-mcp/index_repository, then the tool result. Set "nyx.toolPermissions": { "mcp:codebase-memory-mcp": "allow" } to skip prompts for that server.ollama pull qwen2.5-coder:7b, run Nyx: Toggle Tab Autocomplete (status bar shows Nyx Tab), open a code file and start typing → ghost-text completions appear; Tab accepts.browser_navigate; the agent reads the page snapshot and can click/type via element refs.+ for a new chat, × (on hover) to delete, ⧉ in the title bar to hide/show the strip. While a job runs, switching is blocked with a hint instead of aborting the run.git_diff (no approval prompt) and summarizes your working-tree diff.git_log returns the one-line history.Cmd/Ctrl+Alt+K, type “add a doc comment” → a native confirmation shows the +/− diff; Apply writes the change (checkpointed & backed up). Check the ✎ review view — the file is listed and revertible."nyx.batchVerifyCommand": "node -e \"process.exit(0)\"" and rerun → each job is marked ✅ verified."nyx.includeActiveFile": false and it no longer does."nyx.benchmarkRouting": true and start an agent task → the status line reports which model plans and which one executes.\( … \) / $$ … $$ text. A sentence like “costs $5 and $10” stays plain text.Cmd/Ctrl+Alt+N in a code file → focus jumps to Nyx; press it again inside Nyx → focus returns to the editor.ls or a failing command), then run Nyx: Attach Terminal Output to Nyx → a ⌘ chip with the terminal name appears; ask “What does my terminal say?” and the answer references it.read_terminal (auto-allowed on Balanced, asks on Safe).find_files detour (the map is in its system context). Set "nyx.repoMap": false and it searches again.ollama ps in a terminal → the model shows an UNTIL ~30 minutes in the future (default nyx.keepAlive: "30m"). The 🛡 privacy report lists the host with purpose model keep-alive.ollama serve / LM Studio server is running and the URL matches settings; click ↻. For remote machines, use Manage models → Test to probe the host.moondream; watch the status line, or pre-pull with ollama pull moondream.~/.cursor/extensions/local.nyx-local-ai-* and reinstall.Everything runs against your local endpoints. Nyx has no accounts and no
telemetry. The only outbound network calls are:
fetch_url/web_search tools, or auto-fetching links in your message (nyx.autoFetchUrls),run_command / run_script (only what the agent runs, with your approval),nyx.autoInstallVisionModel is on).The 🛡 privacy report in the sidebar lists every host the current session
actually contacted (with purpose and count), so this list is verifiable at
runtime.
Hardening:
fetch_url refuses localhost / private-network addresses unless nyx.allowPrivateNetworkFetch is enabled (SSRF guard).Chats and session history are stored as JSON files in the extension’s workspace
storage; project memory lives in the workspace state.
| Command | Purpose |
|---|---|
| Nyx: New Chat | Start a fresh conversation |
| Nyx: Refresh Local Models | Rescan Ollama / LM Studio / configured machines |
| Nyx: Open Nyx in Editor Tab | Open Nyx as a full editor tab, next to Cursor’s agent |
| Nyx: Toggle Nyx Focus | Jump into Nyx — or back to the editor (Cmd/Ctrl+Alt+N) |
| Nyx: Import Handoff from Clipboard | Attach a copied conversation as context for the next message |
| Nyx: Attach Terminal Output to Nyx | Attach the active terminal’s visible output |
| Nyx: Move Nyx to the Right Side Bar | Open the view-move picker for the secondary side bar |
| Nyx: Show Panel | Reveal and focus the Nyx chat view |
| Nyx: Reset Panel Location | Reset all view locations and restore Nyx to the left |
| Nyx: Add to Nyx context | Attach the selected Explorer/editor file |
| Nyx: Attach Active File to Nyx | Attach the currently open editor file |
| Nyx: Add Selection to Nyx | Attach the current editor selection (Cmd/Ctrl+Alt+L) |
| Nyx: Attach Files or Folders… | Open the native file/folder picker |
| Nyx: Build/Update Semantic Index | Index new/changed files for semantic_search |
| Nyx: Rebuild Semantic Index from Scratch | Drop and re-embed the whole index |
| Nyx: Toggle Tab Autocomplete | Turn inline FIM completions on/off |
| Nyx: Check for Updates | Query GitHub releases and offer a one-click update |
| Nyx: Export Chat as Markdown | Save the current chat (incl. tool cards) as a .md file |
| Nyx: Copy Chat as Markdown (Handoff) | Copy the whole chat to the clipboard for handoff |
| Nyx: Quick Edit Selection | Inline-edit the selection with an instruction (Cmd/Ctrl+Alt+K) |
| Nyx: Run Queued Jobs (Batch) | Run all queued jobs sequentially with a final report |
Default keybindings: Cmd/Ctrl+Alt+N toggle focus (editor ↔ Nyx) · Cmd/Ctrl+Alt+Shift+N new chat · Cmd/Ctrl+Alt+L add selection · Cmd/Ctrl+Alt+K quick edit.
npm run build # one-off bundle (extension + webview)
npm run watch # rebuild on change
npm run typecheck # tsc --noEmit
npm run package # create the .vsix
Project layout:
src/
extension.ts # activation, commands, keybindings, drop zone, status bar
provider/
SidebarProvider.ts # webview host: runs, queue, checkpoints, approvals
SessionStore.ts # file-based chat persistence (one JSON per session)
context.ts # attachments, @-mentions, URL auto-fetch (vision-aware)
networkLog.ts # per-session privacy report (contacted hosts)
DropZoneProvider.ts # no-Shift drop target
agent/
agent.ts # tool-calling loop, retry/failover, compaction
tools.ts # tool implementations (ripgrep search, WorkspaceEdit, fuzzy edits)
toolSchemas.ts # tool schemas + reduced profile for small models
processes.ts # killable/streaming/background command execution
checkpoints.ts # per-message file snapshots & restore
permissions.ts, memory.ts, diff.ts
models/
client.ts # OpenAI-compatible streaming + tool-call parsing/repair
discovery.ts # model discovery + capability probing (/api/show)
machines.ts # machine store (settings + SecretStorage keys)
hosts.ts # helper-host resolution (vision/embeddings/autocomplete)
eval/
benchmark.ts # in-product 9-request model benchmark
routing.ts # benchmark-based setup advice + multi-model routing
mcp/client.ts # MCP client (stdio + streamable HTTP), Cursor mcp.json configs
context/ # rules, skills, media (vision/PDF), web fetch/search
semanticIndex.ts # local embedding index (Ollama, int8, incremental)
types.ts # shared types (host ⇄ webview messages)
webview/ # sidebar UI, split into modules
main.ts # message dispatcher & wiring
transcript.ts # bubbles, tool cards, approvals, questions, thinking
composer.ts # input, queue, attachments, @-mentions, drag&drop
history.ts, machines.ts, markdown.ts, dom.ts, state.ts
media/main.css # theme-aware styles
.harness/ # dev helpers (smoke.mjs: logic smoke tests)
promo/ # HyperFrames source of the 30-second promo video
MIT