Commit graph

4091 commits

Author SHA1 Message Date
Petr Houška
b5fc6dc84f fix(coding-agent): strip private-parameter CSI sequences in stripAnsi
stripAnsi treated <, = and > after ESC [ as final bytes, so ESC[<35;59;1M
(SGR mouse report) left 35;59;1M in tool output. Accept one private prefix
byte only directly after a CSI introducer (ESC [ or 0x9B). After a bare ESC
these bytes stay final bytes, so ESC= and ESC> (keypad modes) keep the
digits of the text that follows.

closes #11377
2026-10-28 20:14:09 +01: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
robopipi[bot]
b9e3eef6c2
fix(coding-agent): keep active tool order when the same tools are set again (#11514)
The system prompt lists tool snippets and per-tool guidelines in the
order of the active tools. When an extension called
`pi.setActiveTools()` with the same tools in a different order (for
example `[...pi.getActiveTools()].reverse()`), the "tools" and "rules"
sections came out different. pi then recorded a "System Prompt Update:
tools, rules" message even though the active tools had not changed. On
providers without mid-conversation system messages, this also rewrites
the prompt head and loses the cache.

`_applyToolLoadout` in `agent-session.ts` now treats the requested names
as a set:
- tools that stay active keep their current order;
- newly activated tools are appended at the end.

Reordering the same set therefore leaves the prompt unchanged. Adding a
tool only appends its bullet. The same rule covers tool lists changed by
`before_agent_start` handlers, since they go through the same function.

Testing:
- Added a test in `test/system-prompt-updates.test.ts`: it reverses the
active tools between two prompts and expects a single system message. It
fails without the change (a second message patches `tools` and `rules`)
and passes with it.
- `npm run check` passes.
- `./test.sh`: all coding-agent suites pass. The only failure is an
unrelated Anthropic model metadata test in `packages/ai`.

Fixes #11495

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-28 10:09:41 +01:00
robopipi[bot]
f6ee9ce9c1
fix(coding-agent): summarize union and multi-line codemode output types in tool descriptions (#11513)
With `codemode.mode: "on"`, each declared tool gets a description suffix
that says what a script call resolves to. For an object output schema,
`describeOutput` shortens this to field names. Any other schema fell
back to the whole rendered TypeScript type, collapsed onto one line. A
tool with a union output schema therefore put its full output type,
nested fields and comments included, into every request. The reporter's
`process` tool, a union of 7 variants, added about 8,000 characters.
Collapsing the type onto one line also left `//` comments in the middle
of the text.

This change:
- **Unions:** for `anyOf`/`oneOf` schemas where every variant is an
object, the suffix lists each variant's field names, joined by ` | `.
- **`const` fields:** `describeFields` now shows the value of `const`
fields, so a discriminator like `action` stays visible. For example: ``
`{ action: "start", process } | { action: "list", processes, truncated?
}` ``.
- **Other multi-line types**, such as an array of objects: instead of
the inlined type, the suffix says `` a value; `await
describeTool("name")` shows its type ``. `describeTool()` already
returns the full declaration.
- **Single-line types** such as `number` are unchanged.

Testing:
- I added a test in `test/suite/agent-session-codemode.test.ts` that
checks the suffix for three tools: one with a union output, one with an
array-of-objects output, and one with a `number` output. It fails
without the change and passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/suite/agent-session-codemode.test.ts
test/suite/agent-session-mcp.test.ts` passes, including the existing MCP
description assertions.
- `npm run check` is clean.

Fixes #11501

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-28 09:58:20 +01:00
robopipi[bot]
7391dbb0f8
fix(coding-agent): show model scope in the fullscreen header instead of the shell (#11512)
In fullscreen mode, the startup `Model scope: …` notice was still in the
shell after pi exited. With `fullscreenExitOutput: "resume-hint"`, the
shell showed it above the resume command.

Cause: `InteractiveMode.init()` printed the notice with `console.log`
before the renderer started. In fullscreen mode that output goes to the
terminal's normal screen. Pi then draws on the alternate screen, and
when it exits the terminal switches back to the normal screen, where the
notice is still visible.

The change:
- Building the notice text moves into `getModelScopeNotice()`.
- Regular mode prints it with `console.log` before the TUI starts, as
before.
- Fullscreen mode adds it to the header container below the built-in
header. It uses a `ThemedText`, so the text is built after the terminal
reports its colors and follows theme changes.

Since fullscreen is the default `tuiMode`, this applies to every user
who has `enabledModels` set and quiet startup off.

Testing:
- Added
`packages/coding-agent/test/interactive-mode-model-scope.test.ts`. It
checks two things for fullscreen: the notice renders in the header and
`console.log` is not called, and nothing is added when startup details
are hidden.
- `npm run check` passes.
- Manual check in tmux with `enabledModels` set:
- fullscreen + `resume-hint`: before the change, `Model scope:
claude-sonnet-4-5, claude-opus-4-5 (Ctrl+P to cycle)` was left in the
shell after `/quit`. Now the line shows inside pi's header and the shell
is clean after exit.
- `tuiMode: "regular"`: unchanged, the line is still printed above the
header.

Fixes #11508

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-28 09:54:06 +01:00
robopipi[bot]
88ffcdc734
fix(coding-agent): report a deleted working directory instead of crashing on startup (#11511)
Starting `pi` from a directory that has been deleted crashed with an
uncaught Node `ENOENT ... uv_cwd` stack trace. This also happened for
`--version` and `--help`. The cause is the unguarded `process.cwd()`
call in `main()` (`packages/coding-agent/src/main.ts`).

`main()` now catches `ENOENT` from that call, prints one line telling
the user to change to an existing directory, and exits with code 1.
Other errors are rethrown unchanged. Argument handling is otherwise
unchanged: `--version` still needs a valid cwd, but it now gets the
readable error instead of a crash.

**Testing**
- Added `test/deleted-cwd.test.ts`. It stubs `process.cwd()` to throw
ENOENT and checks that `main()` prints the message and exits with code
1. It also checks that a non-ENOENT error is rethrown. The first test
fails without the change and passes with it.
- `npm run check` passes.
- Manual check with the built CLI. Before, all three commands below
printed the `uv_cwd` stack trace. After, each prints `Error: The current
working directory no longer exists. Change to an existing directory and
try again.` and exits 1:
  ```sh
  d=$(mktemp -d); cd "$d"; rmdir "$d"
  pi -ne --version; pi -ne --help; pi -ne
  ```

Fixes #11509

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-28 09:51:39 +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]
807a5c051f
fix(coding-agent): keep session memory in line with the file when a write fails (#11486)
When writing a session entry failed (ENOSPC, EBUSY, ETIMEDOUT on network
drives), `SessionManager._appendEntry` had already added the entry to
memory and made it the current leaf. The process kept running, and the
next entry written to disk pointed to a parent that was not in the file.
On resume, pi follows the history only back to that gap: everything
before it disappears. In #11460 that also dropped the first system
message, which declares the tools, so resume turned off every built-in
tool.

`_appendEntry` now undoes the in-memory append if writing fails, then
rethrows. The error reaches the caller as before; later entries chain to
the last entry actually on disk.

Reports of this problem:
- #11460: ENOSPC; resume dropped all built-in tools.
- #10166: failure injected with EBUSY; the next entry on disk points to
a missing parent.
- #10661: ETIMEDOUT on Windows + NFS; resumed sessions showed only the
last few messages. That report also describes NUL holes in the file,
which this change does not address.

**Testing**
- Two tests in `test/session-manager/file-operations.test.ts` make
writes fail with real file permissions: a read-only session file (failed
append) and a read-only session directory (failed first file creation).
They check that the entry is not kept, the leaf is unchanged, the next
entry chains to the last written one, and the reopened session's branch
contains both entries. Both fail without the change and pass with it.
- `test/session-manager/` and `test/default-tools-setting.test.ts` pass
(140 tests).
- `npm run check` passes.

Fixes #11460

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-27 15:58:08 +01:00
robopipi[bot]
269afc66ab
fix(coding-agent): pass thinking level to branch summarization (#11482)
The `/tree` "Summarize" option ignored the session's thinking level.
`AgentSession` already passes `thinkingLevel` into the
`generateBranchSummary` options, but `generateBranchSummary` never read
it, so the request was built without `reasoning`.

For reasoning models on OpenAI Responses and Azure Responses, a missing
`reasoning` becomes `effort: thinkingLevelMap.off ?? "none"`. Models
that don't accept `"none"` then reject the branch summary with a 400
error:

```
Unsupported value: 'none' is not supported with the 'gpt-6-astra-…' model.
```

This change adds `thinkingLevel` to `GenerateBranchSummaryOptions`. It
sets `reasoning` on the request when the model supports reasoning and
the level is not `off`, the same rule compaction uses in
`createSummarizationOptions`.

**Testing**
- Added a test to
`packages/coding-agent/test/branch-summarization.test.ts` that records
the stream options for three cases: a reasoning model at `xhigh`, a
reasoning model at `off`, and a non-reasoning model. The expected
`reasoning` values are `["xhigh", undefined, undefined]`. Without the
fix, the test fails and every case gets `undefined`. With the fix, it
passes.
- Ran `branch-summarization`, `agent-session-branching`,
`branch-summary-extensions` and `compaction-summary-reasoning`: all
pass.
- `npm run check` passes.
- Not tested against a real provider endpoint.

Fixes #11444

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-10-27 15:38:06 +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
robopipi[bot]
a6a7bac868
fix(coding-agent): hidden thinking with an empty label renders no rows (#11470)
When thinking was hidden and the hidden-thinking label was set to `""`
(for example with `ctx.ui.setHiddenThinkingLabel("")`), every assistant
message that contained thinking still drew blank rows:
- thinking followed by a tool call drew 2 blank rows;
- thinking followed by text drew 3 blank rows before the text.

`AssistantMessageComponent.updateContent()` had three causes:
- It counted hidden thinking as visible content when deciding on the
leading and trailing spacers.
- It always added the label component. With an empty label the theme's
escape codes are still there, so the component rendered one blank row.

**What changed**
- A hidden thinking run with an empty label is now skipped entirely. Its
run index is still advanced, so click overrides on other runs keep their
indices.
- Spacers are now added lazily, just before the next visible block. The
result is the same as before: one spacer before the first visible block,
and one after a visible thinking run that is followed by more visible
content. But spacing now follows what actually renders.
- Default behaviour (the "Thinking..." label, or expanded thinking) is
unchanged.

**Testing**
- Added two tests to `test/assistant-message.test.ts`:
- a hidden thinking run with an empty label renders nothing, both before
a tool call (`[]`) and before text (`["", "answer"]`);
- with the default label, rendering still gives `["", "Thinking...", "",
"answer"]`.
- The empty-label test fails without the fix and passes with it.
- `npm run check` passes. The `test/assistant-message.test.ts` and
`test/interactive-mode*` tests pass.
- Rendering the component directly, with hidden thinking and an empty
label:
  - thinking + tool call: before, 2 blank rows; after, `[]`;
- thinking + text: before, 3 blank rows then the text; after, `["", "
hi"]`.

Fixes #11402

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:19:37 +01:00
robopipi[bot]
5fb9a0b110
fix(coding-agent): skip npm audit and fund output when installing pi packages (#11469)
`pi install`, `pi remove` and `pi update --extensions` run npm without
`--no-audit`, so npm prints its "N vulnerabilities … run `npm audit
fix`" advice after every install. pi gives users no way to clear that
warning, and following it causes harm. In `~/.pi/agent/npm`, `npm audit
fix` runs without `--legacy-peer-deps`, so npm installs the
host-provided `@earendil-works/pi-*` peers there: a second copy of pi.
The comment above `getNpmInstallArgs` warns against exactly that.

The change adds `--no-audit --no-fund` wherever pi already passes
`--legacy-peer-deps` to npm:
- `getNpmInstallArgs` (npm package installs and updates)
- `getGitDependencyInstallArgs` (dependency installs for git packages)
- npm uninstall

pi's managed self-update (`runManagedNpmCi`) already passes both flags.
bun and pnpm arguments are unchanged.

I did not edit `CHANGELOG.md`, per CONTRIBUTING.md.

**Testing**
- Updated the expected npm argument arrays in
`test/package-manager.test.ts`. With the source change reverted, 11
tests fail; with it, all 132 pass.
- `npm run check` passes.
- Ran the built CLI by hand with `PI_CODING_AGENT_DIR=/tmp/pa pi install
npm:lodash@4.17.20`:
- Before the change, the output ended with npm's "To address all issues,
run: npm audit fix" block.
- After, it shows only "added 1 package" and "Installed
npm:lodash@4.17.20".

Fixes #11409

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:17:54 +01:00
robopipi[bot]
f049787d9e
fix(coding-agent): default openai provider to gpt-6.1-sol (#11467)
The `openai` provider still defaulted to `gpt-5.5`. The same default
applies to API-key logins and to "Sign in with ChatGPT", so logging in
through ChatGPT selected `openai/gpt-5.5`. The reporter says that model
no longer works with a ChatGPT subscription. The `openai-codex` and
`github-copilot` providers already default to `gpt-6.1-sol`.

This change sets `defaultModelPerProvider.openai` to `gpt-6.1-sol` in
`packages/coding-agent/src/core/model-resolver.ts`. The new value
applies in three places: the model picked after `/login` when no model
was set, the fallback model at startup, and the default model for
`--provider openai`. API-key users get the new default too.

I did not test against a real ChatGPT account, so the claim that the
subscription rejects `gpt-5.5` is not checked here.

Testing:
- Updated the assertion in `test/model-resolver.test.ts` ("openai
defaults track current models"). It now expects `gpt-6.1-sol`; the
model-resolver tests pass.
- `npm run check` passes.
- `pi --list-models gpt-6.1` still lists `openai/gpt-6.1-sol`, so the
new default is in the catalog.

Fixes #11442

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:13:12 +01:00
robopipi[bot]
7853186d14
fix(coding-agent): keep open extension dialog visible when the editor component is replaced (#11466)
`ctx.ui.setEditorComponent()` always cleared the editor slot and put the
new editor there with focus. If an extension dialog
(`confirm`/`select`/`input`/`editor`, or an inline `custom()` component)
was open at that moment, the editor replaced it on screen. The dialog
was still on the dialog stack and its promise still pending, but nobody
could see or answer it. It hung until its timeout, or forever without
one.

Now `setCustomEditorComponent` leaves the editor slot and focus alone
while a dialog is open. The new editor is still created and wired up,
and `showTopExtensionDialog` shows it when the last dialog closes.
Without an open dialog nothing changes.

Testing:
- Added a regression test to
`test/interactive-mode-extension-dialogs.test.ts`. It opens a confirm,
replaces the editor, then checks that the confirm stays visible and
answerable and that the new editor is focused afterwards. It fails
without the fix and passes with it.
- `npm run check` passes.
- Ran the reproduction from the issue in tmux (`pi -e ./a.ts -e ./b.ts`,
where a.ts opens a non-blocking `confirm` in `session_start` and b.ts
calls `setEditorComponent`).
  - Before: only the editor shows, and the confirm times out.
  - After: the confirm shows, and Enter gives `answer: true`.

Fixes #11431

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:11:18 +01:00
robopipi[bot]
fecc19e4e9
fix(coding-agent): keep grep context lines aligned with ripgrep when a file has a lone CR (#11465)
When `context` is set, the grep tool reads the file again to print the
lines around each match. It turned CRLF into LF and also turned every
lone CR into LF before splitting the text into lines. ripgrep, which
supplies the match line numbers, counts lines only at LF. A lone CR
therefore added an extra line on pi's side, so every later line was off
by one.

For example, in a file containing `before\rprogress\nneedle\nafter`, a
search for `needle` with `context: 1` showed `progress` as the line-2
match and left out `after`.

The fix keeps the CRLF normalization and splits only on LF, so pi
numbers lines the same way ripgrep does. Displayed lines already have
`\r` removed, so the line before the match now shows as
`beforeprogress`.

**Testing**
- Added a test in `packages/coding-agent/test/tools.test.ts` for a file
with a lone CR searched with `context: 1`. It fails without the fix and
passes with it.
- `test/tools.test.ts`: all 87 tests pass.
- `npm run check` passes.
- Reproduction from the issue. Before the fix:
  ```
  f.txt-1- before
  f.txt:2: progress
  f.txt-3- needle
  ```
  After the fix:
  ```
  f.txt-1- beforeprogress
  f.txt:2: needle
  f.txt-3- after
  ```

Fixes #11398

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-27 09:07:23 +01:00
Armin Ronacher
89c921d8b5 fix: harden handling of untrusted URLs, terminal text, and streamed data
- ai: OAuth logins only accept http(s) URLs from servers (shared
  trustedHttpUrl, now also checked by Radius); pi-messages rejects
  contentIndex values that are not the next or an existing block; the
  CLI auth file is written with mode 0600
- ai: OpenCode DeepSeek V4.1 Flash (renamed from V4 Flash upstream) no
  longer sends prompt_cache_retention
- tui: setTitle() replaces control characters; export
  replaceControlCharacters()
- coding-agent: the login dialog only opens web URLs and never prints
  control characters from sign-in, device-code, or info URLs
- durable: partial commits no longer merge a new __proto__ member into
  Object.prototype
2026-10-26 20:46:31 +01:00
Petr Houška
ae823bc0bb fix(coding-agent): rank tools first when the search query is their name
A query that equals a tool's full name or the part after a __ separator
(thread_read for mcp__app__thread_read) now ranks that tool first in
tool_search and codemode searchTools(). BM25 length normalization could
otherwise rank a shorter description sharing the name's words above it.

closes #11331
2026-10-26 14:17:45 +01:00
triagent
66c2fa3d7a fix(coding-agent): update undici to 8.12.3 for security advisories 2026-10-26 13:50:55 +01:00
robopipi[bot]
984cf6643d
fix(coding-agent): resolve provider/id references when the provider id contains a slash (#11397)
`--model "<provider>/<id>"` returned "not found" when the provider id
itself contains a `/`. URL-derived ids such as
`llama-server=http://host:10800` are one example.
`splitProviderPrefix()` in `model-resolver.ts` split the reference at
the first `/`, which turned the prefix into `llama-server=http:`. No
provider has that id, so resolution fell through to not-found.

`splitProviderPrefix()` now compares the text against the provider ids
in the catalog. A provider matches when the text starts with its id
followed by `/`, and the longest matching id wins. Ordinary provider ids
resolve exactly as before.

Testing:
- I added two cases to the `resolveModelReference` table in
`test/model-resolver.test.ts`, using a provider
`llama=http://host:10800`: one plain, one with a `:high` thinking
suffix. Both fail without the change and pass with it. The test runs
every case against the catalog in both orders.
- `node ../../node_modules/vitest/dist/cli.js --run
test/model-resolver.test.ts`: 64 passed.
- `npm run check`: passes.
- Before the change, a standalone check of `resolveModelReference` with
the reporter's exact provider and model
(`llama-server=http://host:10800/unsloth/Qwen3.8-27B-GGUF:BF16`)
returned `not-found`. It now returns `resolved`.
- One test, `test/model-runtime-classifiers.test.ts` ("applies provider
compat from models.json to classifier models"), fails in my environment
both with and without this change. It is unrelated to this fix.

Fixes #11394

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-25 08:48:27 +01:00
robopipi[bot]
5f960a0e2b
fix(coding-agent): pass real path as npm prefix when agent dir is a symlink (#11396)
When the agent directory is reached through a symlink (for example
`~/.pi` linked into a dotfiles repo), pi passed the symlinked path as
`--prefix` to `npm install` and `npm uninstall`. npm then writes lock
entries such as `../../../dotfiles/pi/agent/npm/node_modules/foo`
instead of `node_modules/foo`. Each later install adds one more nesting
level, so `package-lock.json` keeps growing.

This change resolves the install root with `realpathSync` before passing
it to the package manager:
- in `getNpmInstallArgs()`, which covers install and update for npm,
pnpm and bun;
- in `uninstallNpm()`.

The paths pi loads extensions from are unchanged.

**Testing**
- Added a regression test in `test/package-manager.test.ts`. It uses a
symlinked agent dir and checks that install and uninstall get the real
path as `--prefix`. The test fails without the fix and passes with it.
- Wrapped the existing `--prefix`/`--cwd` expectations in
`realpathSync(...)`. They now match on systems where the temp dir is
itself a symlink, such as macOS.
- Ran `npm run check` (clean) and `test/package-manager.test.ts` plus
`test/package-manager-ssh.test.ts` (all pass).
- Manual reproduction with `~/.pi` as a symlink, running `pi install
npm:ms` several times:
- before: lock keys grew to
`../../../dotfiles/pi/agent/npm/node_modules/ms`,
`../../../dotfiles/dotfiles/pi/...`, and so on;
- after: `[ '', 'node_modules/ms' ]`, and `pi remove npm:ms` leaves `[
'' ]`.

Fixes #11358

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-10-25 08:47:34 +01:00
Mario Zechner
f715a15c10 fix(ai,chord,durable,coding-agent): parse streamed tool-call arguments incrementally (fixes #9265)
- ai: StreamingJsonParser / ToolCallArgumentsStream read each argument delta once (tolerant,
  immutable snapshots, linear cost); every provider, pi-messages and the faux provider use it
  and keep stream state (partialJson, partialArgs, index, customInput, streamIndex, Bedrock
  reasoning bytes) off the content blocks; grammar tool input appends per delta
- agent: streamProxy uses the same parser
- chord: Delta append ops own their text (ownedSlice), so ops and replicas no longer keep the
  whole growing string alive
- durable, ai, mcp: bounded error excerpts and summaries own their characters
- coding-agent: tool calls render streamed arguments once per frame, not once per delta
2026-10-24 08:00:07 +00:00
Petr Houška
acf83acfd8 docs: restore existing changelog attributions 2026-10-23 22:10:30 +02:00
Petr Houška
e9c3ac9d98 docs: drop author attribution from backfilled changelog entries 2026-10-23 22:10:30 +02:00
Petr Houška
a8fe8e0adc docs: add missing changelog entries for #9301, #11289, #11290, #11293, #11296, #11347 2026-10-23 22:10:30 +02:00
David Brailovsky
ed76cca69b fix(coding-agent): test forkFrom copied content instead of fs calls
Move the forkFrom test into custom-session-id.test.ts, drop the fs mock
and call-count checks, and clean up the temp directory.
2026-10-23 21:24:38 +02:00
triagent
8ddc44ec8b fix(coding-agent): write forked session through one fd
forkFrom reopened the target file with appendFileSync for every entry,
which is slow where open/close is expensive. fixes #11253
2026-10-23 21:24:38 +02:00
Petr Houška
606af8662e feat(coding-agent): show model names in model selectors when they add information
/model and /scoped-models listed only `id [provider]`, so renamed
overrides from models.json were invisible in the list. Rows now show
the name in parentheses when it has words the id lacks, e.g.
`minimax/minimax-m3 (MiniMax M3 [parasail]) [openrouter]`. Names that
only restate the id with different casing, separators, or a dropped
namespace/version suffix stay hidden.

closes #5916
2026-10-23 21:19:51 +02:00
yzchen
3953e9ef49 fix(examples): todo renderResult returns undefined on validation errors
When a todo call fails schema validation, createErrorToolResult produces
details: {} (a truthy empty object), so renderResult skips both the
!details and details.error branches, then switch (details.action) has no
default and returns undefined. ToolExecutionComponent.updateDisplay()
then calls addChild(undefined), and the next Box.render pass crashes the
whole interactive TUI.

Add a default case that renders the validation error text.
2026-10-23 20:20:02 +02:00
Petr Houška
c55972086d docs(coding-agent): add changelog entry for #11209 2026-10-23 19:48:59 +02:00
Petr Houška
c8ec752cf9 fix(coding-agent): skip fd/rg downloads on unsupported architectures
Only x64 and arm64 map to prebuilt release archives. ARM32 and ia32 previously
fell back to x86-64 binaries; now pi warns and recommends the system package manager.

fixes #11209
2026-10-23 19:48:59 +02: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
robopipi[bot]
36be9c612e
fix(coding-agent): decode pi.exec output across UTF-8 chunk boundaries (#11326)
`execCommand()` (the function behind `pi.exec()`) decoded each stdout
and stderr chunk on its own with `data.toString()`. When a multi-byte
UTF-8 character was split across two pipe chunks, each half was turned
into U+FFFD, so the result was corrupted. For example, `"你好"` came back
as `"���好"` while the command still exited with code 0.

This change gives stdout and stderr each their own
`StringDecoder("utf8")`. A decoder holds back incomplete bytes until the
rest of the character arrives. Both decoders are flushed with `end()`
before the promise resolves, on both the success path and the error
path. `waitForChildProcess()` can resolve without a stream `end` event,
so the decoders are flushed there and not in an `end` handler. The
return shape, exit codes, and kill behavior are unchanged.

**Testing**
- Added a regression test to `packages/coding-agent/test/exec.test.ts`.
The child process splits `"你好"` across two writes on both stdout and
stderr. The test fails without the fix and passes with it: `node
../../node_modules/vitest/dist/cli.js --run test/exec.test.ts` reports 3
passed.
- `npm run check` passes.
- Reproduction from the issue: a child that writes the first byte of
`"你好"` and then the rest 100 ms later. Before the fix the result was
`{"stdout":"���好",...}`. After the fix it is
`{"stdout":"你好","stderr":"","code":0,"killed":false}`.

Fixes #11312

Requested by @davidbrai

---------

Co-authored-by: triagent <triagent@localhost>
Co-authored-by: David Brailovsky <davidbrai@gmail.com>
2026-10-23 14:47:34 +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
Armin Ronacher
9ffd27cfaa fix(mcp): accept only http and https OAuth URLs
A malicious MCP server could supply a file: or custom protocol
authorization endpoint, which sign-in passed to the platform URL handler.
2026-10-23 12:19:56 +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
8c03fef4d3 fix(tui,coding-agent): rank contiguous fuzzy matches and search session fields separately
fuzzyMatch scored only the greedy leftmost alignment, so a query could be
scattered over an earlier unrelated part of the text (opus via "openrouter",
astra via "radius radius/gpt") and the real match ranked last. It now also
scores every contiguous occurrence and keeps the best alignment.

/resume matched one concatenated id/name/messages/cwd string. Scattered
matches in megabytes of message text or in UUIDs were noise, and the
position penalty buried real hits deep in a transcript. Fields are now
matched separately: name and cwd fuzzy, id and messages exact only, with no
position penalty. Ranking is name text, then id/message/cwd text, then
scattered name/cwd matches. Lowercased message text is cached per session.

closes #11220
2026-10-22 21:39:17 +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
muyiyr
c60de42e98 fix(coding-agent): preserve pi.exec spawn errors
Fixes #11085
2026-10-22 21:14:23 +02:00
Petr Houška
1c720d2850 fix(coding-agent): render HTML export for sessions deeper than the call stack
Replace recursive tree walks in the export template (sortChildren, mapNodes,
markActive) with iterative traversals, and build getPath with push + reverse.

closes #11225
2026-10-22 20:51:38 +02:00
Ville Penttinen
64627838b2
feat(coding-agent): add messageId and timestamp to markdown transformer context
* feat(coding-agent): add messageId and timestamp to markdown transformer context

* fix(coding-agent): update compaction render test for messageMeta argument

---------

Co-authored-by: Petr Houška <houskape@gmail.com>
2026-10-22 20:49:38 +02:00
Petr Houška
9359adf0c6 fix(coding-agent): render system messages in HTML export
Render the leading system message as the system prompt and tool list,
and later system messages as named section and tool updates. The header
prompt/tools from agent state remain as a fallback for sessions without
system entries. CLI --export now shows the prompt and tools too (#3464).

closes #11261
2026-10-22 20:37:03 +02:00
Petr Houška
27cc6bfc0c fix(coding-agent): stop macOS shell groups before killing them
Reduce the race where killing a foreground child wakes its shell before the shell is signalled. Keep Linux and Windows termination unchanged and preserve kill fallbacks when stopping fails.

closes #11252
2026-10-22 20:22:20 +02:00
David Brailovsky
476a303c12
fix(coding-agent): drop min-release-age override from managed npm ci (#11270)
Updates of pi.dev installer installs run npm ci against a release
lockfile that pins every package, so npm never resolves versions and a
configured min-release-age does not apply. The --min-release-age=0 flag
had no effect, but security scanners that block it stopped the update.

Global npm, pnpm and bun self-updates keep their age-gate overrides.

closes #11262
2026-10-22 13:50:26 +02:00
Armin Ronacher
2685de7a17 fix: update deps for security advisories
Bump vitest to 4.1.12 (vite 8.3.4) and the gondolin example extension to 0.14.0, which drops node-forge. Gondolin's undici moves to 6.29.1.
2026-10-22 11:24:50 +02:00
Vegard Stikbakke
cae8d69e57
fix(ai): use OpenRouter-reported total cost (#10286)
Use authoritative OpenRouter billing for Chat Completions and Anthropic Messages while retaining catalog component estimates and fallback behavior.

Fixes #9980
2026-10-22 11:01:39 +02:00
Vegard Stikbakke
7c22a93d0e
feat(ai): add totalFromProvider flag to usage cost (#11178)
Add Usage.cost.totalFromProvider to record whether cost.total is the
billed cost reported by the provider or an estimate from catalog rates.
Catalog-derived totals set it to false; a missing value also means
false. combineUsage marks a sum as provider-reported only when both
parts are.
2026-10-22 09:02:01 +02:00