Commit graph

4168 commits

Author SHA1 Message Date
gaoyk19
14b804d809
feat(ai): add requiresNonNullAssistantContent compat flag (#8118)
Closes #8063
2026-11-04 11:29:54 +01:00
Michael Renner
aba42ae6b9
fix(coding-agent): make subagent progress and failures reliable (#8250)
The shipped subagent example could say a child was done while it was
still working, lose the useful part of process failures, and return a
normal-looking tool result when the delegated work had actually failed.
Single and chain output could also exceed the normal tool limit.

This also adds a dedicated regression suite covering progress state,
cancellation, process failures, output limits, cwd-based agent
selection, and persisted tool-result error state.

After this change:

- queued, running, completed, failed, and aborted work stay distinct
throughout the run;
- interrupting the parent run preserves partial child results, stops
queued work from starting, and escalates when a child ignores SIGTERM;
- signal exits, spawn failures, provider errors, stderr, and assistant
output remain visible instead of collapsing to `(no output)`;
- failed subagents become failed tool results, so the parent model can
react instead of treating them as successful calls;
- single and chain results stay within the normal 50 KB / 2,000-line
limit;
- project agents are selected and confirmed from each task's actual
working directory rather than the dispatcher's directory.

## Further potential improvements

These are useful follow-ups, but they are separate product and storage
decisions rather than part of the failure-handling fixes in this PR.

<details>
<summary>Potential follow-ups</summary>

### Keep persisted transcripts bounded

Subagent tool results persist the full child message history in
`details`, which can make parent session files large and slow to reload.
A follow-up could compact that history or spill it to disk.

### Define an aggregate parallel-output budget

Parallel mode still gives every task its own 50 KB budget. A follow-up
could keep every task represented while enforcing one overall
tool-result limit.

### Report child usage to the calling session

Per-child token and cost usage is visible in result cards but is not
added to the calling session's totals.

### Isolate malformed agent definitions

One malformed YAML agent can still break discovery for every valid agent
in the same directory.

### Limit subagent recursion

A nesting limit would prevent accidental process and paid-request
multiplication when subagents can invoke the subagent tool again.

* https://github.com/earendil-works/pi/issues/8195

### Add a configurable timeout

Unattended runs would benefit from a generous opt-in timeout for
children that never complete.

</details>

## Related issues

### Spawn failures lose their diagnostics

This directly addresses the reported case where a nonexistent child
working directory produces `(no output)` instead of the actual spawn
error. The regression suite includes that same reproduction.

* https://github.com/earendil-works/pi/issues/8129

### Persisted child history can bloat sessions

This PR intentionally keeps full child history in tool-result details,
so session compaction or spill-to-disk remains follow-up work rather
than something fixed here.

* https://github.com/earendil-works/pi/issues/7452
2026-11-04 11:24:12 +01:00
wren
51313540a4
fix(coding-agent): preserve pasted text during editor restoration (#10146)
Closes #9809
2026-11-04 11:20:58 +01:00
Gangqiang Yao
8a3e163b0d
fix(coding-agent): prevent prompt and tree navigation overlap (#9155)
Closes #9154
2026-11-04 11:18:25 +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
Alexey Zaytsev
aceb1bd128
feat(coding-agent): upgrade Mermaid terminal rendering (#8158)
Replaces grok-mermaid with lovely-mermaid 0.3.3. Closes #8157
2026-11-04 11:06:51 +01:00
Pier Carlo Cadoppi
e6865ec7e1
fix(coding-agent): realpath extension entries before jiti import (#8112)
Closes #8092
2026-11-04 11:04:24 +01:00
Giannis Papakostas
ee962fb4db
fix(plan-mode): make progress tracking robust and tolerant (#7950)
## What

Hardens the plan-mode example extension's step-progress tracking.
Previously, completion markers were only recognized in the exact
`[DONE:n]` form inside visible text blocks, so steps frequently never
got checked off during execution.

## Changes

- **`getTextContent`** now reads `thinking` blocks as well as text
blocks, so `[DONE:n]` tags emitted during reasoning are honored.
- **Marker tolerance** (`extractDoneSteps`): accepts `[DONE: 1]`, `[DONE
1]`, `Done: n`, and prose phrasings like "Step n done / is complete /
finished" — all adjacency-anchored so negations ("Step 9 is not done")
never match.
- **Whole-plan fallback** (`allStepsComplete`): if the run's final
message says the plan is done (e.g. "All steps complete"), all remaining
steps are marked at `agent_end`.
- **Plan extraction dedup**: only the last `Plan:` section is extracted,
so a plan drafted in a thinking block no longer duplicates the steps
from the text block.
- **No more truncation**: step text is kept in full (previously cut at
50 chars); the widget truncates for display only. The execution prompt
now shows the complete step text.
- **No more dropped steps**: steps whose text starts with a backtick
(e.g. `1. \`index.ts\` — …`) are no longer skipped.
- **Widget refresh** happens on every turn instead of only when a marker
matched.
- **Plan-complete card** is delivered with `nextTurn` instead of
triggering an extra LLM turn.

## Tests

`packages/coding-agent/test/plan-mode-utils.test.ts` updated for the new
behavior (truncation, backtick steps, marker-count semantics) and
extended with coverage for the new cases (marker variants, prose
phrasings, negation safety, thinking-draft dedup, markdown titles,
whole-plan completion).

All 43 tests in `plan-mode-utils.test.ts` +
`plan-mode-extension.test.ts` pass (`npx vitest run`).

Note: the repo-wide type-check currently fails on `main` with
pre-existing `ModelCatalog`/`never` errors unrelated to this change;
this branch adds none (same count with and without the diff).
2026-11-04 10:53:52 +01:00
robopipi[bot]
c13f6acfbe
Highlight .dart, .nix, and .groovy files in read/write views (#11790)
The `read` and `write` tool views find a file's language from its
extension with `getLanguageFromPath()`. That function's extension table
had no entries for Dart, Nix, or Groovy. Their highlight.js grammars are
loaded at startup, but the lookup returned `undefined`, so these files
were shown as plain uncolored text.

This change adds `dart → dart`, `nix → nix`, `groovy → groovy`, and
`gradle → groovy` to the table in
`packages/coding-agent/src/modes/interactive/theme/theme.ts`.

HTML export keeps its own separate extension map and a vendored
highlight bundle without a Dart grammar. This change does not touch
either.

## Testing

- Added a test to `test/syntax-highlight.test.ts` that checks the new
extension mappings. It fails without the fix and passes with it.
- `node ../../node_modules/vitest/dist/cli.js --run
test/syntax-highlight.test.ts` passes.
- Before, `getLanguageFromPath("a.dart")` (and `.nix`, `.groovy`,
`build.gradle`) returned `undefined` from the built dist. After, they
return `dart`, `nix`, `groovy`, and `groovy`.
- `npm run check` reports 6 type errors in `packages/ai/test` (model ID
types). Main without this change has the same 6 errors.

Fixes #11643

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 10:45:55 +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
梧桐雨
a63c4ec5c8
fix(coding-agent): clear delivered image-only queue entries (#8612)
## Summary

- remove delivered queue entries even when the user message has no text
- keep image-only steering and follow-up pending counts in sync with the
agent queue
- cover image-only steering delivery with a regression test

## Testing

- `npm run check`
- `./test.sh`

Fixes #8581
2026-11-04 10:24:43 +01:00
Sai Mallikarjun
f3a64af07b
fix(ai): preserve aborted stop reason during lazy setup (#8635)
Fixes #8409

## Summary
- Pass the request abort signal through lazy stream setup wrappers.
- Report setup failures as aborted when the request signal is already
aborted.
- Added a regression test for aborting during a tool execution before
next auth setup.
2026-11-04 10:23:35 +01:00
Gangqiang Yao
7a53c310a3
fix(coding-agent): settle tool results before disposal (#9126)
Fixes #9124.

Disposing a runtime during tool execution disconnects persistence before
the interrupted turn settles, leaving a stored assistant tool call
without its result. Await `session.abort()` before shutdown and disposal
so the interrupted tool result and final assistant message are
persisted.

This matches `teardownCurrent()`. `AgentSession.dispose()` remains
synchronous. Like the existing replacement path, the wait has no
timeout; handling tools that ignore cancellation is outside this change.

Related: #9986 concerns results for tool calls skipped during a
mid-session abort. This fix preserves the result of an already-running
tool during runtime shutdown.

Validation on main `f1b2e77f5`:

- The regression fails without the fix and passes with it, checking the
durable message order and `Operation aborted` result immediately after
`dispose()` returns, before awaiting the prompt.
- `npm run check`, offline build, and `./test.sh` pass.
2026-11-04 10:21:16 +01:00
Sviatoslav Abakumov
2cfab7c269
fix(coding-agent): render session tree search cursor (#9157)
This PR adds the cursor block to the **Type to search** input in
**Session Tree**. This is similar to the search input in **Resume
Session**.

Before:

<img width="1120" height="778" alt="Screenshot 2026-09-04 at 19 25 30"
src="https://github.com/user-attachments/assets/8f2e0ba9-8476-452b-a6be-1ad9b8d7d6c8"
/>

After:

<img width="1120" height="778" alt="Screenshot 2026-09-04 at 19 25 50"
src="https://github.com/user-attachments/assets/7f6ffcbf-db5a-4732-84fe-6ee0544116fa"
/>
2026-11-04 10:19:38 +01:00
Gangqiang Yao
c3b938e3e9
fix(coding-agent): reject reload while running or compacting (#9222)
Fixes #9221.

An extension tool can lose its context when an RPC command or SDK caller
reloads the session during tool execution: the tool awaits work, reload
invalidates its runner, then `ctx.getSystemPrompt()` throws a
stale-context error. The failed tool result is stored in session
history.

Reject reload when `isStreaming` or `isCompacting` is already set,
before shutdown or runner invalidation. This gives RPC and direct
callers the same entry checks as the TUI. The production change remains
seven lines.

#9548 removed the wrapper access behind the original reproduction. The
regression exercises the tool's own context access after an await,
through both an RPC-style extension command and direct
`AgentSession.reload()`. Rejection preserves the successful result in
session history and the next provider request. Compaction, tree
navigation, and idle reload are also covered.

Scope: this guards operations already marked running or compacting. It
does not serialize reload with prompt preflight or prevent a new prompt
from starting during an ongoing reload.

Related: #5735 proposes a broader deferred-reload API. This patch only
adds the existing TUI entry checks to the core reload method.

Validation on main `f1b2e77f5`:

- Without the guard, four regression cases fail and idle reload passes.
With it, all five pass.
- `npm run check`, offline build, and `./test.sh` pass.
2026-11-04 10:18:59 +01:00
Danila Poyarkov
dca1f55527
fix(coding-agent): preserve indentation in rendered diffs (#9274)
## Problem

The edit tool's intra-line renderer can drop indentation from a removed
line when text is inserted before otherwise unchanged content.

For example, this input:

```diff
-83   await commitRecordingChunk({
+83   const result = await commitRecordingChunk({
```

was rendered as:

```diff
-83 await commitRecordingChunk({
+83   const result = await commitRecordingChunk({
```

`diffWords()` groups the shared leading spaces into the added segment
(`" const result = "`). The existing highlighting logic moved those
spaces outside inverse styling, but only appended them to the added
line.

## Solution

Extract whitespace shared by both lines before calling `diffWords()`,
then initialize both rendered lines with that prefix. This preserves
indentation without changing the existing word-level highlighting
behavior. The existing handling for whitespace that belongs to only one
side remains unchanged.

Added regression coverage for:

- insertion before indented content;
- replacement within indented content;
- keeping shared indentation outside inverse highlighting.

## Validation

- `npm run check`
- `./test.sh`
- targeted `packages/coding-agent/test/diff.test.ts` (3 tests)
2026-11-04 10:17:34 +01:00
Gangqiang Yao
66c6556771
fix(coding-agent): track discarded user bash output (#10165)
Closes #10164.

A user `!` command can discard earlier chunks but report `truncated:
false` when the retained tail fits within the output limits. The model
then receives no truncation notice or full-log path.

Track discarded chunks and include that flag in the executor's result
for both completion and cancellation. This applies to the current shared
finalization path and preserves streaming ANSI handling. Output limits
and message format stay unchanged.

Related: #2852. Here the complete log is already saved; the incorrect
flag hides its path from the model.

Validation on main `f1b2e77f5`:

- Two deterministic 64 KiB + 44 KiB cases fail without the fix. All
three regressions pass with it, checking completion, cancellation, the
complete log, model-facing path, and short-output behavior.
- Regressions are in `test/bash-executor-truncation.test.ts`; that file
and `test/tools.test.ts` pass all 88 tests.
- `npm run check`, offline build, and `./test.sh` pass.
2026-11-04 10:16:53 +01:00
Christian Tietze
3b57ed7e27
fix: limit selector window height (#11072)
With this PR, we stop  rendering every option.
Instead, we render a centered, clamped window of up to 10 options and
show `(selection/total)` when the list overflows.

This keeps the selected option visible in long extension-provided lists
without changing navigation, keybindings, callbacks, countdowns, or
short-list rendering.

Fixes #6688.

## Merge Danger

**Door:** Two-way

The change is localized to `ExtensionSelectorComponent.updateList()` and
can be reverted without migration or persisted-state concerns.

**Blast Radius:** Low

Only extension selectors with more than 10 options change appearance;
they now render a window and position indicator instead of every option.

---------

Co-authored-by: Pi via openai-codex (gpt-5.6-sol) <noreply@openai-codex>
2026-11-04 10:15:27 +01:00
robopipi[bot]
b0c64f33c8
fix(coding-agent): load user keybindings in pi config (#11788)
`pi config` ignored `~/.pi/agent/keybindings.json`. `selectConfig()`
never called `setKeybindings()`, so the config selector's
`getKeybindings()` fell back to the default TUI bindings. Remaps such as
`"tui.select.down": ["down", "ctrl+n"]` did nothing there, although they
work in `/settings` and the `--resume` picker.

`selectConfig()` now installs
`KeybindingsManager.create(options.agentDir)` before it builds the UI,
the same way the session picker and startup UI do.

**Testing**
- Added `test/config-selector-keybindings.test.ts`. It runs
`selectConfig()` with a stubbed terminal and component and an agent dir
whose `keybindings.json` remaps `tui.select.up` and `tui.select.down`,
then checks that the active keybindings include `ctrl+p` and `ctrl+n`.
It fails without the change and passes with it.
- Manual check: ran `pi config` in tmux with that `keybindings.json`.
Before the change, Ctrl+N did nothing. After it, Ctrl+N moves the
selection like Down.
- `npm run check` reports only two errors, both missing modules in
`packages/schema/test/bench/candidates.ts`. They occur on `main` without
this change.

Fixes #11651

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:59:49 +01:00
robopipi[bot]
081a6e87f6
fix(coding-agent): share the auth.json lock across symlinked paths (#11787)
`FileAuthStorageBackend` locked `auth.json` with proper-lockfile and
`realpath: false`, so the lock directory was `<path as given>.lock`.
Reads and writes follow symlinks to the same real file, but each path
alias got its own lock. For example, an agent dir whose `auth.json` is a
symlink to a shared file used a different lock than the real file. Two
pi processes could then enter the credential-modification section at the
same time and refresh the same OAuth token concurrently.

This change drops `realpath: false` from the sync and async lock calls,
so proper-lockfile resolves the path and every alias of the file uses
one lock. Resolving the path needs the file to exist, which is already
the case: `ensureFileExists()` runs before either lock is taken.

The same `realpath: false` pattern is also in `settings-manager.ts`,
`trust-manager.ts`, and `package-manager-cli.ts`. This change leaves
those alone.

## Testing

- I added a test to `test/auth-storage.test.ts`. It holds the lock on
the real `auth.json`, then checks that a backend opened through a
symlink alias waits in `withLockAsync` until aborted, and gets `ELOCKED`
from `withLock`. Neither path runs the mutation. Without the fix the
test fails because the async call enters the lock; with the fix it
passes.
- `node ../../node_modules/vitest/dist/cli.js --run test/auth-storage
test/settings`: 151 tests pass.
- `npm run check` passes.
- With the reproduction script from the issue, current main prints
`symlink alias: ENTERED`; with this change it prints `symlink alias:
BLOCKED`. The same-path control prints `BLOCKED` in both cases.

Fixes #11655

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
Co-authored-by: Mario Zechner <contact@badlogicgames.com>
2026-11-04 09:57:47 +01:00
robopipi[bot]
4cf69332c3
fix(coding-agent): resize images in queued steering and follow-up messages (#11782)
Images in idle prompts go through `processImage`: they are resized to
the model's limits, and a coordinate hint is added to the text. Images
in messages queued while the agent was busy skipped this step. That
covers `steer()`, `followUp()`, `prompt()` with `streamingBehavior`, and
RPC `steer`/`follow_up`. Those images were sent and saved at full size.
For example, a 1200x9000 PNG stayed 1200x9000, which is over Anthropic's
8000 px limit, and because it is saved in the session it was replayed on
every later request.

What the change does:
- `_queueSteer`/`_queueFollowUp` now mark queued user messages that
carry images.
- When such a message is delivered (`message_start`), `AgentSession`
normalizes its images in place with `_normalizePromptImages`, the same
code the idle prompt path uses, and appends the resize hints to its
text. This runs before extensions and listeners see the message, before
it is saved, and before it is added to the request context.
- Doing it at delivery keeps enqueueing synchronous. A slow resize
therefore cannot leave a message queued after the run has ended. I hit
exactly that when I first tried normalizing at enqueue time. Delivery is
also when the resize uses the model current at that moment.
- The queue display still matches and removes the message, because
matching runs before the text is changed.

Testing:
- New `test/agent-session-queued-images.test.ts`: a 1200x9000 PNG is
queued while the agent is streaming, through `steer()`, `followUp()` and
`prompt(..., { streamingBehavior: "followUp" })`. The test checks three
things: each saved message has a resized image and the "original
1200x9000, displayed at 267x2000" hint; no request sent to the model
contains the original image; and the queue is empty afterwards. It fails
without the change and passes with it.
- `npm run check` passes. The `agent-session*` tests, the queue- and
image-related tests and `test/suite` pass.
- Before the change, the saved steer and follow-up messages had
`image/png 1200x9000` while the idle prompt had a resized JPEG. After
it, all three are resized.

Fixes #11738

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:36:54 +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
robopipi[bot]
ea0f49a365
fix(coding-agent): name the llama.cpp server and cause when it cannot be reached (#11779)
When `/login llama.cpp` could not connect to the server URL, pi showed
only `Error: Failed to save API key for llama.cpp: fetch failed`. That
happens on a refused connection, a wrong port, or `https://` against a
plain-HTTP server. Node's `fetch` keeps the real reason in
`error.cause`, and pi dropped it, so the user had nothing to go on.

`LlamaClient` now sends its requests, including the `/models/sse` watch,
through a small `fetchServer` helper. When `fetch` fails at the network
level, the helper throws `Could not reach llama.cpp server at <url>:
<cause>` and keeps the original error as `cause`. Aborts and timeouts
are rethrown unchanged. HTTP error responses are handled as before.

The same message also improves `/llama` and the model catalog refresh,
because they use the same client.

**Testing**
- Added a test in `test/llama-extension.test.ts` that calls
`LlamaClient.list()` against a closed local port and expects the server
URL and `connect ECONNREFUSED ...` in the error. It fails without the
change (`got 'fetch failed'`) and passes with it. The whole file passes
(16 tests).
- `npm run check` passes.
- Manual check, `/login llama.cpp` with `http://127.0.0.1:8099` (nothing
listening):
  - Before: `Error: Failed to save API key for llama.cpp: fetch failed`
- After: `Error: Failed to save API key for llama.cpp: Could not reach
llama.cpp server at http://127.0.0.1:8099: connect ECONNREFUSED
127.0.0.1:8099`

Fixes #11745

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-04 09:32:51 +01:00
Petr Houška
676af1d777
fix(coding-agent): find and ls limit edge cases (#11747)
Some checks failed
CI / build-check (push) Waiting to run
CI / test (coding-agent) (push) Waiting to run
CI / test (durable) (push) Waiting to run
CI / test (other) (push) Waiting to run
CI / mcp-conformance (push) Waiting to run
Environments / daemons (push) Has been cancelled
Environments / tests (macos-latest) (push) Has been cancelled
Environments / tests (ubuntu-24.04-arm) (push) Has been cancelled
Environments / tests (ubuntu-latest) (push) Has been cancelled
Environments / tests (windows-11-arm) (push) Has been cancelled
Environments / tests (windows-latest) (push) Has been cancelled
Environments / check (push) Has been cancelled
Environments / release-daemons (darwin-arm64, macos-latest) (push) Has been cancelled
Environments / release-daemons (darwin-x64, macos-latest) (push) Has been cancelled
Environments / release-daemons (linux-arm64, ubuntu-24.04-arm) (push) Has been cancelled
Environments / release-daemons (linux-x64, ubuntu-latest) (push) Has been cancelled
Environments / release-daemons (windows-arm64, windows-11-arm) (push) Has been cancelled
Environments / release-daemons (windows-x64, windows-latest) (push) Has been cancelled
Three limit problems in the find and ls tools.

**1. `limit: 0` and other values below 1.** find passed `limit` to fd
unchanged. fd treats `--max-results 0` as no limit, so `find({ pattern:
"*", limit: 0 })` returned every match, followed by `[0 results limit
reached. Use limit=0 for more, or refine pattern]`. ls with `limit: 0`
listed nothing and printed `(empty directory)`. A fractional limit such
as `1.5` printed `1.5 results limit reached. Use limit=3`.

Fix: both tools use `Math.max(1, Math.floor(limit ?? DEFAULT_LIMIT))`.
grep already clamps to at least 1. The fd path and the custom `glob`
path share this value.

**2. Notice at exactly N results (#7121).** With 3 matching files and
`limit: 3`, fd returned 3 results. find checked `results >= limit` and
printed `[3 results limit reached. Use limit=6 for more, or refine
pattern]`, even though there were no more files.

Fix: find asks fd for `limit + 1` results, shows the first `limit`, and
prints the notice only when the extra result exists. ls already worked
this way. Custom `FindOperations.glob` now also gets `limit + 1`. The
tool shows at most `limit` of the returned results and prints the notice
only when more were returned. The interface is unchanged. This follows
closed PR #7122 without its unrelated changes.

**3. Results past the 50KB cap.** find kept every fd line in memory.
`truncateHead` then dropped everything past 50KB. find now handles each
line as it arrives. It keeps lines up to the first one past the cap and
only counts the rest, as grep does in #11705. Because that first line
past the cap is kept, `truncateHead` makes the same truncation decision
as before. `details.truncation.totalLines` and `totalBytes` come from
the counts, so the notices and `details` are unchanged.

Behavior differences:
- A custom `glob` that ignores `options.limit` and returns more results
used to have all of them shown. Now the tool shows the first `limit`
results with the limit notice.
- Custom `glob` implementations get `limit + 1` instead of `limit`.
- In the fd path, output made only of blank lines with exit code 0 now
gives `No files found matching pattern` instead of empty text. fd does
not produce such output.

**Testing**
- New tests in `test/tools.test.ts`:
- find: exactly N matches gives no notice, and N+1 matches give the
notice with N lines (#7121). Fails on main.
- find: `limit` 0, -5, 0.5 and 1.9 each show 1 result with the `1
results` notice. Fails on main.
- find with custom `glob`: the operation gets `limit + 1`, and exact and
over-limit results give the right output. Fails on main.
- find past the byte cap: 300 names of 200 bytes each.
`details.truncation` equals `truncateHead` of the full output, except
for `content`. Passes on main too; it guards the counting. It fails if
the `totalLines` override is removed.
- ls: `limit` 0, -5, 0.5 and 1.9 each list 1 entry with the notice.
Fails on main.
- ls: exactly N entries gives no notice. Passes on main too; it guards
the existing behavior.
- `test/tools.test.ts` and the find regression suites 3302, 3303 and
6104 pass (110 tests). `npm run check` passes.

fixes #7121

This PR is AI-generated.
2026-11-03 23:56: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]
efb45b51d4
fix(coding-agent): dispose session runtime after startup benchmark (#11736)
With `PI_STARTUP_BENCHMARK=1`, pi initializes interactive mode, stops
the TUI, prints the timings and returns. It never disposed the session
runtime, so extensions never got `session_shutdown`. If an extension
cleans up a timer or a socket in that handler, the resource stays open
and the process never exits after printing the timings.

This change calls `await runtime.dispose()` right after
`interactiveMode.stop()` in the benchmark path. That is the same order a
normal interactive quit uses: stop the TUI, then dispose the runtime.

No test was added. Nothing in the test suite covers the startup
benchmark path, and that path needs a real TTY.

Testing:
- `npm run check` passes.
- I wrote an extension that starts a `setInterval` in `session_start`
and clears it in `session_shutdown`. Then I ran `PI_STARTUP_BENCHMARK=1
PI_TIMING=1 pi --offline -ne -e ./ext.ts` in a terminal, wrapped in
`timeout --foreground 15`:
- before the change: the timings print, then the process hangs until the
timeout (exit 124);
  - after the change: it exits 0.
- Without extensions it exits 0 both before and after.

Fixes #11693

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
2026-11-03 22:11:40 +01:00
Petr Houška
9a19539617
fix(coding-agent): ignore prerelease suffix of last seen changelog version (#11753)
Fixes #11703.

**Problem:** `getNewEntries()` parsed the saved version with
`split(".")`. `0.1.5-20261020.7` became `["0", "1", "5-20261020", "7"]`,
`Number("5-20261020")` is `NaN`, and `|| 0` turned it into `0.1.0`. The
`[0.1.1]` to `[0.1.5]` notes counted as new, were shown, and the same
prerelease string was saved again, so they repeated on every fresh
launch.

**Fix:** read only the leading `major.minor.patch` of the saved version,
the same way changelog headings are parsed. `0.1.5-20261020.7` and
`0.1.5+build.3` now count as `0.1.5`.

If the saved version has no `major.minor.patch` (for example `latest`),
nothing is shown and the installed version is saved in its place, like
on a fresh install. Before, it was read as `0.0.0` and the whole
changelog was shown. Saving the installed version matters because
callers only save when there are new entries, so an unreadable value
would otherwise silence the changelog for good.

Alternatives considered (noted in a code comment):
- plain string comparison: orders `0.10.0` before `0.9.0`
- `localeCompare` with `numeric: true`: works only because `0.1.5-x`
happens to sort after `0.1.5`, and depends on locale collation
- strict semver: orders `0.1.5-x` before `0.1.5`, so the `[0.1.5]` notes
would still repeat forever

Tests added in `test/changelog.test.ts` and
`test/interactive-mode-extension-changelog.test.ts`. `npm run check`
passes.
2026-11-03 21:59:05 +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
Armin Ronacher
797e122cb1 fix(mcp): refresh with the app's redirect URI, cancel sign-in during the lock wait
Tokens from a Client ID Metadata Document sign-in through an https
callback route were refreshed with the loopback redirect URI, which the
documents do not list, so every expiry needed a browser sign-in.
createMcpAuthProvider takes the app's redirectUrl and startMcpSignIn
passes its own.

startMcpSignIn's wait for the refresh lock now ends when the signal
aborts. Only McpRefreshLockBusyError (coding-agent maps proper-lockfile's
ELOCKED to it) falls back to the browser step; other lock errors fail
the sign-in.
2026-11-03 14:17:21 +01:00
Armin Ronacher
76c040a2ad feat(mcp): shared tool naming, schema, output limit and config helpers
createMcpToolName, mcpNamespace, normalizeMcpInputSchema,
limitMcpContent (with a caller-provided saver) and validation of the
standard mcpServers shape move from coding-agent into pi-mcp, so other
applications name and limit MCP tools exactly like pi. Applications
validate their own fields on the tagged result; coding-agent keeps
exposure, auth.provider and its Client ID Metadata Document rule.
2026-11-03 14:17:21 +01:00
Armin Ronacher
bb266bf359 feat(mcp): MCP OAuth sign-in in two halves, moved from coding-agent
pi-mcp now has startMcpSignIn and completeMcpSignIn, so a server app can
start a sign-in in one request and complete it in another (a callback
route, a pasted redirect URL, or a loopback server via
listenForLoopbackSignIn/waitForLoopbackSignIn), plus createMcpAuthProvider.
Application specifics (client name, Client ID Metadata Document base URL
and listed redirect URIs) are options. coding-agent's signInMcpServer is a
thin wrapper over them.

authorizeMcp gains refreshOnly: a failed refresh no longer registers a
client or replaces a pending sign-in's PKCE verifier and state. Sign-in
refreshes with the stored client before dropping it for another redirect
URI.
2026-11-03 14:17:21 +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
Mario Zechner
ed4f488401 feat(chord): remove Chord v1 (/old, /old/node), its tests and docs
p2 runs on Chord v2; apps still on v1 pin @earendil-works/chord@1.5.0.
2026-11-03 12:11:22 +00:00
Mario Zechner
e9c0ff4d84 feat(chord): Chord v2 is the package's root
src/next is src (the main export), src/next-node is src/node with short subpaths (/http, /workers, /plugins,
/plugins/runtime, /app, /surfaces, /web, /react, /tui, /tui/launcher); the foundations get /json and /watch;
Chord v1 moves to src/old (/old, /old/node) until p2 runs on v2; tests and docs move likewise (test/, docs/,
test/old, docs/old). Durable and env import the foundations from their subpaths.
2026-11-02 22:36:55 +00:00
Petr Houška
806cdd68e3
fix(coding-agent): only report packages as up to date when pi update checked them (#11711)
## Problem

#11707 prints `Packages are already up to date` whenever no package
changed. In two cases that claim is false.

**1. npm left the old version installed.** Extension updates install
`@latest` or the configured range. If npm resolves stale registry
metadata, it reinstalls the old version and exits 0. The existing check
warns, then the summary contradicts it:

```
Updating npm:foo...
npm:foo is still 1.2.0 (registry reports 1.3.0); npm registry metadata may be stale, try again in a few minutes
Packages are already up to date
```

**2. Offline mode.** With `--offline` or `PI_OFFLINE=1`,
`updateConfiguredSources()` returns before checking anything, and the
CLI still prints `Packages are already up to date`.

## Change

The CLI only prints "already up to date" when the update ran and printed
no warning:

| Situation | Output |
|---|---|
| Packages changed | `Updated N packages:` and the list (unchanged) |
| Nothing changed, no warnings | `Packages are already up to date`
(unchanged) |
| Nothing changed, a warning was printed | `No packages were updated,
see the warnings above` (or `<source> was not updated, ...`) |
| Offline mode | `Skipped package updates (offline mode)` |

The CLI counts `progress` update events, which are always warnings, and
does not try to map them to packages. That is deliberate: the git
warning about untracked files that could not be cleaned is printed even
though the commit did change, so a warning does not mean "this package
was not updated".

In offline mode, `update()` still runs, so `pi update <unknown source>`
still fails with "No matching package". `isOfflineModeEnabled()` is now
exported from `core/package-manager.ts` (not from the package index) so
the CLI uses the same check.

The changelog entry from #11707 is adjusted, since it is unreleased.

## Tests

`test/package-command-paths.test.ts`, both through `main()`:

- npm `view` reports 1.3.0 but `install` leaves 1.2.0: the
stale-metadata warning is printed, then `No packages were updated, see
the warnings above`, and no "up to date".
- `PI_OFFLINE=1`: `Skipped package updates (offline mode)`, no "up to
date".

Both fail without the fix. `cycles project package overrides in config
local mode`, `prints a pnpm metadata hint when self-update fails` and
`should expand ~ in exclusion and exact patterns` fail locally on
Windows, on `origin/main` as well.
2026-11-02 21:49:07 +01:00
Petr Houška
342514bdf8
feat(coding-agent): list updated packages with version changes in pi update (#11707)
## Problem

`pi update`, `pi update --all` and `pi update --extensions` print only
`Updating <source>...` progress lines and a final `Updated packages`.
Users cannot see which extensions changed or to which version. Several
outdated npm packages in the same scope are installed as one batch, so
the progress output does not even name them.

## Change

`pi update` now prints a summary of what actually changed:

```
Updated 2 packages:
  npm:foo 1.2.0 -> 1.3.0
  git:github.com/user/bar (project) abc1234 -> def5678
```

When nothing changed it prints `Packages are already up to date` (or
`<source> is already up to date`).

- npm: compares the installed `package.json` version before and after
the batch install. The "before" version comes from the existing
`shouldUpdateNpmSource` check, so this adds no extra registry calls.
- git: compares `git rev-parse HEAD` before and after `updateGit`.
Commits are shortened to 7 characters.
- Packages whose version or commit did not change are omitted, including
npm installs that hit stale registry metadata (those already get the
existing warning).

## API

`PackageManager.update()` still returns `Promise<void>`.
`ProgressEvent.type` gains a `"result"` member. Each changed package is
reported through the existing progress callback as:

```ts
{ type: "result", action: "update", source: "npm:foo", message: "npm:foo 1.2.0 -> 1.3.0" }
```

The CLI collects these messages to print the summary. A separate type
keeps the existing contract that each `start` is followed by exactly one
`complete` or `error` for the same source. Reusing `complete` would have
produced a second `complete` for git packages and a `complete` without a
`start` for batched npm packages. The only compatibility cost is for
TypeScript consumers with an exhaustive `switch` on `event.type`.

## Tests

- `test/package-manager.test.ts`: npm update emits `start`, `complete`,
then a `result` with `npm:example@^1.0.0 (project) 1.0.0 -> 1.2.0`; an
up-to-date package emits no result.
- `test/git-update.test.ts`: git update emits a result with the old and
new commit; an up-to-date clone emits no result.

`should expand ~ in exclusion and exact patterns` fails locally on
Windows. It tests path resolution and is unrelated to this change.
2026-11-02 21:21:23 +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
Vegard Stikbakke
62752fd4c3
fix(coding-agent): serve catalogs without version redirect (#11690)
Update the model catalog protocol to pi.dev after a change to stop
pi.dev redirecting released clients to `?pi-version=` URLs and serve
their catalog directly. The released CLI is unaffected.
2026-11-02 14:42:17 +01:00
Armin Ronacher
db6c084455 Add [Unreleased] section for next cycle 2026-11-02 11:38:21 +01:00
Armin Ronacher
f9a2636556 Release v1.5.0 2026-11-02 11:38:21 +01:00
Armin Ronacher
7e7c98a0f5 docs(coding-agent): note branch summary context edits 2026-11-02 11:38:20 +01:00
Armin Ronacher
ddbe02cd20 docs: audit changelogs and add release highlights 2026-11-02 11:03:52 +01:00
robopipi[bot]
d0fe213089
fix(coding-agent): apply context edits to branch summary input (#11680)
When a partial assistant reply fails and automatic retry succeeds, pi
appends a `context_edit` that hides the failed attempt from model
context. Branch summarization (`/tree` with summarize) built its input
from raw session entries through `getMessageFromEntry`, which ignored
`context_edit` entries. As a result, the summarizer received the hidden
attempt again. Content replacements were ignored the same way.

What the change does:
- `prepareBranchEntries` now collects the latest `context_edit` for each
target in the branch.
- It applies those edits to the entries it converts, using the same
`projectContextEntry` logic as the live context and compaction:
  - an omission (`replacement: null`) drops the entry;
  - a replacement swaps its content.
- `projectContextEntry` is now exported from `session-manager.ts` so
both paths share it.
- The existing branch-summary behavior is unchanged: tool results are
still skipped, and compaction or branch-summary entries still become
summary messages.

Testing:
- Added `applies context edits to branch entries` to
`test/branch-summarization.test.ts`. It covers one omitted assistant
entry and one replaced assistant entry. The test fails without the
change and passes with it.
- Ran `branch-summarization`, `branch-summary-extensions`,
`agent-session-branching` and the compaction tests: all pass.
- `npm run check`: biome and the coding-agent checks pass. `tsc` reports
6 errors in `packages/ai` model-catalog tests, and the same 6 errors
occur on unmodified `main`.
- End-to-end reproduction with the faux provider and `AgentSession`: a
failed attempt, a successful retry, then `navigateTree(target, {
summarize: true })`. Before the change, the summary request contained
the failed attempt's text. After the change, it contains only the
successful retry.

Fixes #11602

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
2026-11-02 10:55:30 +01:00
Armin Ronacher
0f21a3319d fix(coding-agent,tui): reuse syntax highlighting and rendered Markdown blocks
Codemode and write rows keep their last highlighting in a CodeHighlighter
that checks theme and grammar changes itself. Markdown reuses the top-level
blocks of its last render, held weakly, and assistant messages reuse their
Markdown instances while streaming. Text.setText() keeps its wrapped lines
for unchanged text.

closes #10971
2026-11-01 14:40:56 +01:00
Mario Zechner
6291b9bcc3 feat(agent): pi-schema's Static types TypeBox-built schemas too, without depending on TypeBox 2026-11-01 11:12:44 +00: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
e3da248ea9
fix(coding-agent): keep updating git packages when git clean fails (#11648)
Closes #11597

## Problem

On Windows, `pi update` stopped with `git clean -fdx failed with code 1`
for git extensions such as pi-lens.

1. Another pi session has the extension loaded, so a native module
(`@ast-grep/napi` `.node`) or `ast-grep.exe` is open in that process.
2. npm renames the in-use package dir to
`node_modules/@ast-grep/.napi-win32-x64-msvc-<hash>`. Windows allows the
rename, but npm cannot delete the dir afterwards.
3. The next `pi update` runs `git reset --hard`, then `git clean -fdx`.
git prints `warning: failed to remove ...: Invalid argument` and exits
1.
4. pi threw, leaving the checkout at the new commit with the old
dependencies.

The suggested `-ffdx` does not help. Reproduced locally (git 2.52,
Windows): with a running `.exe`, or a `.node` loaded by another node
process and its dir renamed, both `-fdx` and `-ffdx` fail with `Invalid
argument`. Once the process exits, `-fdx` succeeds.

## Fix

In `cleanAndInstallGitDependencies`, a failed `git clean` no longer
aborts the update. pi:

- prints a warning that files may be in use by another pi session
- still runs `npm install`, so dependencies match the new commit
- keeps the `.pi-update-incomplete` marker, so the next update retries
the clean

## Tests

- `test/git-update.test.ts`: new regression test. A failing clean still
updates and installs dependencies, warns, and keeps the marker. The next
update cleans and removes the marker.
- `test/package-manager.test.ts`: updated the test that expected the
throw.

`npm run check` reports `install-lock is out of date`, and
`package-manager.test.ts > should expand ~ in exclusion and exact
patterns` fails on Windows. Both also happen on `main` without this
change.
2026-10-31 18:19:44 +01: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