Commit graph

851 commits

Author SHA1 Message Date
Dale Lowe
3e4f872654
feat(ai): add Qwen Token Plan Individual CN provider (#7989)
Closes #7847
2026-11-04 11:32:45 +01:00
gaoyk19
14b804d809
feat(ai): add requiresNonNullAssistantContent compat flag (#8118)
Closes #8063
2026-11-04 11:29:54 +01:00
Julien Chaumond
592667dffd
docs(coding-agent): llama.cpp tweaks (#11624)
use llama.cpp's default port and remove the stale `--jinja` note

same change as #11616 by @pcuenca with the original commit preserved

disclaimer: my julien-cto agent helped me write this

Co-authored-by: Pedro Cuenca <pedro@huggingface.co>
2026-11-04 11:07:48 +01:00
robopipi[bot]
c5d3978350
fix(ai): send session-id instead of session_id on OpenAI Responses (#11793)
The `openai-responses` client sent the session-affinity header as
`session_id`. Proxies such as nginx with `underscores_in_headers off`
silently drop headers with underscores, and Envoy can be configured to
reject them. Codex now sends `session-id`, and the
`openai-codex-responses` path already followed it (#4967).

With the default `openai` session-affinity format, `openai-responses`
now sends `session-id`. `x-client-request-id` and `prompt_cache_key` are
unchanged. The `openai-nosession` and `openrouter` formats behave as
before.

The compat schema description for `sessionAffinityFormat`
(`openai-responses`) and `docs/models-configuration.md` are updated to
match.

Not changed: `openai-completions` still sends `session_id`. I did not
check whether xAI or other backends on the Responses API depend on the
underscored name. Their `prompt_cache_key` is still sent, so prompt
caching should not depend on the header.

Testing:
- `test/openai-responses-compat.test.ts` now captures `session-id`. It
also checks that `session_id` is no longer sent, and the
explicit-override test uses the new name. `test/xai-responses.test.ts`
expects `session-id` and no `session_id`.
- Without the source change, these tests fail (3 failures); with it,
both files pass (50 tests).
- `npm run check` passes.
- The full `packages/ai` suite passes, except
`generate-models-strict.test.ts`. That failure is unrelated: the test
depends on live model catalog data.

Fixes #11639

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 10:28:59 +01:00
robopipi[bot]
37b38e9f65
fix(coding-agent): keep ctrl+v as a paste key on Windows and WSL (#11780)
On Windows and WSL, `app.clipboard.pasteImage` was bound only to
`alt+v`. Most Windows terminals handle Ctrl+V themselves and send a
bracketed paste. Some terminals have no Ctrl+V paste action and pass
Ctrl+V to the program as the raw `0x16` key instead; Tern's embedded
terminal is one. In those terminals nothing in pi was bound to `ctrl+v`,
so pressing it did nothing and showed no message.

This change makes the Windows/WSL default `["alt+v", "ctrl+v"]`. `alt+v`
keeps working as before. In terminals that handle Ctrl+V themselves, pi
never receives the key, so the extra binding has no effect there. No
other default binding uses `ctrl+v`. `docs/keybindings.md` is updated to
match.

Testing:
- Added a test to `test/keybindings.test.ts`. It sets `WSL_DISTRO_NAME`,
re-imports the keybindings module, and checks that the paste defaults
are `["alt+v", "ctrl+v"]`. It fails without the change and passes with
it.
- Updated the existing defaults assertion in the same file to match the
new value.
- `node ../../node_modules/vitest/dist/cli.js --run
test/keybindings.test.ts`: 6 passed.
- `npm run check`: passes.

Fixes #11765

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:33:31 +01:00
Petr Houška
8da141c2ad
fix(coding-agent): take grep context lines from ripgrep instead of reading matched files (#11705)
When `context` is set, the grep tool read every matched file whole and
split it into lines, only to print the lines around each match. Memory
grew with the size of the matched files, not the output. In the issue's
reproduction (8 log files of about 95 MB, one match each, `context: 2`,
`--max-old-space-size=1024`), the process ran out of heap after about 10
s.

ripgrep already supports context. This change passes `--context N` to rg
and formats the `context` events from its JSON output. The tool no
longer opens matched files.

Details:
- After the last allowed match (`limit`), rg still sends that match's
trailing context. The tool keeps those lines, then stops rg. A match
inside that window is shown as a context line, as before.
- Lines past the 50 KB output cap are counted but not stored, since
truncation drops them anyway. Match counts, the match-limit notice, and
`details.matchLimitReached` stay as before.
- rg merges overlapping context, so lines shared by nearby matches are
printed once instead of repeated per match. This is the one intended
output change.
- Context lines are decoded by rg, so UTF-16 files with a BOM show
correctly with context (#11423, closed as no-action). Non-UTF-8 lines
and paths, which rg sends base64-encoded, are decoded too.

Two related grep fixes in the same code:
- **Exact match limit:** with exactly N matches and `limit: N`, grep
said "N matches limit reached. Use limit=2N". It now sets the notice and
`details.matchLimitReached` only after rg sends one more match. Cost:
when no further match exists, rg finishes the search instead of being
stopped at the Nth match, the same as a search with fewer than N
matches.
- **Lone CR:** grep removed every `\r` in a line, so `one\rtwo needle`
showed as `onetwo needle` and text copied from grep failed to match in
edit. It now strips only the line terminator (`\n` or `\r\n`), like the
read tool. The TUI still hides CRs when rendering. This updates the
expectation in the #11398 test from `beforeprogress` to
`before\rprogress`.

Breaking: `GrepOperations.readFile` is removed because nothing uses it
anymore. rg always ran locally, so this hook never made grep work on a
remote backend. Docs now say to override `execute()` for remote search,
as the Gondolin example does. Code that passes `readFile` still runs,
and the value is ignored. Only code that type-checks an object literal
against `GrepOperations` fails to compile.

Known remaining differences and limits:
- Memory no longer grows with file size or match count, but a single
huge matching line (tens of MB) is still buffered whole before being cut
to 500 characters. That was already true for `context: 0` on main.
Bounding it would mean discarding oversized rg JSON records, which is a
separate change.
- In a directory search, rg stops reading a file once it finds a NUL
byte, so context past that point in a text-looking file that turns
binary later is omitted. main reread the file and printed it. Explicit
file paths are unaffected.
- `details.truncation.totalLines`/`totalBytes` count formatted rows.
With deduplicated context, or file names containing newlines, they can
differ from main. No renderer or notice displays them.

**Testing**
- Issue reproduction: before, `FATAL ERROR ... JavaScript heap out of
memory`. After, correct output in about 60 ms with 135 MB RSS.
- `pattern: "line", context: 3, limit: 100000` over the same 770 MB of
logs reports both limit notices and peaks at 154 MB RSS.
- New tests in `test/tools.test.ts`:
- Merged overlapping context, plus trailing context at the limit
including a match inside it. Fails on main.
  - UTF-16 with context. Fails on main.
- Truncation reported when stored output ends exactly at 50 KB. Guards
the counting logic.
- Matches still counted past the byte cap. Passes on main too; it guards
against stopping rg early at the cap.
  - Huge `context` value accepted.
- No limit notice with exactly `limit` matches (with and without
context), notice with more. Fails before this change.
- Lone CR kept and CRLF stripped, with and without context. Fails before
this change.
- All 94 tests in the file pass. `npm run check` passes.

fixes #9276

This PR is AI-generated.
2026-11-03 23:43:40 +01:00
Petr Houška
eebd5d90ca
feat(coding-agent): let Markdown transformers opt into more surfaces (#11756)
Problem: `pi.registerMarkdownTransformer()` only runs on user, assistant
and thinking Markdown. Pi renders other Markdown that extensions cannot
reach: custom messages, skill blocks, branch and compaction summaries,
and the changelog notices (What's New, `/changelog`, Extension Updates,
the Update Available note). For example, a transformer cannot rewrite
text in an extension's changelog shown under "Extension Updates",
because that call site passes no transform.

Running every transformer everywhere would break existing ones. The
documented example only skips thinking blocks and replaces `-->` with
`→`; on a changelog it would turn `<!-- note -->` into `<!-- note →`.

Solution: an optional `{ messageTypes }` lists the surfaces a
transformer runs on. Without options it runs on the same three types as
before.

| `messageType` | Surfaces |
|---|---|
| `user`, `assistant`, `assistant-thinking` | transcript messages
(default) |
| `custom` (new) | custom messages without a message renderer; expanded
skill blocks |
| `summary` (new) | expanded branch and compaction summaries |
| `changelog` (new) | What's New, `/changelog`, Extension Updates, the
Update Available note |

```typescript
pi.registerMarkdownTransformer((markdown) => markdown.replaceAll("⚠️", "⚠"), {
  messageTypes: ["changelog"],
});
```

Details:
- Filtering happens in one place,
`ExtensionRunner.getMarkdownTransformers()`: each transformer is wrapped
so Markdown of unlisted types passes through unchanged. Components still
take a plain list of transformers.
- Skill blocks and summaries pass only their content to transformers; pi
adds its bold header after the transform.
- Custom, skill and summary messages now get `messageId` and `timestamp`
when loaded from the session. Changelog notices have no session entry,
so both are `undefined`.
- The built-in Mermaid transformer renders in user and assistant text
and now also in summaries, which are model-written like assistant text.
It still skips thinking, custom messages and changelogs.
- `/hotkeys` does not run transformers.
- New exports: `MarkdownMessageType`, `MarkdownTransformerOptions`,
`MarkdownTransformerRegistration`.

Tests: runner type filtering (default vs. opted in), custom message,
skill and summary components (type, header excluded, `messageId`),
Mermaid on the new types, and startup notices running `changelog`
transformers on What's New and Extension Updates while default
transformers are skipped.

Related: #11710 (HTML comments in Extension Updates). #11755 already
fixed it in the renderer; this PR lets extensions transform those
notices in general.
2026-11-03 23:10:47 +01:00
Petr Houška
ace560c302
fix(coding-agent): let a started RPC or print shutdown finish; only a repeated signal forces exit (#11752)
Follow-up to #11746 for the case @fcatuhe reported on #11599: a second
shutdown trigger cut off cleanup.

Problem: Paseo stops pi by closing stdin and sending SIGTERM in the same
tick (SIGKILL 2 s later); systemd sends SIGTERM and stdin closes after.
In RPC mode:

- stdin EOF, then SIGTERM: `shutdown()` removed pi's SIGTERM handler
when it started, so the signal killed pi (exit 143) mid-`dispose()`.
- SIGTERM, then stdin EOF: the second `shutdown()` call ran
`process.exit()`.

A pending MCP token refresh was lost: the server had rotated the refresh
token, the next pi sent the old one, and the server revoked the grant.

Rule after this change: once a shutdown has started, a stdin EOF or a
single signal does not interrupt it; a second signal exits at once.
SIGKILL still always works.

- **RPC mode** (`rpc-mode.ts`): signal handlers stay registered until
exit; `shutdown()` runs once and later calls do nothing; the signal
handler counts signals and exits only on a repeated one.
- **Print mode** (`print-mode.ts`): the final `finally` removed the
signal handlers before `disposeRuntime()`, so a signal during the final
disposal killed pi. Handlers are now removed after it,
`disposeRuntime()` returns the shared in-flight disposal (a signal then
waits for it), and a repeated signal exits at once, as before.
- **Interactive mode**: unchanged. It already lets a started shutdown
finish.

Docs (`docs/rpc.md`) and the CHANGELOG are updated.

## Testing
- `test/rpc-prompt-response-semantics.test.ts`: EOF then SIGTERM,
SIGTERM then EOF (both wait for a slow dispose, exit once with 0 and
143), and EOF then two SIGTERMs (exits at once with 143). All three fail
on main.
- `test/print-mode.test.ts`: SIGTERM during the final disposal waits for
it, a second SIGTERM exits with 143, handlers are removed afterwards.
Fails on main.
- Real process, `pi --mode rpc` with a `session_shutdown` handler that
takes 1 s and real signals:
  - EOF then SIGTERM: cleanup done, exit 0 (main: no cleanup, exit 143)
  - SIGTERM then EOF: cleanup done, exit 143
  - EOF then two SIGTERMs: exit 143 at once
- `npm run check` passes; all `test/rpc*` and `test/print-mode` tests
pass.
2026-11-03 22:21:44 +01:00
robopipi[bot]
1708b47889
fix(coding-agent): process idle ctx.shutdown() requests in RPC mode (#11746)
In RPC mode, `shutdownHandler` only set a `shutdownRequested` flag. That
flag was checked in two places: after a stdin command was handled and on
`agent_settled`. If an extension called `ctx.shutdown()` while the
session was idle (for example from a timer), neither check ever ran. The
process stayed alive until the client sent another command. Interactive
mode already shuts down right away in this case.

Changes in `packages/coding-agent/src/modes/rpc/rpc-mode.ts`:
- **One check for all paths:** `checkShutdownRequested()` shuts down
only when a request is pending, input is attached, no command is being
handled, and the session is idle. Each of those ending calls it again:
command completion, prompt settle, `agent_settled`, and input attach.
- **Idle requests:** the shutdown handler schedules the check on the
next tick, so the requesting handler finishes first. The conditions are
checked when the tick runs, not when it is scheduled: a stdin command
that arrives in between is handled first, and shutdown follows when it
finishes.
- **Requests during startup:** a request made before input is attached
(for example a synchronous `ctx.shutdown()` in `session_start`) is
checked once the input reader is attached.
- **Requests during a command:** shutdown waits until the command
finishes, including extension commands. A `prompt` command returns
before `session.prompt()` settles, so it stays counted as in flight
until the prompt settles, and the shutdown check runs then. This also
fixes an existing ordering problem: an extension command that called
`ctx.shutdown()` could exit the process before its `prompt` response was
written.
- **Repeated requests:** `checkShutdownRequested()` does nothing once
shutdown has started, so a second `ctx.shutdown()` does not call
`process.exit` in the middle of async cleanup. A second stdin EOF or
signal still forces exit, as before.

Shutdown requested during a running turn is still handled on
`agent_settled`, as before. A request made while an extension-started
compaction runs (not idle, no run to settle) still waits for the next
command or run; `docs/rpc.md` and `docs/extensions.md` note this.

Docs (`docs/rpc.md`, `docs/extensions.md`) and the CHANGELOG are
updated.

## Testing
- Added three tests to `test/rpc-prompt-response-semantics.test.ts`:
  - an async `ctx.shutdown()` on an idle session exits;
- an extension command that calls `ctx.shutdown()` gets its `prompt`
response written before exit;
- a command that arrives after an idle request but before the scheduled
check runs gets its response written before exit.

  All three fail without the change and pass with it.
- `npm run check` passes. All `test/rpc*` tests pass.
- In `./test.sh`, all coding-agent tests pass. The only failures were
timeouts in durable/harness tests (`store-storage`, `tools-images`,
connection close), which this change does not touch.
- Reproduction from the issue: run `pi --mode rpc --no-session -ne -e
./shutdown-probe.mjs` with stdin held open and nothing sent.
- Before the change, the process stayed alive until a timeout killed it
(exit 124).
- After the change, `session_shutdown` fires and the process exits with
code 0.
- A synchronous `ctx.shutdown()` in `session_start` also exits with code
0.

Fixes #11599

Requested by @petrroll

---------

Co-authored-by: triagent <triagent@localhost>
Co-authored-by: Petr Houška <houskape@gmail.com>
2026-11-03 21:27:14 +01:00
David Brailovsky
a758f3ae4c
Anthropic message threads (#11545)
Anthropic can keep a conversation on the server (message threads beta).
A request can then continue from the previous reply and send only the
new messages, instead of the whole history every turn.

pi uses this when a session id is set and caching is on. It still builds
the full request, compares it with the previous one, and only sends the
new part if nothing earlier changed.

- Default: on only for the anthropic provider on api.anthropic.com.
  Gateways that drop unknown fields would lose the conversation.
  Override with compat.supportsMessageThreads.
- If the server rejects a continuation, pi resends the full request
  as a new thread. Other errors turn threads off for the session.
- New option StreamOptions.disableContinuation. The cache warmer sets
  it so warm requests don't replace the session's thread.
- cleanupSessionResources(sessionId) frees a session's thread state.
2026-11-03 14:07:57 +01:00
m3
0ec20e157b
feat(ai): add Microsoft-Decision-1 classifier to GitHub Copilot (#11679)
Closes #11678

Changes:
- `catalog/providers/github-copilot.ts`: add `microsoft-decision-1` as a
`typesafe-system-one` classifier (text input, 32768 context, $0.042/M
input, Copilot's static headers).
- `src/providers/github-copilot.ts`: register the classifier and wrap
`typesafeSystemOneApi()` to add `/v1` to the base URL, since Copilot
serves `/v1/systemone` and the OAuth login replaces the base URL with
the account endpoint.
- README and `docs/models.md`: add the model to the classifier tables.

Tests:
- `test/classifier-models.test.ts`: with an OAuth credential, the
request goes to `https://api.enterprise.githubcopilot.com/v1/systemone`
with the Copilot headers, and the model stays available although
`availableModelIds` lists only chat models. With an API key, it goes to
`https://api.individual.githubcopilot.com/v1/systemone`. Both tests fail
without the wrapper.
- Ran against `api.enterprise.githubcopilot.com` with a Copilot login:
bool, choice, and score questions return answers and usage.

Co-authored-by: Hongwei Ma <11957602+Marvae@users.noreply.github.com>
2026-11-02 16:12:43 +01:00
Mario Zechner
6536f80129 feat(ai,agent,coding-agent): validate tool arguments with pi-schema, drop TypeBox
pi-schema: opt-in coercion, compile(schema, { coerce: true }) with coerce() and
parse(), the superset of TypeBox's Value.Convert rules and pi-ai's old coercion;
unresolvedRefs option; url and json-pointer-uri-fragment formats.
pi-ai: Tool takes any JSON Schema; validation through pi-schema; Type, Static
and TSchema are no longer exported (breaking). pi-agent-core and pi-durable:
tools on pi-schema, no TypeBox dependency. Coding agent: extensions keep Type,
Static, TSchema and StringEnum through an extension view of pi-ai; built-in
tools stay on TypeBox.
2026-11-01 10:08:42 +00:00
Petr Houška
70ca8e8960
docs(coding-agent): list external limitations as known issues with remedies (#11647)
Restructures external limitations in the tmux and Windows guides as
"Known issues" with a remedy for each entry.

- `tmux.md`: the tmux 3.6 color reply section becomes a Known issues
table (issue, affected versions, remedy).
- `windows.md`: the conhost problem table moves to a Known issues
section at the end of the page, with a Remedy column instead of the
cause column. The terminal table links to it. Issue links are reduced to
one per row.

Known issues are limited to things outside Pi's control or deliberate
limitations. Bugs Pi can fix stay in the issue tracker.
2026-10-31 17:55:58 +01:00
Armin Ronacher
b4c2e92acb fix: remove vulnerable dependency chains
Replace build-time shell utilities with Node filesystem operations and retire the native sandbox example in favor of existing isolation options. Upgrade glob matching and worker-test dependencies and refresh the lockfiles.
2026-10-31 17:54:29 +01:00
Petr Houška
6de14ec098
docs(coding-agent): note tmux 3.6 color reply leak in tmux guide (#11645)
Adds a troubleshooting section to the tmux guide
(`packages/coding-agent/docs/tmux.md`, published at
https://pi.dev/docs/latest/tmux) for the color-code text that tmux 3.6
types into the editor at startup.

tmux 3.6 forwards Pi's palette queries to the outer terminal. When a
reply reaches tmux split across reads, tmux treats its bytes as key
presses and sends them to Pi as typed text. tmux 3.7 fixes this
([tmux#4749](https://github.com/tmux/tmux/issues/4749),
[tmux#4793](https://github.com/tmux/tmux/issues/4793)). Pi cannot filter
the text because it arrives exactly like typing.

The section also notes that switching to a fixed theme does not help:
`applyFromSettings()` queries terminal colors regardless of the theme,
which reporters confirmed in #10679 (`--use-theme dark`) and #10256
(`"theme": "dark"`).

Related: #10250, #10679, #10256.
2026-10-31 17:29:36 +01:00
Petr Houška
25426f72ae
docs(coding-agent): document Windows terminal and conhost limitations (#11644)
Adds a "Choose a terminal" section to
`packages/coding-agent/docs/windows.md`
(https://pi.dev/docs/latest/windows), with two tables (last updated
October 2026):

- Terminals and which conhost they use: Windows Terminal (recommended),
VS Code, WezTerm nightly (bundled, up to date); WezTerm 20240203
(bundled, old); Alacritty (built-in unless `conpty.dll` +
`OpenConsole.exe` are provided); classic console window, mintty,
ConEmu/Cmder, Windows OpenSSH (built-in).
- Known problems by conhost type:
- Built-in: crash when switching fullscreen -> regular or using `Ctrl+G`
from fullscreen (#9186, microsoft/terminal#17515).
- Built-in behind another terminal (mintty, OpenSSH): query replies leak
into the prompt, BEL opens the external editor (#10256, #10362, #10742).
- Old bundled (WezTerm 20240203): reply fragments in the prompt (#10717,
#10970).
- Built-in, classic console: reported regular-mode redraw issue (#6300).
- Notes that the fixes exist upstream and reach the built-in conhost
with a future Windows update, timing unknown.

Sources: VS Code defaults `terminal.integrated.windowsUseConptyDll` to
`true` (bundled conpty v1.25). Alacritty loads `conpty.dll` only if
present (`alacritty_terminal/src/tty/windows/conpty.rs`) and its
installer ships only `alacritty.exe`.

The crash was verified locally in the classic console window on conhost
10.0.26100.9807: fullscreen -> regular crashed 3/3 runs and `Ctrl+G`
from fullscreen crashed 2/2 runs (`0xC0000005`).
2026-10-31 16:15:42 +01:00
Petr Houška
9376c8d17c
fix(coding-agent): pass host keybinding hints to extension renderers (#11630)
Fixes #4748.

## Problem

Extensions that import `keyText`/`keyHint` from
`@earendil-works/pi-coding-agent` can resolve their own copy of the
package (their own `node_modules`, or the bundled `cli.js` vs
`dist/index.js`). That copy's keybindings are never set by the host, so
it falls back to a manager with only `tui.*` bindings, and
`keyHint("app.tools.expand", "to expand")` renders as ` to expand`.

## Change

The host passes its own `keyText` and `keyHint` to extension renderers,
the same way it passes `theme`:

- `ToolRenderContext` (`renderCall`/`renderResult` context):
`context.keyHint(...)`
- `MessageRenderOptions` and `EntryRenderOptions`:
`options.keyHint(...)`

All three extend a new exported `KeybindingHints` interface. The docs
and the `debug-provider` example now use the injected functions and warn
against importing them in renderers.

Not done:

- Moving the `pi-tui` keybindings singleton to `globalThis`. Per the
discussion in #7011, extension-facing state should be host-owned and
passed through the extension API instead of shared through process-wide
module state.
- `ctx.ui.keyText`/`keyHint`. No reported case needs them outside
renderers, RPC and print modes never install app keybindings, and adding
them to `ExtensionUIContext` would break SDK hosts that implement it.

## Tests

- `test/extension-keybinding-hints.test.ts`: tool, message, and entry
renderers format a user-overridden `app.tools.expand` binding.
- `npm run check` passes.
2026-10-30 23:01:20 +01:00
Petr Houška
8bc94b19ff
feat(coding-agent): add PI_SELF_UPDATE_COMMAND and selfUpdateCommand for pi update (#11629)
Refs #7756, #5607.

Problem: when a wrapper or another package manager provides Pi (mise, a
`PNPM_HOME` wrapper such as nub, Nix), `pi update` cannot update it and
prints "pi cannot self-update this installation." Users with several
machines have to remember which tool updates Pi on which machine.

Example:

```sh
export PI_SELF_UPDATE_COMMAND="mise use -g npm:{packageName}@{version}"
pi update
# Updating pi with mise use -g npm:@earendil-works/pi-coding-agent@1.4.0...
# Updated pi from 1.3.0 to 1.4.0
```

or in `~/.pi/agent/settings.json`:

```json
{ "selfUpdateCommand": ["nix", "profile", "upgrade", "pi"] }
```

Behavior:

- `pi update` still checks pi.dev first. The command runs only when a
newer release exists, or with `--force`, and the release note prints
first.
- Placeholders come from that release: `{packageName}`, `{version}`,
`{installSpec}` (`<packageName>@<version>`). A package rename announced
by pi.dev flows into them.
- `PI_SELF_UPDATE_COMMAND` takes precedence and is split on whitespace
(no quoting). The setting is argv, for arguments with spaces, and is
read only from global settings, so a project's `.pi/settings.json`
cannot inject a command.
- The configured command replaces Pi's own update logic for every
install method, including installer-managed installs: it is explicit
user intent.
- Exit code 0 does not prove an update. Pi reports "Updated pi from X to
Y" when its own `package.json` now shows Y (in-place updates) or when
`pi --version` from PATH reports Y (wrappers such as mise or Nix that
install each version into a new directory). Otherwise it warns: ``Ran
<cmd>, but `pi --version` reports X, not Y.``
- "cannot self-update this installation" now mentions the env var and
setting.

Docs: `docs/cli.md` (new "Custom update command" section),
`docs/settings.md`, `docs/environment-variables.md`, `--help` env list,
regenerated `schemas/settings.schema.json`.

Tests: unit tests for argv resolution and placeholders in
`test/config.test.ts`; CLI tests in `test/package-command-paths.test.ts`
for env precedence over the setting and the managed install, global-only
setting gated by the version check, and command failure. `npm run check`
passes.

Manual end-to-end check with real mise 2026.10.23 in a sandboxed HOME,
running this branch as the mise-installed 1.3.0 (`PI_PACKAGE_DIR`
pointing at mise's install):

| Scenario | Result |
|---|---|
| `mise use -g npm:{packageName}@{version}` | mise installs 1.4.0;
`Updated pi from 1.3.0 to 1.4.0` |
| Already 1.4.0 | `pi is already up to date (v1.4.0)`, mise not run |
| In-place `npm install -g --prefix <dir> {installSpec}` via the setting
| `Updated pi from 1.3.0 to 1.4.0` |
| Command `true` (exit 0, installs nothing) | ``Ran true, but `pi
--version` reports 1.3.0, not 1.4.0.`` |

This PR is AI-generated.
2026-10-30 22:30:39 +01:00
Christian Klotz
2a46c42080
docs(coding-agent): document which working directory resumed sessions use (#11617)
Closes #11532.

The docs and CLI help did not say which working directory a resumed
session runs in. That directory controls tools, `@path` references,
project configuration, and context files.

## Changes

- `docs/sessions.md`: states that a session keeps the working directory
it was created in, adds a table mapping each entry point to its working
directory, and describes what happens when that directory no longer
exists. Removes a paragraph that repeated the `--session`/`--fork`
descriptions.
- `docs/cli.md` and `--help`: clearer `--continue`, `--resume`,
`--session`, `--session-id`, and `--fork` descriptions. The help only
mentions the directory where it is not obvious (`--fork`).
- `/resume` and `/import` command descriptions, mirrored in
`docs/slash-commands.md`. `/import` notes that it preserves the session
ID and working directory.

| Entry point | Working directory |
|---|---|
| `--continue` | The current directory |
| `--resume`, `/resume`, `--session`, `/import` | The session's working
directory, which can differ from the current directory |
| `--fork`, or a `--session` ID from another directory that you choose
to fork | The current directory, in a new session |

Behavior was verified against `createSessionManager` in `src/main.ts`,
`SessionManager.open`/`continueRecent`/`forkFrom`, and
`AgentSessionRuntime.importFromJsonl`.
2026-10-30 17:22:05 +00:00
Armin Ronacher
be15fe3c17 fix(coding-agent): accept -l in pi mcp login and logout
Report unknown options as typed and suggest a configured server for a
mistyped name.
2026-10-30 14:15:25 +01:00
Armin Ronacher
908aa7b74b feat(coding-agent): prefer the client ID metadata document for MCP OAuth
oauth.clientRegistration defaults to auto: pi's Client ID Metadata Document
when the authorization server supports it for public clients, otherwise
dynamic client registration. Registered clients stay in use. pi mcp add
gains --oauth-client-registration.

closes #11426
2026-10-30 10:36:22 +01:00
Petr Houška
bec1e0f5a4 docs(coding-agent): document how -e packages are cached and refreshed 2026-10-29 21:43:11 +01:00
Petr Houška
86dc686cf2 fix(tui): treat ESC+Return with native Shift held as Shift+Enter
Some Apple Terminal setups send ESC+CR for Shift+Enter, which parsed as
alt+enter and queued a follow-up. The native Shift fallback now covers
ESC+CR as well as plain CR.

fixes #11559
2026-10-29 20:17:54 +01:00
Christian Klotz
c466a35957
fix(coding-agent): cap pi.exec output at 256 MiB (#11546)
Closes #11360.

## Summary
- Cap combined stdout/stderr with `ExecOptions.maxBuffer`, defaulting to
the smaller of 256 MiB and the runtime string limit. Invalid explicit
limits reject before spawning.
- Terminate oversized commands and return `truncated: true`, `killed:
true`, and a nonzero exit code. Close output pipes so writing
descendants cannot keep the call pending, and discard incomplete UTF-8
suffixes rather than adding replacement characters.
- Fix SIGKILL escalation for children that ignore SIGTERM, and simplify
completion to `await` with one cleanup block and result.
- Document the API and add regressions for byte limits, shell
descendants, UTF-8 chunk boundaries, and 32-bit runtime limits.

`ExecResult.truncated` is required. Existing extensions reading
execution results continue working; typed mocks or wrappers constructing
results must add the field.

## Verification
- `npm run build` and the post-rebase `npm run build:offline`
- `npm run check`
- 39 focused execution and extension tests
- `./test.sh` passes after building the env daemon. Earlier runs hit
remote-cleanup and reftable-watcher timeouts; both passed in focused
reruns and the final full run.
2026-10-29 00:13:41 +02:00
Petr Houška
3bae85274a fix(tui,coding-agent): handle selection page keys in all scrolling lists
tui.select.pageUp/pageDown were configurable but ignored by SelectList,
SettingsList, editor autocomplete, and most coding-agent selectors. In
fullscreen mode, the default PageUp/PageDown also scrolled the transcript
instead of reaching the focused list.

- add getSelectPageIndex() and handle page keys in SelectList, SettingsList,
  editor autocomplete, and the model, scoped models, auth, user message,
  settings submenu, llama search, and /mcp selectors
- add Component.capturesViewportInput: a focused component that sets it
  receives fullscreen viewport keys instead of the transcript, as a focused
  overlay already does
- let page keys move an open autocomplete menu before app actions, so a
  remapped Ctrl+V pages the menu instead of pasting an image

closes #7629

Co-authored-by: muyiyr <940955635@qq.com>
2026-10-28 19:05:21 +01:00
Cristina Poncela Cubeiro
b4b72a3510
feat(coding-agent): extensions changelog (#8790)
Addresses #5958.

Adds `changelogPath` to `PiManifest`. Safe package-relative changelog
resolution, with `CHANGELOG.md` fallback, and carries it through
`PathMetadata`/`SourceInfo`.

Added extension changelog handling like Pi’s own changelog.

If the package exposes `pi.changelogPath` or has `CHANGELOG.md`, Pi
stores the last seen package version in settings. On each fresh
interactive launch, we compare the stored version to the installed
package version. Then parses the changelog entries in between, shows an
Extension Updates section, then marks the current version as seen. The
first time an extension is seen, its version is recorded without showing
a changelog, same as Pi's own on a fresh install. `collapseChangelog`
condenses this to one line per package.

Any update that bumps the extension's version shows this on the next
launch, e.g.:
```bash
 pi update --extensions
 pi update --all
 pi update <extension-source>
 pi update --extension <extension-source>
 pi update --self --extensions
 pi update pi --extensions
 pi update self --extensions
```

<img width="798" height="626" alt="Screenshot 2026-08-28 at 16 34 15"
src="https://github.com/user-attachments/assets/c882f01c-94b9-4857-b9b7-243d88c0d437"
/>
2026-10-28 11:25:04 +01:00
Petr Houška
e509d7edc7
docs(quickstart): add Windows installer tab (#11231)
The Quickstart only showed the macOS/Linux `curl` installer, even though
`install.ps1` installs Pi the same way: a managed install pinned by the
published lockfile, updated with `pi update`. This PR adds the Windows
installer as a second tab next to the existing one. All other sections
stay the same.

````markdown
Install Pi with the installer on macOS, Linux, or Windows:

```bash tab="macOS / Linux"
curl -fsSL https://pi.dev/install.sh | sh
```

```powershell tab="Windows"
powershell -c "irm https://pi.dev/install.ps1 | iex"
```

Both installers pin all dependencies and update Pi with `pi update`. ...
````

The tabs are labelled by platform rather than by shell: newcomers know
their OS, and the command also works when pasted into cmd.

Tab rendering on pi.dev comes from earendil-works/pi.dev#183. This PR
can merge before or after it. Until the renderer ships, and on GitHub,
the page shows two ordinary code blocks, and the prose still reads
correctly.

## Screenshots (with earendil-works/pi.dev#183)

| | Default | Windows tab selected |
|---|---|---|
| Dark | ![dark
default](https://gist.githubusercontent.com/petrroll/27308d9d8522c90ea5b290b0c3ba9862/raw/cad05d1cf643903955af7282763e22ff2a101e73/dark-unix.png)
| ![dark
windows](https://gist.githubusercontent.com/petrroll/27308d9d8522c90ea5b290b0c3ba9862/raw/cad05d1cf643903955af7282763e22ff2a101e73/dark-windows-tab.png)
|
| Light | ![light
default](https://gist.githubusercontent.com/petrroll/27308d9d8522c90ea5b290b0c3ba9862/raw/cad05d1cf643903955af7282763e22ff2a101e73/light-unix.png)
| ![light
windows](https://gist.githubusercontent.com/petrroll/27308d9d8522c90ea5b290b0c3ba9862/raw/cad05d1cf643903955af7282763e22ff2a101e73/light-windows-tab.png)
|
2026-10-28 10:10:44 +01:00
Christian Klotz
85c2ee3e76
docs(coding-agent): restore the full extensions guide, corrected against current source (#11498)
## Summary

The docs refresh (#9898) cut `docs/extensions.md` from about 3,100 lines
to 130, dropping API reference and tool guidance. #11039 restored part
of it. This PR restores the full guide and corrects it against current
source.

- **Structure:** the original sections, with the ASCII lifecycle diagram
replaced by three Mermaid diagrams (run, turn, session) and ordered hook
lists, plus a handler composition table.
- **Merged:** content added after the cut: tool exposure, calling other
tools, structured output, MCP servers, `registerToolRenderer`, virtual
models, `provider_stream_event`, `before_agent_start` `messageType`.
- **Corrected:** removed or renamed APIs (`modelRegistry.stream()`,
prompt sections are `<tools>`/`<rules>`, `builtin:read` paths,
`highlightCode`), wrong behavior claims (errors only by throwing,
`model_select` on restore, `sendUserMessage` always triggering a turn,
override warning, `truncateLine` bytes), examples that did not
type-check, links to removed `tui.md` patterns and `rpc.md` anchors, and
wrong API lists in the examples table.
- **Kept:** anchors that other docs and changelogs link to, as `<a id>`
tags.
- **Fixed example:** `reload-runtime.ts` tool queued `/reload-runtime`
with `deliverAs: "followUp"`. Queued text reaches the model as a plain
user message, so the command never ran and Pi never reloaded. The tool
now sets a flag and returns `terminate: true`; an `agent_settled`
handler sends `/reload-runtime` with `expandPromptTemplates: true`,
which runs after the settled handlers finish, while the agent is idle.

In the long run it may be better to revisit how API reference is,
provided to the agent, especially since more capable models do explore
the source without it.

## Validation

- `npm run check`
- Edited TypeScript examples type-check against current types
- All relative links and anchors in `extensions.md` resolve, including
inbound anchors from other docs
- `reload-runtime.ts`: faux-provider harness test (temporary, not
committed) shows one reload after the run settles and no extra model
request; the old version never reloads. Also verified in interactive
mode with a real model.
- Reviewed by GPT-6 Astra and GPT-5.6 Sol against source; all findings
applied
2026-10-28 00:31:24 +02:00
Julien Chaumond
59ca40cf58
docs(coding-agent): update llama.cpp setup for llama.app (#10179)
update the llama.cpp docs to use the llama.app installer and the new
`llama serve` command

commit by @hanouticelina

disclaimer: my julien-cto agent helped me write this

---------

Co-authored-by: Celina Hanouti <hanouticelina@gmail.com>
Co-authored-by: Mario Zechner <contact@badlogicgames.com>
2026-10-27 16:21:37 +01:00
robopipi[bot]
c1810bf70f
feat(coding-agent): let inline ctx.ui.custom() components report blocked program status (#11471)
Inline `ctx.ui.custom()` components never report pi as blocked through
OSC 7501 program status. This is on purpose, because the same API hosts
loaders. As a result, a custom dialog that waits for the user (an
approval prompt, the `question.ts` example) shows up as `working`, and
the terminal cannot tell that pi needs input.

The extension dialog stack already stores a blocked status for each
dialog and reports the visible one. This change adds an optional
`blocked` option to `ctx.ui.custom()`:

```ts
ctx.ui.custom(factory, { blocked: { kind: "question", message: "Pick a color" } });
```

The inline branch of `showExtensionCustom()` passes it to
`pushExtensionDialog()`. Components without the option behave as before.
Overlays ignore the option because they are not part of the dialog
stack. `docs/tui.md` documents the option.

Testing:
- Added a test to `test/interactive-mode-extension-dialogs.test.ts`:
  - A custom component that opts in reports `blocked`.
- A plain custom component stacked on top of it clears the blocked
status.
  - Closing the plain component restores the blocked status.
  - Closing the opted-in component clears it.
- The test fails without the change to `interactive-mode.ts` and passes
with it.
- `npm run check`: biome and lint pass, and there are no type errors in
coding-agent. The only errors are existing type errors in
`packages/ai/test` from locally generated model data. They are not
related to this change.

Fixes #11428

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:27:28 +01:00
Christian Klotz
ede2c9b468
docs: fix provider checklist, MCP anchors, and missing commands/settings (#11338)
Fix docs and `--help` drift: update the provider checklist to the
`src/api` layout, fix broken MCP anchors, and document `/mcp`, `pi mcp
add|remove`, `PI_CLEAR_ON_SHRINK`, and `"disabled"` timeouts.

closes #11243
2026-10-23 17:02:41 +02:00
Lincoln
6dfb546785
fix(coding-agent): start when a configured package fails to install (#11304)
fix(coding-agent): start when a configured package fails to install

A package in settings.json that could not be installed (registry
unreachable, package missing, failing git prepare step) crashed startup
with an uncaught rejection and a raw Node stack.

Resolving packages now catches the install failure, skips that package
the same way offline mode does, and reports it as an extension warning:

  Warning: Extension package "npm:foo": failed to install (...).
  Run "pi remove npm:foo" to remove it from settings.json.

Project packages point at `pi remove <source> --local`. `-e` sources
say to check the source instead. Pi tries the install again on the next
start or /reload.

--help and --list-models now print runtime diagnostics, so the warning
shows there too. pi config prints it as well. pi install and
pi update --extensions still fail with a non-zero exit.

closes #11088
2026-10-23 12:27:45 +02:00
Vegard Stikbakke
14c8118412
feat(ai): carry responseModel on pi-messages terminal events (#11265)
This commit makes it so we store the responseModel (i.e. the "phyiscal"
model used) on each PiMessage. This allows e.g. Radius to know which
physical model was used by virtual models (like `cheap`). It's
especially important after we merged
https://github.com/earendil-works/pi/pull/11000
(9113094291), where Pi will drop thinking
traces when the model name doesn't match - with this change we can check
the physical model instead.
2026-10-23 10:54:18 +02:00
Petr Houška
90d5cf7846 docs(coding-agent): shorten device-code login guidance 2026-10-22 21:38:58 +02:00
Petr Houška
7151099d02 feat(coding-agent): confirm device-code browser and clipboard actions
Knowing how annoying re-logins in corporate environments can be, I think
we can open the browser and copy the code to the clipboard (what the user
asks for) without forcing it upon users (which I assume was the reason
it was removed in 8f0e9251b9).

This change adds optional browser opening and code copying during
device-code login after an additional confirmation (usually Enter).
It should work for all providers, with an optional opt-out controlled
by the provider author, not user settings.

Fixes #9282.
2026-10-22 21:38:58 +02:00
Armin Ronacher
7f225f25d8 feat(ai,coding-agent): image input for System One classifiers
Jev (TypeSafe, Workers AI), Clef (Workers AI), and OpenRouter decision
models now accept ClassifierContext.images. Each service takes its own
wire form: TypeSafe assets with the opt-in header, Clef images data URLs,
or OpenRouter image_url parts in the state. A new SystemOneCompat
(imageInput, sendImageOptInHeader) selects the form; catalog entries set
it explicitly and models.json provider compat now applies to classifiers.

Adds Clef Omni and Jev 1.14 on Workers AI and fixes Clef Flash and
Workers AI Jev catalog pricing.
2026-10-21 23:53:47 +02:00
Mario Zechner
885a564b3b fix(coding-agent): queue codemode scripts at the worker limit
Scripts wait in arrival order for a free worker instead of failing
immediately. The wait counts toward timeout_ms, honours abort, and is
capped at 60 seconds. PI_CODEMODE_MAX_WORKERS raises the process-wide
limit of two workers for hosts running many sessions in one process.

closes #11184
2026-10-20 20:50:27 +02:00
Christian Klotz
1d01b3b1f8
Break single Mermaid diagram into two 2026-10-20 11:41:53 +03:00
Petr Houška
61060ddf8c fix(coding-agent): resolve Windows shells from installation directories
Use installation environment variables for PowerShell discovery and legacy WSL launcher detection.
2026-10-19 21:55:12 +02:00
Christian Klotz
0b102787cd
docs(coding-agent): restore extension guide details (#11039) 2026-10-18 00:52:04 +01:00
Christian Klotz
8efd314fa3
docs(ai,coding-agent): generate models.json reference from schemas (#11038)
* docs(ai,coding-agent): generate models.json reference from schemas

Add npm run generate:references, which renders docs/models-configuration.md
from the models.json TypeBox schemas, and describe provider and model fields
in those schemas. Overrides get their own descriptions without defaults, and
compat sections cover the Responses API aliases.

closes #10790

* Modify changelog

* docs(ai,coding-agent): use shared thinkingLevelMap and promptCache schemas

Replace the per-use schema helpers with fixed schemas whose descriptions
hold for both models and modelOverrides entries.
2026-10-16 21:53:00 +01:00
David Brailovsky
6fcb48670f
fix(coding-agent): remove fixed output limits from summary requests (#11036)
Compaction summaries were limited to 0.8 * compaction.reserveTokens (13107 by default), turn-prefix summaries to 0.5 * reserveTokens, and branch summaries to a hardcoded 4096. On providers where reasoning shares the output limit (OpenAI Responses, adaptive Anthropic), high thinking levels used most of that limit and compaction failed with "generation hit the token cap", even though the model allows far more output.

Tying the limit to reserveTokens assumed the summary request is as large as the full context, so its output had to fit in the reserved space. It is usually much smaller: recent messages kept by keepRecentTokens are not sent, a split turn goes to a separate request, tool results are truncated, and tool definitions and the normal system prompt are not included. Manual compaction can also run long before the context is full. The provider layer already sizes output from the actual request, using the model's output limit reduced to fit the remaining context, as for normal turns.

Summary requests now set no maxTokens and rely on that. compaction.reserveTokens only controls when compaction starts.

generateSummary() and generateSummaryWithUsage() now ignore the reserveTokens parameter.

closes #9512, closes #9075, closes #10908
2026-10-16 13:53:16 +02:00
Christian Klotz
d7f7e0dfee
fix(coding-agent): resolve model references through one provider selection policy (#10913)
* fix(coding-agent): resolve model references through one provider selection policy

Model references from --model, --models/enabledModels, /model, and auth credential printing now share one resolver. It collects every candidate before choosing a provider, so catalog order never decides, a :<thinking> suffix cannot change the provider, and an ambiguous exact match no longer falls through to a fuzzy match.

provider/id still prefers the named provider's exact or fuzzy match. A gateway exposing provider/id as a raw model id is used when the named provider has no match, or lacks credentials while the gateway has them. Several gateways without a unique authenticated one are reported as ambiguous.

fixes #10160

* docs(coding-agent): shorten model selection notes in CLI reference

* docs(coding-agent): condense #10160 changelog entry

* fix(coding-agent): move model reference resolution into model-resolver

* fix(coding-agent): warn when a model scope pattern matches several providers

* docs(coding-agent): trim model resolver comments to the reasons

* fix(coding-agent): build custom model ids from the resolver's not-found target
2026-10-14 11:00:21 +01:00
Armin Ronacher
3daa20280f fix(coding-agent): keep codemode cancellation prompt and bounded
Refs #10748
2026-10-13 18:17:31 +02:00
Christian Klotz
b9ab2a6238
docs(coding-agent): restructure NVIDIA RTX llama.cpp prerequisites 2026-10-13 15:04:38 +01:00
Vegard Stikbakke
cea257439d feat(coding-agent): configure session share destination
closes #10870
2026-10-13 15:41:13 +02:00
Christian Klotz
746d995b88
docs(coding-agent): add NVIDIA RTX llama.cpp setup 2026-10-13 14:15:52 +01:00
David Brailovsky
3e477eab5f feat(coding-agent): add PI_CLIPBOARD_OSC52 to force OSC 52 copies
Sends copied text to the terminal with OSC 52 after the local clipboard
write, as over SSH, for remote clients without SSH variables such as
multiplexer attaches and remote IDE daemons.

closes #10826, closes #10835, closes #10114
2026-10-12 18:07:27 +02:00
Armin Ronacher
b6d505cd1f fix(codemode): reject // @options: lines that are not the first line
A misplaced options line was ignored, so its timeout_ms never applied and
the script ran without a deadline. parseCodemodeSource now throws
CodemodeSourceError before the script runs.

fixes #10631
2026-10-12 17:55:33 +02:00
David Brailovsky
441f5767c4 fix(coding-agent): warn and exit non-zero in print mode on truncated response
Print mode now writes a warning to stderr and exits 1 when the final
response stopped at the model's output token limit. The partial text
is still written to stdout.

fixes #10799
2026-10-12 16:06:42 +02:00