Download Latest Version nanocoder v1.31.0 source code.zip (4.3 MB) Google Add to Preferred Sources
Home / v1.31.0
Name Modified Size InfoDownloads / Week
Parent folder
nanocoder v1.31.0 source code.tar.gz 2026-09-26 3.7 MB
nanocoder v1.31.0 source code.zip 2026-09-26 4.3 MB
README.md 2026-09-26 111.4 kB
Totals: 3 Items   8.1 MB 1

What's Changed

  • Added a first-class provider template for Cheaper Inference, an OpenAI-compatible gateway, to the /settings providers wizard. Selecting it fills in the base URL (https://api.cheaperinference.com/v1) so only an API key and a model name are needed, and the wizard can fetch the account's model list over the standard /models endpoint.
  • Added a showAgentBashOutput preference (/settings → Behavior → Tool Results and Thinking). By default a completed card for a command the agent runs shows the command and its status, and the command output is not kept; with compact tool display on, the card collapses into a tally line. This preference shows the output on that card, whether compact tool display is on or off, and for failed commands too. Commands you type yourself (!command) always show their output and are unaffected.
  • Publish a JSON Schema for agents.config.json as schemas/agents.config.schema.json. It is generated deterministically from the on-disk DiskConfig type (pnpm run generate:schema), ships with an Ajv validation suite plus a CI drift check, and enables editor autocompletion — either by dropping the $schema key into your config or by wiring up the schema via jsonValidation / a JSON Schema mapping in your editor.
  • Added automatic session titles. A session keeps its opening prompt as the title, and when that prompt is too thin to be useful the agent generates a descriptive name once, after the first turn that ran a tool or the first follow-up message. Manual renames are never overwritten.

Titling runs in the ACP agent, so it applies to the VS Code extension and other ACP clients; the CLI keeps its heuristic title. It uses the session's own model by default - set sessions.titleModel / sessions.titleProvider to point it at a cheaper or local one, or sessions.smartTitles: false to turn it off. Pointing titleProvider at a different provider sends it the opening user turns and a summary of the tools that ran, which includes file paths and bash command strings.

Two things worth knowing: the tokens a title costs are billed by the provider but are not counted in /usage, since the call is made outside the conversation loop that builds usage records; and existing sessions are retitled from their first message on their next autosave, which is a one-time visible reshuffle of the history list.

Also fixed the CLI's autosave deriving the session title from the latest user message and rewriting it on every save, which overwrote titles in the store the VS Code extension reads from. Closes [#808].

  • Added Anthropic prompt caching. The system prompt, tool schemas, and conversation history are now marked with cache breakpoints, so multi-turn sessions read the stable prefix back out of cache instead of paying full price for it every turn. Cost reporting is cache-aware throughout: /usage and the per-response indicator price cache reads and writes at their own rates instead of billing every cache hit at the full input rate, and the per-response indicator surfaces the cached token count alongside the total. Opt out with "promptCaching": false on the provider config. Closes [#888].
  • Fix multi-line paste submitting the prompt partway through.

Nanocoder never enabled bracketed paste, so the terminal delivered a paste as bare bytes and the carriage return at each line break reached Ink's keypress parser as Enter. Pasting two lines sent the first line to the model and left the second in the input box. Paste handling relied entirely on heuristics (input rate, size, line count) that only ever saw text which had already made it into the buffer.

DECSET 2004 is now enabled in both screen modes. Paste payloads are lifted off stdin before Ink sees them and delivered to the input as a single event, so a pasted newline can no longer submit. The old heuristics remain as a fallback for terminals without bracketed paste support.

Fullscreen mode enables mouse reporting for wheel scrolling, which takes click-drag text selection away from the terminal. Selection there is Shift+drag (Option+drag in iTerm2), or turn mouse reporting off for the session with --no-mouse.

  • Add JSON exports for /export --json and .json filenames, preserving full message content and session metadata for replay and evaluation workflows. Closes [#1309].
  • Grouped each VS Code chat turn's thoughts, tool calls, edit cards, and task plan into one ordered, collapsible work summary while leaving the final answer visible. The summary reports completed, stopped, or failed duration and reopens for pending approvals. Thanks to @Gambit-Checkmate. Closes [#858].
  • Added support for a .nanocoderignore file. Patterns in it keep tracked-but-noisy files (lockfiles, generated fixtures) out of directory listings, file search and the file explorer, so they stop eating context even though .gitignore doesn't cover them. It is a context-hygiene tool rather than a secrets boundary: read_file and execute_bash don't consult it, and checkpoints deliberately skip it so hidden files are still snapshotted and restored. Thanks to @A-S-Manoj. Closes [#755].
  • Add configurable agent-loop retry limits to prevent token drain (#897). A new nanocoder.retries section in agents.config.json exposes the previously hardcoded caps: maxRepeatedToolCalls (default 3), maxEmptyTurns (default 2), and maxMalformedRetries (default 2). When the repeated-tool-call limit is hit in an interactive session, Nanocoder now pauses and asks whether to continue (granting another window of attempts) or stop, instead of always hard-stopping; non-interactive runs keep the hard stop. The same limits now also protect the --plain runtime used by nanocoder run in CI and non-TTY environments, which previously had no repeated-call cap at all: each cap hard-stops with a clear error there. Note this also loosens --plain in two places: it used to return an error on the first empty response and on the first malformed tool call, and it now nudges or asks the model to self-correct up to maxEmptyTurns / maxMalformedRetries before stopping, so a silent or malformed-output model costs up to 3 model calls instead of 1. Set either limit to 0 to restore the old fail-fast behaviour. Calls to unknown tools count toward the repeated-call streak in both runtimes, so a model stuck on a nonexistent tool trips the same cap instead of looping until the turn ceiling. Delegated subagent runs, whose loop previously had no cap at all, now apply maxRepeatedToolCalls too and stop with an error naming the setting.

One change reaches further than the retry limits themselves: a tool call naming a tool that does not exist is now kept in the assistant message's tool_calls rather than dropped from it. Without this the paired Unknown tool: X result was orphaned and pruned before the request went out, so the self-correction hint never reached the model and it re-emitted the same nonexistent call. This applies to all three runtimes that partition tool calls, including the ACP loop (--acp, used by editor clients), which is otherwise unaffected by the retry limits. The practical consequence is that providers now receive a tool call naming a tool that was not in the request's tool list.

  • Add nanocoder config to see your settings and which file each one came from.

Settings come from four places: built-in defaults, your global config folder, the project folder you're in, and NANOCODER_* environment variables. When a setting didn't do what you expected, finding out which file set it meant opening all four by hand.

Three commands:

  • nanocoder config list — every setting, its value, and the file it came from
  • nanocoder config show <key> — one setting in detail: its default, and any values it beat
  • nanocoder config diff — only what your files change, plus values that are set but unused

All three take --json.

The part worth knowing. Settings are grouped into blocks, like autoCompact. Nanocoder takes the whole block from the first file that mentions it — it does not mix fields from two files. So if your global config sets threshold and notifyUser, and your project config sets only threshold, your notifyUser is thrown away and the built-in default is used instead. Nothing told you that before. config diff now lists it under "Ignored values".

Two smaller things. Values are shown the way the app really uses them — write threshold: 200 and you'll see 95, because it's capped at 95. And API keys show as <redacted>, so you can paste the output into a bug report.

  • Auto-generate descriptive filenames for /export instead of generic timestamps. Closes [#934]

Exports are now contained to the project directory, matching read_file / write_file / string_replace: ~ is not expanded and absolute paths outside the project root are refused rather than written. Rejections name the specific cause (null byte, ~, .. segment, outside the root) instead of failing generically.

  • Added bundled React, Next.js, and Rust project presets for nanocoder init --preset <type> and /init --preset <type>. Presets seed analyzed AGENTS.md guidance, stack-specific context ignores, and a /check command skill while preserving existing files. Closes [#1008].
  • Make the fullscreen TUI the default for interactive sessions.

Interactive sessions now run on the terminal's alternate screen buffer, the way vim, less and htop do. The chat transcript is a bottom-anchored viewport that clips at the top instead of spilling into scrollback, so the prompt can never be pushed off-screen by a long turn, and the frame is repainted cleanly on resize. Scroll the transcript with PgUp / PgDn or the mouse wheel.

Inline mode is still available for anyone who wants finished messages to land in the terminal's own scrollback, where the terminal's scrollbar and search work: pass --no-alt-screen, or set alternateScreen: false in preferences. There is also an "Alternate Screen" toggle under /settings. Run mode (nanocoder run ...), non-TTY and CI environments are unaffected - they print a transcript you need to keep after exit, so they never enter the alternate screen.

  • toggle todo-list visibility using ctrl-t
  • Add Architect mode with an approval workflow for reviewing changes before execution.
  • Auto-compact now runs on --plain, ACP, and subagent loops, not just the TUI. Subagents also honour sessions.maxMessages. Thanks to @Dhirenderchoudhary. Closes [#1048].
  • Add matchPaths to lifecycle hooks, so a hook can be scoped to the file a tool acted on and not just the tool name. This is what lets one formatter per language be declared directly — {"matchTools": ["write_file", "string_replace"], "matchPaths": ["**/*.{ts,tsx}"], "command": "npx prettier --write \"$NANOCODER_FILE\""} — instead of dispatching on the extension inside the command with a case statement, which was the only option before and is not portable to Windows. It applies to any file-scoped hook, not just formatting: "only lint src/**", "audit-log writes under infra/". Patterns use the same glob dialect as skill subscriptions (**, *, ?, {a,b}), and the file is whichever of path, file_path or filePath the tool was called with.

Two deliberate behaviours: a hook scoped by path does not fire for a tool that touched no file (execute_bash), since matchPaths asks a question about a file and excluding is the right answer rather than waving it through — the opposite of an omitted matchTools, which widens to every tool; and an absolute path is also matched relative to the project root, so a root-anchored pattern like src/** fires whether the model wrote src/a.ts or the absolute form for the same edit.

  • Added discoverable usage tips to the welcome screen and a /tip command for showing another tip on demand. /tip <text> narrows the pick to tips mentioning that text, and consecutive runs will not repeat the tip they just showed. Thanks to @OllieinCanada. Closes [#929].
  • Added an lsp_format_document tool that formats a file through the connected language server (honouring .editorconfig indent settings) and writes the result to disk. Thanks to @Dhirenderchoudhary. Closes [#1171].
  • Added MCP resources and prompts support, closing the gap between the MCP client and docs/battlemap.md's claim of parity with Claude Code. MCPClient now discovers a connected server's resources and prompts alongside its tools (best-effort — a server that doesn't declare either capability just contributes none, same as before). Resources are usable the same way local files already are: type @ to fuzzy-search filenames and connected servers' resources together, and select one to inline its content. Prompts are usable the same way custom commands already are: type /mcp:<server>:<prompt> to fetch the prompt fresh from its server and send it as the next turn, with positional arguments filled in against the prompt's declared parameter order. Refs [#1162].
  • /model-database now does something useful with Enter: it copies the highlighted model's ID to the clipboard, and, when the active provider is OpenRouter, switches the session to that model immediately (same behavior as /model, including its own confirmation toast). The footer hint reflects which of the two Enter will do. Closes [#1310].
  • Scroll the chat transcript with the mouse wheel, and stop the wheel cycling prompt history.

The alternate screen has no native scrollback, so the terminal's own wheel and scrollbar cannot work there - the app has to receive wheel events itself. Nanocoder now enables SGR mouse reporting in fullscreen mode and scrolls the transcript three rows per tick. Text selection in that mode is Shift+drag (Option+drag in iTerm2).

Prefer native double-click and drag selection? Pass --no-mouse, set mouseReporting: false in preferences, or use the "Mouse Wheel Reporting" toggle under /settings. The wheel then no longer scrolls the transcript.

This also fixes a bug in that opted-out path. Terminals enable alternate scroll mode (DECSET 1007) by default: while the alt screen is active and the app is not reporting mouse events, they translate wheel ticks into cursor up/down key sequences. Those are indistinguishable from real arrow keys, so scrolling the wheel cycled through prompt history instead of the transcript. Nanocoder now turns that mode off for the lifetime of the session and restores it on exit.

  • Added a terminal bell option to notifications. Every notifier Nanocoder had was desktop-only — terminal-notifier, osascript, notify-send, the PowerShell balloon — so anyone driving Nanocoder over SSH, inside tmux, or from a remote container got nothing at all when a long run finished. notifications.bell now also writes a BEL character to stdout for whichever events you have enabled, which the terminal in front of you renders as a beep or a visual flash. Toggle it under /settings → Input → Notifications, next to Sound. BEL is non-printing, so it does not disturb the rendered frame, and it is skipped when stdout is not a TTY so piped output and daemon logs stay clean. Also corrected the notification docs, which described the preference as living under a nanocoder.notifications namespace — it is read from the top-level notifications key, so anything written at the documented path was silently ignored. Closes [#931].
  • Optional OS sandbox for execute_bash / ! (nanocoder.sandbox, default off). macOS uses sandbox-exec, Linux uses bubblewrap or errors instead of falling through, Windows is unsupported. Thanks to @Dhirenderchoudhary. Closes [#1049].
  • Added an action timeline in the VS Code sidebar so you can click a prior mutating tool step and revert the workspace files and conversation back to that point.
  • Added a professionalTone preference (/settings → Behavior → Professional Tone). When on, the end-of-turn note drops its random adjective ("Completed in 12s." instead of "Worked for a plucky 12s.") and the system prompt gains a TONE section instructing the model to stay terse and strictly functional — no filler, no preamble, no celebratory wrap-ups.
  • nanocoder run --prompt-file <path> reads the prompt from a file instead of the command line. Linux caps a single argv entry at MAX_ARG_STRLEN — 32 pages, 131072 bytes — independently of the much larger ARG_MAX total, and execve fails with E2BIG before the process starts. macOS has no equivalent per-argument cap.

That combination is a trap for any caller that assembles a prompt rather than typing one: it works in local testing on a Mac and then cannot spawn at all on a Linux CI runner. Sentinel embeds the files it audits into the prompt and hit exactly this — every pack returned spawnSync nanocoder E2BIG on ubuntu-latest at 314 KiB and 218 KiB, having passed on macOS throughout.

A file has no such ceiling. The flag takes precedence over a positional prompt, and a missing or unreadable path exits with a message rather than running against an empty prompt.

  • The CLI parsing test mirror is back in step with the parser. cli.spec.ts duplicates the argv filter by hand, and had fallen six flags behind it: --mode, --json, --output-format, --trust-directory and the alt-screen pair were all stripped by the real parser and left in the prompt by the mirror, so those tests were passing against a parser that is not shipped. A test now asserts the two agree.
  • Add automatic collapsing for long user messages in the conversation view. Messages exceeding 40 words or 300 characters are collapsed for display, while the original prompt remains intact and can be recalled from input history.
  • Added lifecycle hooks: shell commands configured under nanocoder.hooks in agents.config.json that run at fixed points in the agent loop — session-start, session-end, user-prompt-submit, pre-tool-use, post-tool-use, and pre-compact. Hooks cost no tokens and fire every time, so rules the model used to be merely asked to remember ("format after every edit", "never touch .env") are now enforced. A pre-tool-use or user-prompt-submit hook that exits non-zero denies the action and its stdout goes back to the model as the reason; the tool gate runs ahead of the approval decision, so a vetoed tool never renders a confirmation prompt. A hook that hangs past its timeout is killed along with its whole process tree and skipped, rather than wedging the session. post-tool-use output is folded into the tool result — including when the tool failed, so an audit hook sees the errors too — and session-start output is injected as context on the next chat prompt, leaving slash commands and ! bash passthroughs untouched. Hooks run from the project root so a relative command survives a cd, and session-end defaults to a shorter timeout that fits inside the shutdown budget. The tool hooks apply on every surface that runs tools — the interactive TUI (including streamed execute_bash), run, --plain, ACP, and subagents — the session and prompt hooks cover the TUI and run/--plain, and /doctor lists what a project has wired up. Closes [#1032].
  • Added /repomap, a local codebase map. Nanocoder indexes the symbols defined in each source file, links files that reference each other's symbols into a directed graph, and ranks that graph with PageRank - so the map leads with the files the rest of the codebase leans on most. Everything runs on your machine with no LLM round-trip, covering TypeScript/JavaScript, Python, Go, Rust, Java/Kotlin/C#/Swift, Ruby, PHP, and C/C++. The map is budgeted to 1024 tokens by default; /repomap --tokens <n> widens it. Thanks to @akramcodez. Refs [#890].
  • Retired the legacy .nanocoder/tasks.json file in the working directory. Task state is now session-scoped and stored with the session's other artifacts under the app data directory, so nanocoder no longer writes agent bookkeeping into your repository and two concurrent sessions no longer share one task list. Resuming a session restores its tasks. Any leftover .nanocoder/tasks.json is ignored and can be deleted.
  • Add /review slash command and nanocoder review CLI subcommand for AI-powered code review of branch diffs and PRs
  • File search (path matching and content search) is now backed by ripgrep instead of a hand-rolled JS walker.

Search also respects .nanocoderignore and binary files again, matching list_directory and file autocomplete.

A failed search now reports the failure instead of returning an empty result set.

.nanocoderignore directories are skipped during the walk rather than filtered afterwards, so a large ignored directory can no longer crowd real files out of the results.

  • Give every interactive surface the same rounded, titled chrome.

The prompt box now has rounded borders, and the surfaces that sit above it were inconsistent: some drew a bare title over unboxed content, others had no frame at all. The session selector (/resume, /history), the IDE selector (/ide), the file explorer (/explorer), the plan review bar, the question prompt and the tool confirmation prompt are now all enclosed in the same rounded titled box, at the same width, honouring the title shape you picked in /settings.

The transcript and the development mode indicator also line up with the prompt's left border instead of sitting a column off it, so the whole frame shares one vertical edge.

  • Extended prompt scrubbing to tool results. Privacy scrubbing ran on the system prompt and on user/assistant message content, but tool-role messages were passed through verbatim — the largest and least-reviewed body of text leaving the machine. A cat .env, a git diff carrying a key, or a stack trace with a token went to the provider in the clear even with scrubbing on. Three channels are now covered: tool result content, the structuredContent produced by tools like lsp_get_diagnostics and write_tasks (MCP results travel as plain strings on content, already covered), and the arguments of assistant tool_calls, which are rehydrated to their real values before entering history and were therefore replayed unscrubbed on every later turn. Structured payloads and tool-call arguments are scrubbed leaf by leaf rather than as serialised JSON, so a replacement can never span a quote boundary and corrupt the document. Tool output is truncated to MAX_TOOL_RESULT_CHARS before the detectors run, and the truncation boundary is placeholder-aware, so a cut — including the converter's own re-truncation of text that grew past the cap once placeholders replaced what they redact — can never land mid-placeholder. Paths and URLs stay in the clear, as on the existing scrub paths — the agent has to be able to act on the files and endpoints a result names. Closes [#1161].
  • Added a dedicated Semantic Memory toggle to /settings under Advanced, backed by the semanticMemoryEnabled preference. The setting defaults on to preserve existing behavior, and can be turned off to keep agents from persisting reusable context across sessions.
  • The MCP setup wizard (/settings mcp) now offers a Serply template alongside Brave Search, DuckDuckGo and You.com. It builds a remote HTTP MCP config pointing at https://api.serply.io/mcp, giving the agent Google, Bing, News, Scholar, Maps, Jobs and Amazon search plus URL scraping. The API key is required and is sent as an X-API-Key header.

Editing a wizard-built server whose key is stored in an X-API-Key header now re-opens the form with that key prefilled. Previously the edit flow only recovered keys from env vars or a bearer Authorization header, so the required key field came back empty.

  • Added a session-scoped artifact lifecycle to the CLI and VS Code: implementation plans with explicit review and prose-plan fallback persistence, persistent task tracking, completion walkthroughs after an approved plan, clickable artifact shortcuts that survive session resume, and reliable cancellation recovery. Task lists now live with the session instead of .nanocoder/tasks.json in your project, so /clear starts a fresh list while the previous session keeps its record. Plan approval always works, falling back to the plan in the transcript when no artifact was written. Headless --plain runs keep their artifacts ephemeral and are not forced to produce a walkthrough. Subagents can no longer reach the plan, task, or walkthrough tools, and their declared tools: allow-list is now enforced when a tool runs rather than only when the tool set is offered. Thanks to @2409324124. Closes [#805].
  • Added nanocoder completion <bash|zsh|fish>, which prints a self-contained tab-completion script for the requested shell so subcommands, flags, and known flag values (like --mode normal|auto-accept|yolo|plan) complete at the shell prompt. The shell argument is required, so a missing or unknown shell fails with usage instead of silently installing the wrong script. Scripts are rendered from a single spec in source/cli-completions/spec.ts shared by all three shells, and the command runs on the startup fast path so it costs nothing. Install via eval "$(nanocoder completion zsh)" or by piping to your shell's completion directory — see docs/features/shell-completions.md. Closes [#1003].
  • Added nanocoder skills add, an install path for skill bundles. Point it at an index name, owner/repo, any git URL, or a local checkout and it shallow-clones the bundle into a temp dir, strips .git, refuses any bundle containing a symlink, and validates it with the same linter /skills check runs — all before anything is written into the project. Because installing a skill means running its code, the trust prompt names every tool with its declared approval policy (never really does mean "runs without asking") and every event subscription the daemon would fire unattended. Accepting lands the bundle through the same copy /skills promote uses, so an existing skill is never overwritten without --force. Bare names resolve through a plain skills.json index hosted in a git repo — no registry service — overridable with NANOCODER_SKILLS_INDEX or --index. Refs [#1163].
  • Add /stats lifetime usage command with 7d / 3m / all-time ranges (←/→ to switch), cumulative token chart, peak day, streak, and top provider·model pairs. Thanks to @puri-adityakumar. Closes [#933].
  • Added /tasks list, /tasks done, /tasks complete, and /tasks start subcommands for viewing and updating task status, with validation for unknown subcommands and task numbers.
  • Syntax highlighting now follows your theme, and takes a syntaxTheme preference when you want code to keep a palette of its own. All five highlighting call sites - markdown code blocks, string_replace diff context, the write_file preview, and the file explorer preview - passed theme: 'default', a string where cli-highlight expects a token-to-formatter map, so the option was silently dropped and every theme rendered code identically in the library's own colours. Each one now derives its token map from a theme's palette: keywords take primary, built-ins and declarations tool, strings success, numbers warning, comments secondary, attributes and variables info, and everything else the theme's body text. Code follows selectedTheme by default; setting syntaxTheme in nanocoder-preferences.json (e.g. "syntaxTheme": "dracula") points code at any other theme's palette while the rest of the UI stays put, and an unknown name falls back to selectedTheme rather than dropping the styling. Closes [#935].
  • Render optional description/intent field in tool approval preview cards. Tools that modify files or execute shell commands (execute_bash, string_replace, write_file, diff_edit, and file_op) now accept an optional description parameter in their schemas, which renders above the command or path in the approval card to make the model's intent clear before execution.
  • Added created and modified files to the VS Code extension's context panel: every file the agent writes during a turn now appears as a chip above the composer as soon as the edit lands, and clicking it opens the current version in the editor for review. The chips are dashed to set them apart from files you attached yourself, survive sending a message, and can be dismissed individually or, for a turn that touched many files, all at once with a "Clear N changed files" control; the row scrolls at a fixed height rather than growing the composer. They are deliberately not inlined into the next prompt, since the agent just wrote them. The row follows the rest of the file lifecycle too - a deleted file loses its chip, and a rename moves the chip to the new path. Two tools also report themselves more accurately over ACP as a result: diff_edit is now an edit, so edits from the nano tool profile get the same file card and chip that write_file and string_replace already did, and file_op reports the kind its operation actually performs (delete, move, or an edit for a copy) instead of a generic tool call. Closes [#857].
  • Added a "Retry" button to AI responses in the VS Code chat panel. Reorganised the assistant turn footer so the action buttons (Retry, Copy) sit together with the timestamp, and resubmitting a retried prompt no longer creates a duplicate user message bubble.

Behind the scenes, the webview now calls a new retryTurn extMethod on the ACP agent. The method truncates the session message history at the matching user turn and drops any action-timeline checkpoints captured inside that turn, so reverting a checkpoint can no longer rewind past the retried turn. The pendingUserMessageText lifecycle was tightened so user bubbles reconstruct correctly when resuming or switching between history sessions.

  • Added a nanocoder.showTokenUsage VS Code setting that defaults off and hides token usage plus estimated cost footers unless users opt in. Closes [#1096].
  • Redesigned the welcome screen: a gradient block wordmark (full NANOCODER from 90 columns, a compact NC monogram below that), version line, centered quick-action menu, and a branch/directory location line — all centered on one axis at every terminal size, with vertical centering on tall terminals. The prompt input now spans the full terminal width and resizes live with the window, and the boot summary no longer duplicates information the welcome screen already shows. The subtitle under "Welcome to Nanocoder" is the project's own description rather than the old "local-first coding agent" line, wrapped and centered to fit the terminal. Closes [#949].
  • The MCP setup wizard (/settings mcp) now offers a You.com template alongside Brave Search and DuckDuckGo. It builds a remote HTTP MCP config pointing at https://api.you.com/mcp, giving the agent web search, URL reading, and research tools. The API key prompt is optional: pasting a YDC_API_KEY builds an authenticated config with a bearer header, while leaving it empty falls back to the keyless free profile (https://api.you.com/mcp?profile=free) so search works with zero signup.

Wizard-built configs now record which template created them (templateId), so editing a custom-named instance (e.g. you-paid) re-opens the original template's form instead of the generic custom one — previously the saved bearer token was silently dropped on re-save.

  • Fixed ask_user rejecting 5-6 options over ACP. The tool schema allows 2-6 options, but the ACP path used by the VS Code extension capped them at 4, so an identical prompt succeeded in the CLI and failed in the editor. The bound now matches the schema, and the error string returned to the model says "2-6" rather than telling it the limit is 4 and pushing it to retry with a needlessly narrowed list. Closes [#1036].
  • Fixed sub-agent tool calls being denied silently under ACP. A tool call made inside a dispatched sub-agent went through the global approval slot, which no ACP code path installed a handler for, so its safe fallback denied every one without the client seeing a session/request_permission. Delegated work could only write by bypassing approval entirely, which was invisible to a client that gates writes. Sub-agent calls now use the same permission channel as top-level ones, announced first so the request names a known tool call, prefixed so their cards cannot collide with a top-level id, and titled with the sub-agent so the client can tell them apart. The handler is scoped to the turn that installs it, so it no longer outlives that turn holding a finished session. Two known limits remain. The approval slot is still a process-wide singleton, so while two sessions have turns in flight at once the later one's handler answers the earlier one's approvals, against the wrong session id and abort controller; closing that needs the slot keyed by session or an approval channel threaded through the sub-agent executor. And an approved sub-agent call is marked completed as soon as it is approved rather than when it runs, because the sub-agent layer does not report results back, so a client sees completed for a tool that may still fail. Note also that sub-agent approvals ignore the ACP session's mode and the configured alwaysAllow list, so a yolo or auto-accept session is prompted inside a sub-agent for a tool it would not be prompted for at top level. Closes [#1019].
  • Allow projects located at a filesystem root, including / and Windows drive roots, to pass path-containment checks. File tools and search_file_contents no longer reject every path with escapes project directory when the workspace is mounted at the root, as it is in many containers. Closes [#1240].
  • Made the three "ask the user" queues abort-aware, so a cancelled turn no longer strands its callers. The queue only advances when a human answers, so anything parked on an approval, confirmation or question when its turn died waited there forever, and the prompt for that dead turn stayed on screen for the user to answer on its behalf. tool-executor starts a batch of subagents and joins them with Promise.allSettled, so a single stranded subagent kept the whole turn open - [#1156] fixed the overwriting resolver but noted this await was still not abort-aware, and it was the remaining half.

signal() now takes the turn's AbortSignal, passed by the subagent executor, the conversation loop's tool confirmation, and the repeated-tool-call question. An aborted request leaves the queue and settles with the same safe default each slot already used when no handler is installed at all: both approval slots deny, so cancelling a turn can never be a route to approving a tool nobody was shown. Removing the head advances the display to the next request; removing one from the middle leaves the screen alone and closes the gap. A request that was already answered is untouched by a later abort, and a signal that is already aborted never puts a prompt on screen in the first place.

  • Fix six defects in architect mode's review gate. Revert now deletes files the turn created instead of leaving them on disk; Escape keeps rather than silently reverting, and the footer says so; Revert & Revise sends the instructions you typed instead of a fixed string; the composer is hidden while the gate is open, so keystrokes no longer reach two components and the gate cannot be bypassed; the checkpoint now covers every file-mutating tool architect auto-executes, including lsp_format_document and custom file tools; and each turn's checkpoint is released once the gate resolves. Reverting also tells the model its changes are gone, so the next turn does not edit against a stale view of disk. Adds architect to --help and shell completions, and documents the mode.
  • Fixed Backspace deleting the character after the cursor on virtually every terminal. Ink parses both the physical Backspace (\x7f, sent by macOS Terminal, iTerm2 and essentially all Linux terminals) and the forward Delete key (\x1b[3~) as key.delete, so Backspace was routed to a forward delete (a no-op at end of line). TextInput now disambiguates at the raw-sequence level so Backspace always deletes backward.

Fixed undo/redo reading stale stacks when an edit and an undo/redo arrive in the same stdin batch. pushToUndoStack now keeps the undo/redo stack refs in lockstep synchronously (not just after a render), and redo() now caps the undo stack it pushes back onto, matching undo().

  • Fix fetch_url truncation warning to display the content limit in characters.
  • Show the workspace, not a config path, in the startup summary.

The startup line used to print the directory of the closest agents.config.json, which is rarely where you are working and reads as noise. It now shows the directory Nanocoder is operating in, followed by the active branch when the workspace is a git repository. Narrow terminals keep the provider, model and mode on the first line and drop the workspace and branch underneath.

  • The streaming reasoning trace no longer re-wraps its entire history on every token flush. It now wraps only a bounded tail, the same way the streaming message does, and skips that work and the token count altogether while the panel is collapsed — which is the default, so the cost was being paid for a panel nobody could see. Measured over 30 flushes of a ~150KB trace: 91.9ms to 11.6ms per flush collapsed, 99.2ms to 25.2ms expanded. Closes [#1329].
  • Preserve every copy of repeated pasted text through placeholder display, chunked updates, and prompt assembly.
  • Fix fullscreen chat rendering getting progressively slower in long conversations by scaling the mounted transcript tail to terminal height instead of a flat 60-component cap.
  • Resume queued prompts after slash commands and manual context compaction complete. Refs [#1060].
  • /checkpoint load now shows a windowed, filterable list instead of rendering every checkpoint at once. A project with a few dozen checkpoints pushed the picker well past the visible terminal. It now uses the same list component as session history, so typing filters, the window scrolls with the highlight, and it shrinks further on short terminals. Closes [#1372].
  • Fix --model=value, --provider=value, --vscode-port=value, and --context-max=value being ignored and included in prompts after run. Closes [#1238].
  • The live compact tool-activity summary above the input now shows at most 5 tool rows, followed by a +N more line. A turn that uses many distinct tools (for example with several MCP servers enabled) could previously add one footer row per tool and push the input box out of view on a short terminal. Closes [#1291].
  • Grouped the VS Code chat composer controls: model stays on the input row, the current approval mode stays visible on the gear, and provider plus mode pickers live in a Configuration popover. Closes [#859].
  • Tool approval prompts now answer exactly once. The approval queue resolves its head on every answer, so a second answer from an already-settled prompt would have consumed the next queued request and resolved it unseen — approving a tool the user was never shown. The prompt now ignores any answer after the first and resets when a new request arrives.
  • Add terminal support for /copy code.

Running /copy code in the terminal previously returned "This functionality isn't supported in the terminal." It now parses fenced code blocks from the last assistant message and copies the most recent block to the system clipboard, matching the VS Code webview behaviour. Returns "No code blocks found in the last response." when the last response contains no fenced blocks.

  • Fixed subagent tool results that return structured data without llmContent so the complete output is preserved for the model instead of being passed as undefined. Closes [#1033].
  • Fixed custom command arguments containing $ being rewritten before they reached the model. substituteTemplateVariables passed the value straight to String.prototype.replace, which treats a string replacement as a substitution template rather than a literal: $& expanded to the {{arg}} placeholder it was replacing, $` and $' spliced a whole half of the command template into the middle of the prompt, and $$ collapsed to a single $. Those are ordinary characters in a shell snippet, a regex, a Makefile or a price, so /mycommand "use $' to quote" silently sent something other than what was typed, with no way to see it. Values are now inserted through a replacer function, the same way custom-tools/template.ts already did. This is the same defect #1057 fixed for string_replace and diff_edit; the custom-command path was not covered by it.

Also fixed a parameter name containing a regex metacharacter crashing the command. The key was interpolated into the pattern unescaped, so a parameter called a(b built /\{\{\s*a(b\s*\}\}/ and threw Invalid regular expression: Unterminated group. Keys are escaped now, which also means a key like a.b matches literally instead of matching axb.

  • Custom tool cwd now stays inside the project after symlink and ${VAR} resolution. A cwd that really resolves outside the project - an absolute path, ${HOME}, a ../ traversal, or an in-repo symlink pointing out - now fails the tool call with Custom tool cwd escapes the project directory rather than running the script somewhere unexpected. A missing cwd still falls back to the project root. Closes [#1027].
  • Fixed custom tools hanging past their own timeout. Three things went wrong on the timeout path. The SIGKILL escalation was guarded on !child.killed — a flag Node sets the moment a signal is delivered, so the SIGTERM on the line above had already made it true and the force-kill never ran. Nothing held a reference to the escalation timer either, so on a clean exit it was never cleared and fired later against a dead process. And the rejection lived in the close handler, which waits for the stdio pipes to drain as well as for the process to exit; since SIGKILL only reaches the shell, any grandchild that inherited stdout — a background job, a dev server, a wrapper script — held those pipes open and kept the call pending long after the shell was gone. Both timers are now tracked and cleared together, the dead guard is gone, and the timeout settles on exit with the stdio streams destroyed. Thanks to @yashksaini-coder. Closes [#1141].
  • Custom tools on Windows now spawn cmd.exe /d /s /c instead of -c, which cmd does not accept. /d skips AutoRun; /s makes quote stripping deterministic. {{ }} substitution stays POSIX-quoted and is not shell-safe under cmd. Closes [#1028].
  • Fixed nanocoder daemon logs returning only a sliver of the tail when a single oversized log line runs past the start of the 64KB window. Realigning the window to the first line break discarded everything before it, so a log holding one long serialized payload came back as just the few bytes that followed. The tail now only realigns when a line break is near the start of the window, and otherwise keeps the partial first line.
  • Fixed nanocoder daemon logs reading the whole log file into memory to return its last 64KB, so a long-running daemon with a large log made the command allocate the entire file and stall. The tail is now streamed from a byte offset, which also corrects a byte offset applied to a decoded string: any multi-byte content in the log shifted the window and returned far less than the intended 64KB. Closes [#1042].
  • nanocoder daemon start now refuses to boot in a directory that hasn't been trusted. The daemon fast path (cli.tsx) went straight to runDaemonCli → startDaemon → bootSkillPipeline without ever touching App.tsx / useDirectoryTrust, so it loaded and registered every .nanocoder/agents|commands|tools/*.md and skills/<name>/skill.yaml in the project unconditionally. Once booted, headless mode (used for every daemon-triggered run) executes execute_bash, write_file, string_replace, diff_edit, and MCP tools with no confirmation prompt. start now checks preferences.trustedDirectories (reusing the same ensureDirectoryTrust helper --plain already used, now shared via @/config/preferences) before spawning the daemon process, and refuses with a clear message otherwise. A new --trust-directory flag bypasses the check for a single daemon start run without persisting it; NANOCODER_TRUST_DIRECTORY=1 still persists trust as it does for --plain. Closes [#1245].
  • Covered the previously untested daemon status and daemon stop subcommands with regression tests.

daemon/cli.spec.ts had 12 tests for start and logs but none for status or stop, so a regression in either handler (wrong exit code, lockfile race, missing error handling) would not have been caught by the suite. Five new tests cover the empty-state branches (Not running. / No daemon is running.), the stale-lockfile cleanup path (which removes the lockfile and reports the previous pid), the live-status format string (pid, socket, uptime), and the live-stop shutdown path. The live-stop test forks a real child so SIGTERM lands on something other than the test runner, which would otherwise trip the shutdown manager and abort the run.

No runtime behavior change — tests only.

  • The VS Code discovery file's version field is now actually checked. It was parsed and echoed back but never compared, so the "bump on breaking changes" its constant promises would have done nothing: a v2 file written by a newer CLI was consumed by an older extension as if it were v1, surfacing as a failed handshake or a misread field rather than a clean "not ready". A file declaring a newer schema is now treated as missing, which is the direction that matters given the extension and the CLI update independently. Older files, including ones written before the field existed, still load.

PID reuse in the same file's stale detection is documented as an accepted residual rather than papered over with an age cutoff. A recycled PID reads as live, but the recycled process is not listening on the recorded port and does not hold the bearer token, so the cost is one failed connection that the next start() repairs. An age bound would trade that self-healing transient for a worse failure mode: disconnecting a genuinely long-lived session.

  • Document the tool output convention for file content and add a regression test.
  • Overhaul VS Code Add Provider UI:

  • Implement fully dynamic provider and SDK preset dropdowns.

  • Synchronize 23 provider templates exactly with CLI defaults.
  • Add support for defining multiple custom model names dynamically.
  • Fix missing theme CSS tokens in settings panel webview.
  • Fixed the test suite failing on main. [#1417] changed execute_bash's execute function to return {llmContent, isError} instead of a bare string and updated its own spec, but execute-function.spec.ts calls the same function and asserted on the result directly. It passed tsc because it cast the result as string - the cast asserted away the very change that broke it, so the only thing left to catch it was a runtime t.regex() failure. The three execute_bash cases now read llmContent through a helper, and the as string casts are gone from the read_file cases too, so the next return-shape change fails at compile time rather than at assert time.
  • Fix markdown export formatting for tool outputs containing inner code fences
  • fetch_url no longer transfers the target page twice on every call. The redirect walk that validates each hop issued a GET, so when it reached a non-redirect it had already downloaded the whole final response - and then discarded it, because convertToMarkdown fetches the resolved URL itself. Every call therefore moved the target twice: doubled bandwidth, doubled rate-limit consumption, and two hits on any endpoint that meters or logs requests. The walk only ever needed a status and a Location, so it now asks for headers. A server that refuses HEAD (400, 403, 405, 501) or fails it outright still falls back to the GET-and-discard it used before, so nothing regresses on the servers that need it. Hop-by-hop validation is unchanged - a redirect into a private or loopback address is still rejected at the hop rather than followed.
  • fetch_url now blocks the whole .localhost zone, not just the bare localhost label. RFC 6761 reserves it for loopback and systemd-resolved resolves every label under it to 127.0.0.1, so http://foo.localhost reached a local service on Linux. Real hosts that merely contain the string (localhost.example.com) are unaffected.
  • fetch_url now rejects the request URL inside execute, not only the validator. readOnly tools skip confirmation, so yolo/headless/subagents never ran the old hostname list. Blocks loopback CIDR, RFC1918, and GCP metadata names (metadata, metadata.goog, metadata.google.internal). HTTP redirects and DNS rebinding are not covered (#1089).
  • Fixed the @ file mention list losing its highlight after the fifth suggestion, which let Tab insert a file that was never shown. The list now scrolls with the selection and shows "Showing X-Y of Z" when there are more than five matches, like the slash command list. Closes [#1405].
  • Fixed find_files returning no results for patterns written with a leading ./. Project entries are relative paths with no ./ prefix, so ./src/**/*.ts or ./package.json was compared against src/index.ts and never matched, leaving the model to conclude the files did not exist. The prefix is now stripped before matching, and the basename fallback is computed from the stripped pattern so ./*.tsx behaves exactly like *.tsx. Closes [#1343].
  • Use atomicWriteFileSync for writeUsageData to prevent partial or corrupted file visibility.
  • The Architect review gate now lists only files the turn actually changed or created, rather than every file a tool set out to touch, so a rejected or no-op edit no longer shows up as changed. A turn whose edits all failed skips the gate. Creating a new file no longer flashes a "Could not capture file" warning in the chat.
  • The Architect review gate no longer shows "Esc to keep" while you type Revert & Revise instructions. Escape goes back to the options there, which the input's own hint already says.
  • Consolidated the atomic-deletion overlap checks onto a single half-open [start, end) range helper, so the boundary tests that had drifted between < and <= now agree by construction rather than by coincidence. Deletion behaviour is unchanged - the longhand forms were already equivalent for every reachable input. Placeholder lookup at a cursor position does change: a position now counts as on a placeholder when it falls in (start, end], so the position immediately before a placeholder reads as outside it and the position immediately after it reads as inside, matching where the cursor actually sits when you press Backspace. Thanks to @hiarun02. Closes [#977].
  • Fixed control keys that arrive in one read (a held Backspace, a double Ctrl+Z, keys typed while the UI is busy) doing nothing and being inserted into the prompt as raw bytes that were then sent to the model. Each control key is now handed to the input handlers separately.
  • The run mode boot line no longer marks the default branch as (default), as intended when the marker was restored to /status. The boot line had since moved to a different label helper that kept it.
  • Refuse to snapshot a file from outside the workspace. Snapshot keys are the file's path relative to the workspace, so a file above it was keyed ../name and would have been written outside the checkpoint's own files directory. captureFiles now drops those paths and reports them through skipped, so an incomplete checkpoint still says so at restore time. The same containment rule already guarded restore and delete and is now shared by all three.
  • Fixed checkpoints irrecoverably corrupting binary files. Every layer of the pipeline read and wrote contents as UTF-8, so each byte of an image, .vsix bundle or database came back as U+FFFD — and because the damage was done at save time, the checkpoint itself held nothing left to recover. Snapshots are now carried as Buffer and written with no encoding argument; text still round-trips byte-identically.

A checkpoint that came out incomplete also restored in silence. Files that could not be read at capture, and files dropped by the MAX_CHECKPOINT_FILES cap, are now recorded on the checkpoint's metadata and named when it is restored. Old checkpoints still load, though binaries captured before this fix stay corrupt. Closes [#962].

  • Checkpoints no longer snapshot nanocoder's own state. In a repo that doesn't gitignore .nanocoder/, every checkpoint captured the earlier checkpoints and the action timeline, so each one grew (0, 2, 6, 14, 30 files) until the 50-file cap crowded out your real changes, and restoring one wrote stale checkpoint data back over the live store. User content under .nanocoder/ (commands, agents, tools, skills) is still captured.
  • Fixed the cli-value-flags integration tests failing on main. The suite's module loader short-circuits @/config/preferences with a stub, and cli.tsx started importing getAlternateScreen and getMouseReporting from it while that stub still exported only loadPreferences. Both changes passed on their own branches and only collided once merged, so the missing exports surfaced as getMouseReporting is not a function across all 30 cases.
  • Fix Expand Tool Results label inverted and compactToolDisplay preference never read at startup
  • Fixed parallel subagents hanging the turn on the first approval prompt. The three "ask the user" slots each held a single resolver, so a second caller arriving before the first was answered overwrote it and that first promise could never settle. tool-executor starts up to five subagents in one turn and awaits them with Promise.allSettled, so one stranded caller meant the batch never resolved, the turn never ended, and Escape could not free it - the subagent was parked in an await that is not abort-aware, so recovery meant killing the process. Each slot now queues requests in arrival order and presents them one at a time, so every caller settles with its own answer. The same design backs ask_user and the main agent's tool confirmation, and both are fixed by the same change. Closes [#1156].
  • Fixed --context-max and /context-max silently accepting malformed values. 10kg used to parse as 10 and 128kb as 128, so a typo quietly set a context limit nothing like the one you asked for. The value is now validated as a whole, and anything that is not a positive number with an optional k/K suffix is rejected with the existing error message. Values large enough to overflow to Infinity are rejected too, rather than being stored as a session limit. Thanks to @hiarun02. Closes [#973].
  • {{args}} in a custom command now receives the arguments exactly as typed. It was rebuilt from shell-style parsed tokens, so quotes were stripped and an apostrophe opened a quote: /cmd don't break it reached the model as dont break it. Declared positional parameters still use the parsed tokens.
  • Fixed an issue in CustomCommandLoader where encountering an unreadable subdirectory or inaccessible file in custom command directories aborted the entire scan. scanDirectory and loadResources now catch filesystem permission and inspection errors, log a warning, and continue scanning remaining commands.
  • Runs triggered by the skill daemon can now use their own skill's tools, custom tools and MCP tools. The daemon registered skill tools into one tool registry but ran triggered agents against a second, empty one, and it never connected MCP servers, so a subscribed agent could only use the built-in tools. MCP servers are now also disconnected when the daemon stops, so nanocoder daemon stop exits cleanly.
  • Closed the remaining routes by which nanocoder's own UI text reached the model. The ACP timeline-revert notice ("Reverted to before step N…") was still pushed into history as a plain assistant message, and /compact (plus auto-compact) fed display-only notices into the LLM summariser, whose summary re-enters context as a real user message — so a compacted session could still be told it had errored. Notices are now excluded from the summarised segment, and context-usage estimates and the auto-compact threshold count only what the provider actually receives. Also documents the displayOnly contract on Message, warns when a display-only message carries tool_calls (which would silently drop its tool results from the payload), and shares the "Tool approval required for: " prefix as a constant so the non-interactive exit-code path can't break on a reword.
  • Stopped nanocoder's own UI text from being sent to the model as its past output. Cancellation notices (_Cancelled by user._), inline error banners (**Error:** ...), the non-interactive "Tool approval required" notice, and the VS Code replies to built-in slash commands (/help, /copy, /model, unrecognized commands) were all pushed into conversation history as assistant messages, so on the next turn the provider received harness-authored markdown as if the model had written it — teaching it to imitate the chrome and, on a resumed session, to believe it had errored. These are now marked display-only: they still render in the chat and replay with session history, but they are filtered out before messages are converted to the provider payload. Closes [#893].
  • Fixed string_replace, diff_edit and write_file destroying PDF and DOCX files. Reading one of those formats returns a markdown transcript rather than the bytes on disk, and the write side had no matching branch: the edit was applied to the transcript and written back over the document as UTF-8, so a request to fix one word replaced a real document with a few hundred bytes of plain text and the tool reported success. Neither undo system could recover it - checkpoints skip binaries and file snapshots store text - so the original bytes were gone the moment the write landed. The three write tools now refuse any path whose content the read path can only transcribe, naming the reason so the model stops instead of retrying. Closes [#1058].
  • Fix user prompts appearing twice in the terminal scrollback when the model starts streaming a response in default inline mode. Pressing Escape to recall an in-flight prompt in inline mode now leaves the bubble visible in scrollback rather than removing it from the live view, since it has already been committed to Ink's transcript and cannot be un-printed.
  • The empty /resume picker now reads "Press Esc to close" instead of the contradictory "Press Escape to continue • Esc to cancel".
  • Fixed message compression so resolution text only marks an error resolved when it appears after that error.
  • Fix /explorer crashing the CLI.

Running /explorer killed the process with exit code 1 and useUIStateContext must be used within a UIStateProvider. The provider wrapped only the chat input, while the file explorer renders as a sibling, so the hook threw during render and took Ink down with it.

The same placement broke the feature even without the crash: explorer mode unmounts the chat input, so the provider holding the explorer's selection went with it and the files could never have reached the prompt. The provider now wraps the whole interactive tree, and picking files in the explorer inserts them as @ mentions when you exit.

  • Fixed fast typing occasionally scrambling the prompt, for example /tune coming out as /etun, most often on the first command typed after startup. The input compared each echoed value only with its latest keystroke, so a render that ran late looked like an outside change and put the cursor back at the end of the older, shorter text.
  • Fixed the status row under the prompt being cut mid-word in fullscreen (for example norma instead of normal) when the task badge and a session name share a narrow terminal. The row's width budget now accounts for the fullscreen frame's padding.
  • Daemon-triggered (headless) subagent runs are no longer offered the ask_user tool. Nobody can answer in a headless run, so calling it only returned a "Question handler not initialized" error after the model had spent a turn on it.
  • This release introduces a suite of UI improvements to the VS Code chat panel and robust session state recovery:
  • Timeline Removed: Deprecated and completely removed the action timeline feature and its internal event tracking.
  • Artifacts Redesign: The artifacts list has been redesigned into a clean, collapsible container that can be toggled by the user.
  • Session History Durations: Fixed a bug where history playback would render a duration of 0s for cancelled or failed sessions. The chat data schema now officially tracks an outcome ('completed' | 'cancelled' | 'failed') and a durationMs on every assistant message. Limitation: If a turn is cancelled before the agent emits any thought or tool call, no work summary container exists in the webview to display the duration. Those turns show no duration indicator at all (rather than the previous incorrect 0s).
  • Stream-Time Footer Hiding: The message footer (Retry, Copy, timestamp) dynamically hides while the agent is streaming its response, rendering a much cleaner interface that only shows actions once the agent completes.
  • Smooth Auto-Scroll: Restored intelligent auto-scrolling where reading past messages prevents forced scroll-to-bottoms during a stream. A forced scroll is now selectively applied only when a run finishes or a session is loaded.
  • Wider User Bubbles: The user prompt bubble max-width was expanded from 85% to 90%.
  • list_directory and the file explorer now hide a directory matched by a directory-only ignore pattern such as dist/ or node_modules/ in .gitignore or .nanocoderignore. Its contents were already hidden, but the folder itself still listed, and listing it then reported it as empty.
  • Fixed a paste restored from an older session's prompt history being sent to the model as its own label. Placeholder lookup now matches on each entry's display text, but entries persisted before placeholders carried a displayText have none, so they were skipped and their [Paste #N: X chars] label survived into the prompt instead of expanding to the pasted content. Those entries now have their label rebuilt from the ordinal in their key and the content they hold, matching the legacy fallback that the display-text lookup replaced.
  • Fix the exported inline-diff similarity helper name from areLinesSimlar to areLinesSimilar. Thanks to @hafzism. Closes [#968].
  • Fixed string_replace and diff_edit corrupting edits whose replacement text contains $. Both tools passed the model's replacement straight to String.prototype.replace, which treats that argument as a substitution template rather than a literal: $$ collapsed to a single $, $& expanded to the matched text, and $`/$' spliced a whole half of the file into the middle of the edit. Those are ordinary characters in shell scripts, Makefiles, CI YAML and anything that builds a regex, so the bytes on disk silently diverged from the diff the user approved. Replacements are now spliced by index, so the approved preview - in the terminal and over ACP - is what lands. Closes [#1057].
  • Fix macOS native notifications when title or message contains newlines, quotes, backslashes, or Unicode characters by passing arguments out-of-band to osascript. Closes [#1139].
  • Fixed the last row of a markdown table being left outside the rendered table as raw | a | b | text whenever the table ended the reply, which is common. The table pattern required a newline after every row, and replies are trimmed.
  • The MCP setup wizard now shows hand-written .mcp.json servers by name. A server defined only by its JSON key was listed as a blank "• (stdio)", the "Added:" line began with a stray comma, and its edit form opened with an empty name.
  • Ensure mechanical message compression preserves display-only messages in scrollback while excluding them from token counts and the recent message window calculation.
  • Include the provider in the message token cache key. The tokenizer is chosen from provider and model together, so two providers serving the same model name were sharing cache entries and could report counts produced by the other provider's tokenizer.
  • Stop mode-switch model toasts stacking up.

Cycling development modes with Shift+Tab pushed a [mode → model] line into the transcript on every step, and the handler races ahead of prop updates, so a fast cycle left several identical lines in scrollback. Repeats of the same toast are now suppressed, and returning to normal mode posts nothing at all - that only restores your own default model, and the status bar already shows the change.

  • Fix a startup crash when package.json is missing, unreadable, or malformed. Both module-load reads of the version (cli.tsx and the welcome banner) threw before any error handling existed, taking down the CLI on a misbuilt install. Version lookup now lives in a single helper that falls back to unknown, shared by the banner, /help, and /doctor (which previously reported a misleading 0.0.0).
  • Fixed paste detection extracting the wrong text from the input buffer. It assumed every insertion was appended at the end, so pasting with the cursor anywhere but the end stored a truncated placeholder, and deletions or unchanged input reported a slice from the middle of the string. Detection now diffs the buffer against its previous revision and ignores non-positive deltas. Closes [#979].
  • The filterable pickers (/checkpoint load, /resume, model and provider lists) now fit a short terminal. The window only budgeted for the list's own rows, not the titled box and app frame around it, so on an 18-row terminal the hint and the box's bottom border were pushed off screen. The window also now shrinks when the terminal is resized while a picker is open.
  • Fixed paste placeholders silently overwriting each other. Placeholder ids were derived from the number of live entries, so deleting one freed its id for reuse and the next paste clobbered a placeholder that was still in the input - destroying its content and leaving a duplicate label that got sent to the model as literal text. Ids are now namespaced by type (paste_1, file_1) and allocated from the highest id ever used, so a deletion can never free an id. Placeholder lookup now matches on each entry's own display text instead of a paste-shaped regex, which also makes @file mentions delete atomically rather than leaving an orphan entry behind, and lets two placeholders that render identically expand to their own content.
  • nanocoder --plain no longer fails when the last-used provider has since been renamed or removed from agents.config.json. It now falls back to the first configured provider with a notice, as the interactive TUI already did. An explicit --provider that does not exist is still an error.
  • Fixed a {} entry appearing as the oldest prompt in Up-arrow history for new users. The history file is seeded as an empty JSON object, which was being read as a one-line legacy history.
  • The provider settings promptCaching and maxRetries in agents.config.json now take effect. Both were documented but dropped when provider entries were loaded, so "promptCaching": false still sent Anthropic cache breakpoints and a custom maxRetries always fell back to 2.
  • Fixed the run mode boot line never showing the provider, model or mode. It pinned those values on the first render, before initialization had set them, so the line showed only the workspace, and on a narrow terminal it rendered nothing at all.
  • nanocoder run now exits with code 1 when the run fails: a provider or connection error, or a stop by nanocoder.retries (repeated tool calls, empty responses, malformed tool calls). It exited 0 in every one of those cases, because it looked for an error-role message that nothing ever produces, so CI could not tell a failed run from a finished one. --plain already reported these correctly.
  • Prompt scrubbing now applies everywhere once it is switched on, not only to the main TUI conversation. The setting was only passed to the model client by the interactive chat, so nanocoder --plain, ACP sessions (including the VS Code extension), subagents and helper calls such as compaction sent secrets from files, command output and tool results to the provider unscrubbed.
  • Reject unsupported skill:<name> subscription targets with a clear error instead of registering subscriptions that cannot dispatch. Thanks to @1cbyc. Closes [#1011].
  • Fixed a regression where the VS Code extension webview rendered without theme colors after the Tailwind v4 upgrade by migrating custom color variables to an @theme block in the CSS.
  • Timeline index saves are now atomic: timeline.json is written to a temporary file and renamed into place instead of being overwritten in place. If the process dies mid-save, readers only ever see the complete old or complete new index, so a torn write can no longer silently discard every checkpoint of the session. Thanks to @puri-adityakumar. Closes [#1130].
  • Truncated tool output no longer leaks part of a secret. Bash output is cut to fit before scrubbing runs, and a cut through the middle of a key left a fragment such as sk-live-abcdef1234 that no detector recognises, so it went to the provider even with scrubbing on. Cuts now avoid splitting whitespace-separated tokens, so a secret is either kept whole, where it gets scrubbed, or dropped entirely.
  • Fix auto-compact silently no-opping in the TUI after the shared-helper refactor. maybeAutoCompact derived the provider and model from the client and swallowed any failure, so the chat loop's own currentProvider/currentModel were ignored; it now accepts them as explicit overrides. Auto-compact in --plain, ACP, and subagent loops also stops writing the message cap back into stored history when no compaction ran.
  • A model stuck calling a tool that does not exist now trips nanocoder.retries.maxRepeatedToolCalls as intended: the TUI asks whether to continue after three identical calls and --plain stops with an error. The AI SDK was answering each unknown call itself and looping up to ten steps inside a single request, so nanocoder never saw those calls and the cap never applied.
  • An install with a missing package.json no longer posts "Failed to read current version" to the chat on startup, which also pushed the welcome screen away. The update check now uses the shared version helper and skips itself when the version is unknown, and the banner reads "(version unknown)" instead of "vunknown".
  • Fixed update command error detection when later output reports zero errors. Thanks to @anisayakmitra-in. Closes [#974].
  • Fixed /update reporting a failure when the package manager exited 0. hasCommandFailed fell through from the exit-code check into the generic pattern match, so a benign success summary like "0 failed" or "0 cannot be updated" was reported as an error. When the command exits 0, only unambiguous error indicators (command not found, no such file or directory, permission denied, error: lines, fatal) still count as a failure. Closes [#1304].
  • Fix zombie CLI on Nanocoder: Restart Nanocoder Agent Process.
  • Fix unauthenticated cross-origin access to the VS Code companion WebSocket.

The companion server bound to a fixed loopback port (51820) and accepted every upgrade, so any local process or browser tab could deliver {"type":"send_prompt", ...} straight into a running agent and read every broadcast the server pushed out. This made prompt-injection and file-content exfiltration possible on any developer machine that had a session open.

Three changes close the hole:

  1. The server now mints a 256-bit bearer token at startup and only accepts WebSocket upgrades that present it in an Authorization: Bearer <token> header (constant-time comparison). The token is never placed in the URL, where it would show up in browser bars, proxy logs and any other intermediary that happens to inspect the request line.
  2. Any upgrade carrying an Origin header is rejected before the handshake completes. The legitimate extension is a Node ws client and never sends one, so closing that door costs nothing and shuts out browsers specifically.
  3. The server binds to an ephemeral port by default and publishes {port, token, pid, cliVersion, startedAt} to <configDir>/vscode-server.json (mode 0600). The --vscode-port flag remains available for users who need a fixed port (SSH forwarding, etc.); in that mode the discovery file is still written so the extension can pick up the token automatically.

The VS Code extension (plugins/vscode) now reads the discovery file to learn the port and token instead of trusting nanocoder.serverPort. For SSH-style set-ups where the discovery file lives on the remote host, a new nanocoder.serverToken setting lets the user paste the token manually. Stale discovery files (where the recorded PID is no longer alive) are ignored, so a crashed CLI can no longer hold the port hostage; and a live foreign CLI that has taken over the file is left alone on shutdown, so two concurrent sessions do not clobber each other.

  • Fix orphaned ACP session on concurrent init.
  • Reject concurrent prompt submissions while a turn is in flight.
  • Fix web_search TimeoutError not correctly captured as a timeout error
  • Fixed file.changed skill subscriptions silently never firing on Windows. Chokidar reports changed files using the platform separator, so a documented pattern like docs/** was asked to match docs\guide.md and never could — the daemon started, reported healthy, logged nothing, and did nothing. The same mismatch let a root-scoped pattern such as *.md match the nested sub\a.md, dispatching an unattended agent run against a file that was deliberately scoped out. Paths are now normalized to / at the watcher boundary, so the router, activity reports, and the payload handed to a triggered agent all read one path shape. Closes [#964].
  • Fixed models cache path on Windows by using os.homedir() instead of process.env.HOME, which is undefined on Windows. The cache now writes to the correct location instead of creating a literal ~ folder.
  • Hardened Windows native notifications. Notification title and message are no longer interpolated into the PowerShell script; they are passed out-of-band as environment variables and read by a static script. Fixes rendering and parsing edge cases with backticks, quotes and other special characters. Closes [#1138].
  • Fixed the git tools hanging when git or gh needed input. execProcess now closes stdin, sets GIT_TERMINAL_PROMPT=0, and gives up after 60s, so a credential prompt fails with an error instead of freezing nanocoder. Thanks to @Piyushrathoree. Closes [#1344].
  • Fixed skill subscriptions never firing for a brace pattern such as *.{ts,tsx}. The event router's glob matcher escaped {, } and , as literals, so the pattern only matched a path that literally contained the braces. Braces now expand to an alternation, and an unbalanced brace stays literal rather than building a regex that would throw. Negation (!) is still unsupported. Closes [#1012].
  • Hide dotfiles when list_directory uses its default project-root path. Closes [#1237].
  • Fixed lifecycle hook output being garbled when a multi-byte UTF-8 character landed on a chunk boundary. Each chunk was decoded on its own with chunk.toString(), so both halves of a split sequence became U+FFFD. That is the same defect [#1305] fixed for bash and custom tools by routing them through utils/stream-collector.ts; hooks were missed. It is not cosmetic output: pre-tool-use and user-prompt-submit stdout is handed to the model as the reason an action was denied, and post-tool-use stdout is folded into the tool result, so a formatter or linter emitting box-drawing characters, CJK paths or emoji status markers fed the model corrupted text. Each stream now decodes through a StringDecoder and is flushed when the process closes, so a stream ending on a partial sequence no longer drops its last character either. The character cap also stops mid-slice of a surrogate pair.
  • Fixed a lifecycle hook surviving its own timeout. killHookTree sent one SIGTERM to the hook's process group and returned; the SIGKILL beside it ran only in the catch, which is the branch taken when the group is already gone rather than when it refuses to die. A hook that traps SIGTERM, or that is blocked in an uninterruptible call, therefore kept running after the agent moved on - the exact runaway the detached spawn exists to prevent. The timeout now destroys its end of the pipes, signals the group, and arms a deliberately uncancelled SIGKILL escalation a second later, mirroring what custom-tools/handler.ts does since [#1141]: a shell exiting does not mean its group is empty, so the escalation has to outlive it. Windows is unchanged, where the reap is taskkill /T /F and already unconditional.
  • Fix a crash when running /help by bumping the bundled tsc-alias from 1.9.4 to 1.9.5, which includes the upstream fix for justkey007/tsc-alias#276.
  • Fixed a leaked pending slot in the daemon IPC client. If serializing or writing a request threw synchronously, the request's entry stayed in the pending map for the lifetime of the client, one per failed request. The slot is now released and the request rejected explicitly. Closes [#1040].
  • Added a keyboard shortcuts overlay. Pressing ? in an empty prompt now opens a legend of the chat input's shortcuts (submit, new line, history, readline editing, image attachments, mode cycling, compact output, reasoning traces, task list, subagent attach, cancel, exit) instead of typing a literal question mark; ? or Esc closes it. ? typed anywhere after the first character still inserts normally, and /help now points to the overlay. Closes [#1315].
  • Hardened the VS Code action timeline: it no longer snapshots its own before-images, skips a checkpoint rather than recording a wrong one when the workspace scan is truncated, leaves binary files alone, reverts a whole assistant turn at once, validates paths read back from the timeline index, and keeps the chat thread on screen when a revert is refused.
  • Fixed the daemon socket path on Unix when a deeply nested project pushes it past the sockaddr_un.sun_path limit (104 bytes on macOS, 108 on Linux). libuv silently truncates overlong paths rather than failing, so the daemon reported a socket it never bound, its stale-socket cleanup missed the real file, and two projects sharing a truncation prefix could collide on a single socket. Nanocoder now falls back to a stable hashed socket name under the system temp directory (or /tmp if TMPDIR is itself too long), and daemon start reports the path the daemon actually bound instead of recomputing it.
  • Fixed MCP tools bypassing the development-mode policy that governs every other tool. MCP built its own approval predicate rather than routing through the central mode system, which failed in two directions: a tool on a server's alwaysAllow list collapsed to "never needs approval" in every mode, so plan mode executed mutating MCP tools with no prompt despite being the mode you switch into specifically to inspect an untrusted model's intentions without side effects; and every other MCP tool required approval in headless, where no approval handler exists, so daemon-triggered skill runs failed with "Tool execution was denied by the user" when no user was ever asked. MCP tools now take the same posture as built-in tools — headless runs them unattended like execute_bash and the file tools, and plan mode gates them on the server's readOnlyHint annotation, treating an unannotated tool as a possible mutation and hiding it. A server's alwaysAllow list now applies in normal mode only, as its documentation already stated, and can no longer override plan mode. readOnlyHint decides plan-mode availability only: because it is supplied by the same server being gated, it never skips a confirmation prompt in normal mode — the user's own alwaysAllow list remains the only way to do that.
  • Fixed ToolManager.disconnectMCP() rebuilding the whole tool registry from the built-in exports and clearing the custom tool map. The unregisterMany() call above it already removes exactly the MCP tools, so the rebuild was redundant, and it discarded three other things with them: workspace custom tools along with the approval and read_only metadata that plan and headless filtering read, skill and bundle tools registered through registerSkillTool(), and the constructor's removal of web_search when no Brave Search key is configured, which came back as an unusable tool. The only caller today is the shutdown handler registered in initializeMCP, where discarding state is harmless, so nothing user-facing changes. The method is public and any other caller would have hit all four. Closes [#1034].
  • Re-enable project-level MCP credential scanning. loadAppConfig dropped the loader wrapper's source before validateProjectConfigSecurity filtered on it, and env substitution ran before the scanner so $API_KEY looked hardcoded. Capture source and pre-substitution rawEnv/rawHeaders so only real literals warn. Closes [#1248].
  • Confined the MCP readOnlyHint annotation to plan-mode availability, and made "enabled": false actually disable an MCP server. The readOnlyHint a server reports was being copied onto its registered tool entries, where ToolManager.isReadOnly() also decides whether ACP captures a checkpoint before the call and whether the tool joins a parallel execution batch — so a server annotating itself read-only could talk its way out of a restore point. The hint now lives on the MCP tool mapping, which only plan-mode filtering reads, exactly as the documentation describes. Separately, enabled was loaded from config and then ignored: every configured server connected regardless, including in headless runs where MCP tools execute unattended and "enabled": false is the only documented opt-out. Disabled servers are now skipped before any connection is attempted.
  • Moved the VS Code mode selector onto the chat composer row with accessible mode labels and dropdown state.
  • Fixed Settings → Nanocoder Shape doing nothing. The welcome screen redesign hardcoded the wordmark to the block font, so the panel kept previewing and saving a shape that never reached the banner. The banner reads the preference again, and now subscribes to preference writes so the new shape appears as soon as the panel is closed rather than on the next restart. The width and height thresholds that decide between "NANOCODER", the "NC" monogram and no wordmark at all are per-font too, so a short font like chrome gets the full wordmark on a 50-column terminal and a tall one like huge no longer pushes the menu and tip off screen. The default the settings panel reports is also the one now on screen: block, not tiny.
  • Fixed the /settings → Notifications panel dropping the triggeredRunComplete event. The panel seeded its fallback config with only three events and rendered a row for each, then wrote the whole object back on every toggle - so flipping any switch persisted a preference with no triggeredRunComplete key, and the daemon's "triggered run completed" notification silently stopped firing. The event now has a default and a row of its own. Also documented the triggeredRunComplete event in both preference tables, and noted that the terminal bell needs the master Notifications toggle on and that tmux swallows the bell unless monitor-bell is enabled.
  • Added improved spacing and removed the copy button from user messages in the VS Code chat panel.
  • follow-ups to the Ctrl-T task-list toggle: the status-bar badge now yields space under width pressure instead of squeezing the session name and editor pill, the unread marker only fires on real task changes, and the "todo" naming is aligned with the "Tasks" list it summarises
  • Fixed a slash command with leading whitespace being sent to the model as a chat message instead of running. parseInput trims before testing for the / prefix but the routing in handleMessageSubmission did not, so /help reached the LLM. With lifecycle hooks configured it also fired user-prompt-submit and consumed buffered session-start context for an input that never reaches the model. Both the routing and the hook's local-action check now trim, matching the ! bash passthrough. Also added nanocoder.hooks to the agents.config.json JSON Schema — hooks landed alongside the schema and were missing from it, so every documented hooks example was flagged as an unknown key in editors.
  • --mouse and --no-mouse are stripped from a run prompt. They were documented but not filtered, so nanocoder run "fix the tests" --mouse sent the model a prompt beginning with the word --mouse. Display flags on a non-interactive run are meaningless, exactly like the --alt-screen pair that was already stripped.

  • The run prompt parser exists once. It lived inside cli.tsx with a hand-written copy in cli.spec.ts, and the copy had fallen six flags behind the original — --mode, --json, --output-format, --trust-directory and the alt-screen pair were removed by the parser and left in the prompt by the copy, so those tests passed against a parser that is not shipped. No test written against a duplicate can notice it has drifted, because it is testing the duplicate.

It moves to run-prompt-args.ts, imported by both. The tests now derive their cases from the parser's own flag lists rather than a list typed beside them, and a second test reads the flags out of --help and fails if one is documented but not filtered. That second test is what found the --mouse pair above, which a hand-written enumeration had missed twice.

  • Paste bracketed text at the caret, not at the end of the input.

A terminal paste (DECSET 2004) used to append to the end of the prompt no matter where the caret was, and even after splicing the new content in at the caret, the caret itself snapped back to end-of-value. The TextInput caret is now read before the splice, the splice lands exactly at that offset, and the caret stays where it was set — so pasting with the cursor in the middle of existing text drops the placeholder exactly where you were editing and parks the caret right after it. Pasting at end-of-value still works the same way it always did, and the heuristic paste detector paths are unchanged.

Also exposes a TextInputHandle (getCursorOffset / setCursorOffset) on TextInput so the parent can drive the caret without lifting its state up the tree.

  • Multi-line paste placeholders now show a line count instead of a character count, so pasting a code block, log, or stack trace reads as [Paste #1: 7 lines] rather than [Paste #1: 157 chars]. A single trailing line break is not counted as an extra line. Long single-line pastes keep the [Paste #N: X chars] label, and a paste that arrives in chunks updates its label as it grows. Closes [#1292].
  • Update the message token cache in place instead of copying every entry into a new map on each cache miss.
  • nanocoder --plain --json run reports now include steps, the number of model round-trips in the run (retried turns included). It differs from the length of toolCalls, since one step can make zero or several tool calls, and it is 0 when the run stops before the model is called. This gives the agent evaluation harness in [#1197] a step count to measure.
  • Fixed --plain runs reporting success after the model's reply was cut off at the output-token limit. A truncated turn comes back as content with no tool calls, which is exactly what a finished turn looks like, so the headless loop returned kind: 'success' carrying half a sentence — and a run whose entire deliverable was a tool call could exit 0 having written nothing. finishReason is now carried out of the AI SDK client instead of only being logged, and a truncated content-only turn is asked to continue from where it stopped, up to nanocoder.retries.maxTruncatedTurns times (default 2, 0 restores the old accept-the-fragment behaviour). The nudge steers the model to make the outstanding tool call rather than re-explain itself, since spending the whole output budget narrating before acting is what triggers this.
  • A shell command that exits non-zero is now reported as failed in --plain --json output and over ACP. execute_bash returned its output as plain text, so the exit status never reached the tool result: the JSON report filed a failing build's output under result rather than error, and ACP clients such as Zed showed it as completed. The handler now reports the failure alongside its text, which the model still receives unchanged. Closes [#1391].
  • Professional Tone now applies as soon as you toggle it in /settings. Previously the completion note changed immediately but the system prompt's TONE section waited for the next mode or model switch, so the model kept its old register and the toggle looked broken. buildSystemPrompt also takes professionalTone as an explicit argument instead of reading the preference file itself.
  • Professional Tone now uses a shortened TONE section under the nano tool profile, matching how every other prompt section is slimmed for tiny models. The full section cost ~695 characters on a prompt that is deliberately minimal; the nano variant is ~228.
  • Fixed the output-token ceiling being unsettable, which silently truncated long replies. The AI SDK renamed this setting to maxOutputTokens in v5; nanocoder was still sending the v4 name maxTokens, and because object spreads bypass TypeScript's excess-property checking it was dropped silently rather than failing to compile. Every request therefore fell back to whatever ceiling the provider inferred from the model id.

That matters most on sdkProvider: "anthropic", where @ai-sdk/anthropic falls back to 4096 output tokens for any model id it does not recognise as a Claude model. An Anthropic-compatible endpoint serving something else - MiniMax, for instance - had every reply cut off at 4096 with no error.

Two changes:

  • The setting is now sent under the name the installed SDK reads, so /tune's max-tokens control works again. It has been inert since the v5 upgrade, which also means the Nano (low-end hardware) preset's maxTokens: 2048 now actually applies.
  • Provider entries in agents.config.json accept maxOutputTokens. Headless runs never carry /tune parameters, so this is the only way to raise the ceiling in CI. A /tune value still wins where one is set.

Tests assert what reaches the wire rather than what typechecks, since typechecking demonstrably could not catch this.

  • Fixed the bash tool silently dropping stdout when stderr was noisy; each stream now gets its own output budget and truncation marker. Worst case memory goes from 5MB to 10MB since both streams can now hit the cap at once. Closes [#1140].
  • Fixed ACP prompts racing with each other by claiming session turns before asynchronous setup begins, preventing overlapping prompts from corrupting turn state. Built-in slash-command replies are kept in persisted session history for replay, while command-only sessions are not saved. Closes [#1038].
  • Fixed {{args}} in custom commands without declared parameters so it receives the raw command arguments instead of an empty string. Closes [#1035].
  • Fixed read_file never rendering its metadata-only view. The formatter decided a response was metadata-only by testing result.startsWith('File:'), but the metadata branch emits File Information for "...", so the test could never match. The same condition also required no line range and a file over the 1500-line preview threshold, neither of which applies — a metadata_only read returns before either is considered. As a result a metadata_only: true read was displayed as an ordinary content read: no "(metadata only)" marker, the content-read layout instead of the metadata layout, and a token count measured from the metadata block rather than the full file. Detection now keys off the request flag that actually selects the response shape — matched with Boolean() so a truthy non-boolean value (as the XML tool-call fallback can produce) is still recognised, and set before any file read so a directory or symlink still renders the metadata layout even though reading its content throws.
  • Fixed the prompt caret jumping to the start of the text after a redo (Ctrl+Y), which sent the next keystroke to the front of the line - redoing "abcde" and typing "X" gave "Xabcde". TextInput only re-clamped the caret when the value shrank, so a redo that restored a longer value stranded the caret at the offset the preceding undo had clamped it to. Value replacements that come from outside the component - undo, redo, draft restore, a programmatic clear - carry no caret of their own, so the caret is now parked at the end of the restored text the way a fresh mount does. Mid-text typing is unaffected; the component still tracks its own edits and leaves the caret where it is.
  • Fixed two /repomap indexing bugs and added a progress spinner. Python docstring bodies are no longer indexed as definitions (the """ and ''' branches were unreachable in the comment-stripping pattern, so names inside a docstring were reported as real symbols and sorted ahead of them), and a repo holding exactly maxFiles indexable files is no longer reported as truncated. /repomap now shows a "Building repo map" spinner while it scans, and reuses the shared calculateTokens helper for its budget maths.
  • Fixed the VS Code extension dropping token and estimated-cost footers when a saved chat is reopened. Completed ACP turns now persist their response usage on the matching assistant message and restore it during history replay, while existing sessions without usage metadata continue to load unchanged. Closes [#1097].
  • Restored the VS Code panel features a bad merge in [#898] reverted: the action timeline and its undo, slash command autocomplete, editor-tab drag and drop, the MCP settings button opening .mcp.json, and the denied-edit status icon. The work summary from [#898] is kept, and no longer opens a second box that never settles when a tool reports back after Stop, nor a box for whitespace-only reasoning.
  • Prevent fetch_url from following HTTP redirects before the destination has been validated, protecting the no-approval tool from redirect-based access to private and loopback addresses. Closes [#1089].
  • Keep shared cron jobs alive until every subscriber unregisters. ScheduleEventSource.unregister() used to stop the underlying job on the first call even when other subscriptions still used the same expression. Closes [#1239].
  • search_file_contents now shows what it found. The expanded tool display lists the file:line hits under the match count, capped at the same 20 lines as execute_bash output with a … (+N more lines) note, instead of only the query, flags, and a count. Closes [#1290].
  • Fixed the semantic memory lock being stolen from a process that was still using it. The lock file's mtime was stamped once at acquisition and never refreshed, so "held for more than ten seconds" and "abandoned" were the same test. Any write that legitimately took longer had its lock deleted by a waiter in another process; both then ran the critical section at once and finished with atomicWriteFile, so the loser's memories were silently dropped rather than merged. The in-process queue meant this only bit across processes - two sessions in one repo, or a session plus the daemon, which is exactly what repo-scoped memory is for.

Ownership is now decided by whether the recorded owner is still running, with the heartbeat as a tiebreak. A holder refreshes the lock's mtime every two seconds while it works, so a waiter can tell "still going" from "gone". A dead owner is reclaimed immediately however fresh the file looks, which also removes the ten-second stall a crashed session used to impose on the next write. A live owner whose heartbeat has stopped for longer than the stale window is still reclaimed, covering both a wedged holder and a recorded pid that has been recycled by an unrelated process.

  • Wire semantic memory recall into subagent and daemon runs, serialize writes across manager instances and processes, and cap each repo memory file at 500 entries.
  • Rank recalled memories by how much of the query they cover, skip tool-call narration in reversal detection, and drop noisy /memory propose candidates.
  • Added a visual saving indicator in the CLI status line that briefly displays whenever session state is autosaved to disk. Thanks to @rishu685. Closes [#932].
  • Added keyword search to the /resume session selector. Typing now filters the session list by title, using the same type-to-filter list as the model picker, so an old conversation no longer has to be found by scrolling and guessing by date. The filter matches the title only, not the message count or age shown next to it; Backspace edits the query, arrow keys and Enter pick from the filtered results, and Esc cancels. Closes [#1316].
  • Re-enabled the /settings scroll-indicator test. It was skipped because no tab exceeded the four visible rows, so the indicator could not be triggered organically, with a note to restore it once one did. That happened this cycle: Appearance gained Alternate Screen and Mouse Wheel Reporting alongside the fullscreen TUI and now has five rows, and Behavior and Advanced have six each. The settings window is the busiest it has been and had no coverage of its own scrolling; the test now also asserts the hidden-row count rather than only the label, so an off-by-one in the window arithmetic fails it.
  • Display compound bash commands on separate lines in execute_bash output
  • Fixed nanocoder skills add letting a repository or ref reach git as an option. cloneAtRef falls back to git init + git fetch origin <ref> when git clone --branch <ref> fails, and the ref was passed as a bare positional - but git keeps parsing options after positionals, so a ref of --upload-pack=<command> made git run that command through a shell. That is reachable from --ref, and, more seriously, from an index entry's ref: the index is remote JSON fetched before anything is shown, so a compromised index, a hostile NANOCODER_SKILLS_INDEX, or a typosquatted entry executed code before the trust prompt that the whole staged-install flow is built around. Both fetch calls now pass --, and a ref beginning with - is refused by name rather than reaching git at all.

Repository locations are now validated too. git clone -- <repo> stops a leading dash being read as a flag but does nothing about git's remote helpers: ext::<command> is a URL, not an option, and it runs that command. Stock git blocks it via protocol.ext.allow=never, so this was not exploitable by default - but that is git's default doing the work for a property this code claims to own, and it is one git config away from being untrue. A repo must now be an https/ssh/git/file URL, a git@host:owner/repo address, or a local path; anything else, including any name::address transport, is refused before the clone. The check runs at the single boundary every path goes through, so the user's own argument, --ref, and index entries are all covered by it.

  • Fixed checkpoints capturing no files in a repository with no commits. getModifiedFilesResult diffed against HEAD, which does not exist on an unborn branch, so the whole scan fell to its catch and reported git as unavailable - every file in a git inited workspace looked unmodified, and a checkpoint taken there restored nothing. The scan now attempts the HEAD diff directly and falls back to git diff --name-only --cached when it fails, so a staged tree (git init && git add ., or git checkout --orphan, which starts with a fully populated index) is captured rather than skipped; --others alone would have missed all of it, because git ls-files --others excludes anything already in the index. Attempting the diff rather than probing with git rev-parse --verify HEAD beforehand keeps the common case, a repository that has commits, at the two git processes the scan always spawned, which matters because it runs on every checkpoint and twice per ACP tool call. This affects checkpoint creation and both timeline scans, which branch on the available flag. A genuine git diff failure still falls through to the existing error handling and reports git as unavailable, unchanged.
  • The status bar no longer truncates mid-word on an 80-column terminal. It budgeted its segments against the full terminal width while rendering inside a wrapper indented by three columns, so the row overflowed by exactly that much and the terminal cut it ("auto-accept mode o"). The indent is now part of the budget, so a segment is dropped or a name is ellipsised cleanly instead. Closes [#1380].
  • Restored the (default) marker on the /status panel's Git line. Adding the git branch to the boot summary removed the isDefault case from the shared formatGitStatusSummary helper so the boot summary could render a bare ⎇ main, but /status reads the same helper and silently lost its marker too. The shared helper reports every marker again and the boot summary drops the default one itself, which is the only place the shorter label was wanted.
  • On a narrow terminal the status row now drops its optional hints (such as "(Shift+Tab to cycle)") before truncating the session name or the active editor file name.
  • Fixed the subagent activity card in the ACP (VS Code extension) integration truncating token counts to whole thousands (Math.floor(tokenCount / 1000)), so 1–999 tokens showed as 0k and 1500–1999 showed as 1k. Now uses the existing formatCompactTokenCount formatter from source/usage/format.ts, matching the precision already used by the per-response usage indicator (e.g. 1.9k tokens instead of 1k tokens). Closes [#1133].
  • Fixed the conversation summariser's truncation overshooting its character budget. The ... [truncated N chars] notice was appended after slicing to the limit, so every truncated system prompt, tool-call argument, and tool result sent to the summariser ran over budget by the width of that notice. The kept length is now solved for so the notice fits inside the limit, and a budget too small for the notice at all falls back to a plain slice. Thanks to @aashu2006. Closes [#1135].
  • Fixed markdown tables with many columns rendering wider than the terminal and breaking their own borders. Columns now shrink to fit, and a table too wide to fit at all is left as markdown. Closes [#1404].
  • Fix session selector dismissing on any keypress instead of only Escape when no sessions exist
  • Tool-execution safety fixes:

  • Tool approval: never auto-approve a call whose arguments fail schema validation. The approval prompt now renders together with the validation error, so a malformed call is never executed without explicit consent. (Deliberate trade-off: a malformed approval-required call now costs the user a consent prompt where it previously self-corrected without one — the price of "nothing executes without consent".)

  • MCP tools: the executed handler now runs the same lenient schema type-check the approval prompt shows, so a "wrong type" call is rejected locally before it reaches the server instead of relying on the remote validator alone. MCP was the last registry path that bypassed withValidation.
  • Custom tools: cap captured stdout and stderr while streaming, so a large-output tool can't exhaust memory before the final truncation runs. Each stream gets its own budget via the same collector the built-in bash executor uses (now shared in source/utils/stream-collector.ts), so a stdout flood can't silently swallow the stderr explaining why the tool failed.
  • Custom tools: on timeout, the shell is spawned detached (Unix) and the whole process group is signalled, so a backgrounded descendant can no longer outlive the tool's timeout. The SIGKILL escalation now targets the group and deliberately outlives the shell's own exit, since the shell exiting does not mean its group is empty.
  • Long tool output no longer floods the transcript. write_file, string_replace, diff_edit, and tools without a formatter (such as MCP tools) now show at most 20 lines followed by … (+N more lines · /expand n), the same cap execute_bash output already had. When the cap cuts into a diff, the note says how many of the hidden lines are edits (+N more lines, K changed), and a write_file overwrite diff skips long unchanged stretches so an edit deep in a large file is still shown. The new /expand <n> command prints a single tool result in full, including one folded into a compact tally, and /expand on its own lists recent results, so reading one result no longer means switching every result to expanded view with Ctrl+O. Closes [#1289].
  • Startup initialization now waits for the directory-trust disclaimer. useAppInitialization's mount effect ran on the very first render, before the user could answer the trust prompt — React runs every hook regardless of which JSX branch a component ultimately returns, so the <SecurityDisclaimer /> early return in App.tsx never gated it. An untrusted directory could therefore have its agents.config.json / .mcp.json read, its stdio MCP servers spawned via TransportFactory.createStdioTransport(), and its provider entries resolved — including a project provider shadowing the global one by name, which for github-copilot / chatgpt-codex attaches a live OAuth bearer token to a config-supplied baseURL. The gate now lives inside the effect itself and is ref-guarded, so nothing is read or spawned until trust is confirmed and initialization still runs exactly once afterwards. --trust-directory is unaffected. Closes [#1244].
  • Fixed bash and custom-tool output getting garbled when a multi-byte UTF-8 character landed on a chunk boundary. Closes [#1305].
  • Vertically center the welcome banner on tall terminals.

The banner sat flush at the top of the viewport on terminals taller than the banner itself, so a maximized window left the content stranded at the top. The banner's outer Box is now sized to the available rows and centers its children, so the block sits in the middle of the viewport at every terminal height while still fitting a standard 80×24 screen.

Also adds regression tests for the boot summary's working-directory label and for the new tagline text.

  • Use the same button-based dropdown style for the provider and mode pickers as the model selector in the VS Code composer Configuration popover.
  • Added slash command quick actions to the VS Code extension chat panel. Typing / in the input opens an autocomplete menu listing /test, /explain, and /doc, which insert a human-readable prompt template into the textarea so the user sees and can edit exactly what gets sent, alongside the existing /clear and /copy commands, which complete to their name and run as they always have. The menu only opens on a slash that starts a line, so URLs and paths are left alone.
  • Added a TUI-style welcome screen to the VS Code extension's chat panel that displays the Nanocoder logo and open-source mission statement. It automatically hides when a session is loaded or messages are sent.
  • The welcome screen now fits a standard 80x24 terminal. It sized itself against the whole terminal height, but fullscreen mode clips at the chat viewport — the terminal minus the input footer — so the last menu item and the tip were cut off with no hint anything was missing. The banner is now given the rows it actually has, and drops the block wordmark before the menu and tip when they are tight. Closes [#1330].
  • write_file now shows a real diff (added/removed lines) against the file's previous content when overwriting an existing file, matching string_replace's colored-diff treatment, in both the confirmation preview and the post-execution result — including auto-accept/yolo mode, where the file is overwritten before the result is ever displayed. Writing a brand-new file still shows the full syntax-highlighted dump. Closes [#1288].

If there are any problems, feedback or thoughts please drop an issue or message us through Discord! Thank you for using Nanocoder.

Contributors

This release shipped thanks to @1cbyc, @2409324124, @24thAbhinav, @89799969, @A-S-Manoj, @aakash-env, @aashu2006, @addyCooks, @aiapienthusiast, @akramcodez, @alexsmolya, @AniketR10, @anisayakmitra-in, @arpan7sarkar, @AryanNandanwar, @awhite0030, @bigattichouse, @ColumbusLabs, @coralsundy, @DeepamJha, @Dhirenderchoudhary, @egmar, @ethanhawkes-gif, @Gambit-Checkmate, @googio, @hafzism, @hiarun02, @itsnevu, @jan-krieg, @kishore280, @laurinMlt, @macjayz, @mikemikimike, @mouse-value-add, @mrinalg297, @mrprohack, @niukanen1, @OllieinCanada, @Piyushrathoree, @puri-adityakumar, @raheebgill29, @RaunaK-cap, @rishu685, @rohanshrma222, @RRXXZZYY, @Rudra2637, @saksham-rathore, @shoryabansalgithub, @Shreya657, @Shreyasnalle, @soumojit-D48, @stag7824, @Swayamcodes, @tisankanj, @tusharui, @vansh2408, @will-lamerton, @yashksaini-coder, @yulinlina, @yunuschoudhurywork-lang and @Zer0codestuff.

First-time contributors: @1cbyc, @24thAbhinav, @89799969, @aakash-env, @aashu2006, @aiapienthusiast, @alexsmolya, @AniketR10, @anisayakmitra-in, @awhite0030, @ColumbusLabs, @coralsundy, @egmar, @ethanhawkes-gif, @Gambit-Checkmate, @googio, @hafzism, @hiarun02, @itsnevu, @jan-krieg, @laurinMlt, @macjayz, @mouse-value-add, @mrinalg297, @mrprohack, @niukanen1, @OllieinCanada, @Piyushrathoree, @puri-adityakumar, @raheebgill29, @RaunaK-cap, @rishu685, @RRXXZZYY, @Rudra2637, @saksham-rathore, @shoryabansalgithub, @Shreya657, @Shreyasnalle, @soumojit-D48, @stag7824, @Swayamcodes, @tisankanj, @tusharui, @vansh2408, @yulinlina, @yunuschoudhurywork-lang and @Zer0codestuff. Welcome, and thank you!

Installation

:::bash
npm install -g @nanocollective/nanocoder

Usage

:::bash
nanocoder

Full Changelog: https://github.com/Nano-Collective/nanocoder/compare/v1.30.0...v1.31.0

Source: README.md, updated 2026-09-26