Repository navigation
Releases: ChrisZhangJin/nanopi
Release list
v0.13.0
Full Changelog: v0.12.1...v0.13.0
v0.12.1
v0.12.0-preview — plugins can act, not just observe
nanopi v0.12.0-preview
A preview of v0.12.0. The headline is plugins: a WASM extension can
now do things rather than only see them, and /reload picks up plugin
changes without a restart.
Feature-complete and tested — 901 automated tests green (0 ignored, 0
warnings on every build) plus a 64-row manual pass against a live model,
which found and fixed seven defects. Tagged -preview rather than
0.12.0 because it has only been exercised on Linux against one
provider surface; see Known gaps at the bottom.
Plugins can now act, not just observe
v0.11 gave plugins tools, slash commands and lifecycle events — all of
it read-only apart from returning a tool result. v0.12 adds an outbound
surface in five stages. Every capability is deny-by-default and
named in the plugin's [[extensions]] entry.
| Grant | Capability |
|---|---|
allow_store |
host-store-get / host-store-set — a keyed, quota-bounded, crash-safe store (temp file + rename), one file per plugin, surviving restarts and /reload |
allow_context |
host-set-context — contribute up to 4 KiB to the model's system prompt, attributed to the plugin |
allow_tools = [...] |
host-call-tool — call named built-in tools, per tool, never wholesale |
allow_send_message |
host-send-user-message — start or steer a turn, loop-guarded and capped per session |
host-notify needs no grant: it is output rather than access, so it is
bounded by a per-turn rate limit instead of a gate, and the host
applies the attribution prefix so one plugin cannot pose as another.
/tools gained a Plugin grants section, so what a plugin may do is
never invisible. A plugin granted nothing still gets a row reading no grants — "installed and powerless" and "not installed" must not look
the same.
Three details that are load-bearing rather than decorative:
- A plugin's tool call carries a
ToolCallOriginand is deliberately
not written to the session transcript. Atool_callthe model never
emitted replays as atool_usenobody answered, which is the exact
shape that used to make sessions permanently unresumable. The
consequence is stated plainly:--continuewill not show what a plugin
did, so the host prints a live disclosure line per plugin-initiated
call instead — on its own budget, so a plugin cannot suppress it. - Context contributions are disclosed, and the host neutralises any
header-shaped line in the body. host-send-user-messageis the only grant that spends money, so it
warns at startup on its own, and carries three separate loop bounds.
/reload now reloads [[extensions]]
For two releases /reload re-read skills and hooks and deliberately
skipped plugins, and said so. It no longer skips them — edit a grant,
run /reload, done.
Built around the failure cases rather than the happy path:
- Load before unregister. A plugin whose
.wasmno longer compiles
never becomes live, so its previous instance stays live and callable,
and the report line names the failure. You asked to pick up an edit;
the edit not compiling should not also take away the plugin that was
working a second ago. - A call in flight when the swap lands is refused in-band, with two
different wordings on purpose. One for a call refused before entering
the guest; one for a call that had already entered, which says the
result is discarded and that any side effects it already had stand. - Never "extensions reloaded" on its own — a reload that claims more
than it did is worse than one that admits a gap.
State across a reload is decided per kind: host-store files survive,
plugin_send's loop guard and spent budget survive, and a context
contribution survives for a plugin still holding allow_context and is
dropped otherwise, with the report naming every drop.
Five example plugins, two of them worth installing
Catalogued in examples/README.md.
| Plugin | What it is for |
|---|---|
wasm-plugin-minimal |
The skeleton to copy — boilerplate and "your part" split by one marker |
wasm-plugin |
The reference: four tools, slash commands, all three command actions |
wasm-plugin-events |
Event subscription, and the two-list grant |
wasm-plugin-memory |
Durable project memory. Facts become markdown under .nanopi/memory/ you can read, edit, diff and commit. Only the compact index enters the model's context; it calls recall for the full text of whatever looks relevant |
wasm-plugin-report |
Where the time went. Per-tool counts, duration and failures across sessions — tool_execution_end carries a duration_ms that nothing else persists, so "why was that slow" had no answer at all |
The last two are deliberately complementary: between them they exercise
the whole outbound surface. Neither does automatic capture on events,
and the memory plugin's README explains why — event delivery is
drop-on-busy and does not queue, so a memory system built on it would
quietly forget things with no way to tell you which.
message_end now carries the assistant's reply
Previously {turn_count, response_length} — a number. A plugin could see
what the user said and every tool result but never what the model said,
so "remember the preference" was possible and "remember the conclusion"
was not.
Now four fields, with two deliberate choices: response_length keeps
its old meaning (the whole reply's byte length, so existing scripts
read the same number), and truncation at 16 KiB announces itself three
ways — the flag, the length mismatch, and an in-band marker in the
text. The in-band marker is the one that matters: a consumer that only
reads response must not be able to draw a conclusion from half a
sentence.
behaviour change for existing [[hooks.message_end]] scripts. See
Compatibility.
Also in this release
- One vocabulary for the eleven hook events.
pre_tool_use→
tool_execution_start,post_tool_use→tool_execution_end,
user_prompt_submit→input,session_end→session_shutdown. No
alias, no deprecation window — a retired key fails to load and the
error names its replacement. - Honest session payloads.
session_idon lifecycle events is
always the real id; it used to hold the compaction reason. The reason
moved toarguments.reason. - Per-tool
executionMode, plus[tool_exec_overrides].bashnow
declares itself sequential, which fixes two concurrentbashcalls
on one file losing an update while both reported success. Set
bash = "parallel"to take the speed back. /modeland thinking-level switches enter the transcript.
ModelChangehad a reader, a replay path, an/exportrenderer and a
roundtrip test — and no writer, ever. Both writers now exist.- An interrupted tool call no longer bricks a session. An orphan
tool_callused to replay into a request both APIs reject, making the
session permanently unresumable behind a provider 400. nanopi now
synthesises a result that says the outcome is unknown — not
"failed", which would be a lie in the case that matters most. - Inline
<think>blocks render as thinking on any OpenAI-compatible
endpoint, gated on position rather than vendor.
Fixed by the manual test pass
Seven defects, each with a regression test verified by reverting the fix
and watching it fail:
| Defect | Effect |
|---|---|
| Parallel tool batches | Every batch of 2+ tools failed on the Anthropic transport, after the tools ran — side effects done, turn dead before the model saw a result |
| Settings validation | One unsatisfiable matcher silently disarmed every hook in the file behind a scrolling warning |
Shift+Tab |
The documented default never fired in any real terminal |
| Extended thinking | Silently inert on the whole Claude 5 family while three UI surfaces reported it on |
Mid-turn /reload |
Reloaded nothing and blamed a valid config |
| Plugin stores | Ignored NANOPI_HOME, and fell back to a relative path on Windows |
/tools rendering |
Plugin names past 20 columns ran into the value beside them |
Compatibility
Breaking: hook event names. The four retired keys above fail to
load, with the error naming the replacement. NANOPI_EVENT is renamed
identically.
Breaking: session_start / session_shutdown fire on more than
exit — around every agent swap (/new, /resume, /fork,
/import), with arguments.reason saying which.
Breaking: session_id no longer carries the compaction reason. Read
arguments.reason instead.
Behaviour change: message_end hooks now receive the reply text.
Anything reading turn_count or response_length is unaffected, but a
script that logs arguments wholesale or bills by payload size now sees
up to 16 KiB of reply per turn. Check those before upgrading.
Behaviour change: bash is sequential by default. A batch of bash
calls costs the sum rather than the max. [tool_exec_overrides] bash = "parallel" restores the old behaviour.
Behaviour change: unknown slash commands are refused rather than sent
as chat. Use //name to send it as text; a leading space is no longer an
escape hatch.
Behaviour change: an unsatisfiable matcher is now fatal. An
[[hooks.input]] entry with any matcher other than "*" used to load
and silently never fire.
Install
Assets are attached to this release for linux-x86_64 (glibc and static
musl), macOS aarch64, and Windows x86_64. WASM plugin support needs the
wasm feature:
cargo build --release --features wasmThe stock release binary has no WASM runtime and says so — it reports how
many [[extensions]] entries it ignored rather than failing silently.
Known gaps — why this is -preview
- Verified on Linux against one provider surface only. Everyt...
v0.10.1
v0.10.0
v0.9.3
v0.9.2
v0.9.1
nanopi v0.8.0
nanopi v0.8.0
A big-features release: PI-parity across auto-compaction, fork-tree
navigation, extended thinking, vision input, and the full slash-command
menu. 301 lib tests passing (was 254 at v0.6.0).
Highlights
Auto-compaction
Token-based threshold using each model's real context window; fires
both pre-turn (safety) and post-response (matches PI). Visible
[compacting context (threshold)…] / [compacted N messages via summary] markers in the transcript.
Session fork tree
Esc-Esc(or/fork) opens a DFS-rendered picker walking the
parent_idchain, showing every entry type (user / assistant /
tool / compaction) — old branches stay visible after forking.- On select: 3-option "Summarize branch?" modal (No / Default /
Custom prompt). LLM summarizes the abandoned tail and injects it as
aBranchSummaryentry into the new branch's context so the model
picks up where you left off.
Extended thinking
Shift+Tab cycles Off → Minimal (1024) → Low (2048) → Medium (8192)
→ High/Xhigh/Max (16384). Anthropic adapter emits
thinking: {type: enabled, budget_tokens} with model-capability
gating (claude--4-, claude-3-7-sonnet).
Vision
read on a PNG / JPG / GIF / WebP (magic-byte detected, no
extension trust) attaches a base64 image block to the tool result.
- Anthropic adapter forwards it as multimodal
tool_result
content. - OpenAI-compatible endpoints get an auto-injected user-role
follow-up withimage_urldata URLs (OpenAI tool role is
text-only per spec). - Vision-model gating downgrades to a placeholder for text-only
models. 3.5 MB raw image cap under Anthropic's 5 MB encoded limit.
Provider selector
New api_kind config + --api-kind CLI flag:
openai(default) — talks to/chat/completions; works with
oneapi / newapi / litellm / OpenAI / DeepSeek / Groq …anthropic— talks to/v1/messages(native Anthropic API).
Announces• api_kind = anthropic — talking to <url>/v1/messages
at startup.
Slash-command palette (13 total)
| Command | What it does |
|---|---|
/model |
Switch to a different model |
/new |
Start a fresh session — clears view, keeps scrollback |
/resume |
Pick a past session; replays its full transcript to scrollback |
/fork |
Open the fork/tree picker at a past turn |
/session |
Print stats — id / name / model / turns / tokens / cost |
/name [text] |
Show or set the session name |
/copy |
Copy last assistant reply via OSC 52 |
/export [path] |
Bare → self-contained HTML in cwd; .jsonl → raw dump |
/import <path> |
Load a JSONL as a new session and switch to it |
/compact |
Force context compaction |
/hotkeys |
Dump the keybinding cheat-sheet |
/quit / /exit |
Exit |
TUI polish
- Fish-style word-coalesced undo — Ctrl+Z / Ctrl+_ / Ctrl+-.
- Prompt history — 100-entry ring; ↑/↓ on empty editor recall
past submissions. - Esc interrupts a streaming turn immediately (biased select
drops the reqwest stream); "Operation aborted" marker in
transcript; aborted turn gets a directive-only marker in context
so the next turn's LLM doesn't try to "continue" the abandoned
reply. - Long lines wrap to multiple rows instead of being truncated at
the terminal edge (via Paragraph + Wrap). - 60-second send-timeout on both providers so a hung gateway
can't stall the session forever. - Duplicate-tool-use dedup — gateway-replayed tool_use ids get
a fresh uuid before push so we never emit duplicatetool_result
blocks. /newclears the visible screen but preserves scrollback;
scroll up to review previous content./resumereplays the whole transcript (user echoes, assistant
text with markdown, tool cards, compactions, branch summaries).
Provider fixes
- reqwest
.send()wrapped intokio::time::timeout(60s)on both
adapters — no more indefinite waits on stuck connections. - OpenAI adapter routes tool-returned images via a follow-up
user-role message withimage_urldata URLs. - Anthropic adapter emits multimodal
tool_resultcontent when
images are attached and the model supports vision. - Retry/backoff for 408 / 429 / 5xx / queue_timeout preserved.
Binary
nanopi-v0.8.0-linux-x86_64 — 3.8 MB stripped ELF, x86_64-linux-gnu.
Drop into /usr/local/bin/ or wherever your PATH prefers. Run
plain nanopi in a TTY to get the ratatui TUI; pipe input or add
--no-tui for the classic rustyline mode.
Config lives at ~/.nanopi/config.toml. Minimal example:
model = "claude-opus-4-7"
base_url = "https://api.anthropic.com"
api_kind = "anthropic"
api_key_file = "~/.nanopi/api_key"