diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 15211d521..4ddc80f14 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -363,3 +363,13 @@ pablasso pr bilby91 pr giannisCKS pr + +Panoplos pr + +haoyongchun1125-maker pr + +gwokhou pr + +gaoyk19 pr + +cad0p pr diff --git a/package-lock.json b/package-lock.json index 98f077553..5bf2331af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4015,9 +4015,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.17", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -5388,11 +5388,11 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-telemetry": "^0.84.2", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", @@ -5434,12 +5434,12 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", @@ -5480,10 +5480,10 @@ }, "packages/client": { "name": "@earendil-works/pi-client", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-protocol": "^0.84.1" + "@earendil-works/pi-protocol": "^0.84.2" }, "devDependencies": { "shx": "0.4.0", @@ -5495,15 +5495,14 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-client": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1", - "@earendil-works/pi-server": "^0.84.1", - "@earendil-works/pi-tui": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-client": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2", + "@earendil-works/pi-tui": "^0.84.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5545,32 +5544,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.84.1" + "version": "0.84.2" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.14.1", + "version": "1.14.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { "ms": "2.1.3" }, @@ -5641,10 +5640,10 @@ }, "packages/evals": { "name": "@earendil-works/pi-evals", - "version": "0.84.1", + "version": "0.84.2", "devDependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-coding-agent": "^0.84.1", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-coding-agent": "^0.84.2", "@types/node": "24.12.4", "shx": "0.4.0", "typescript": "5.9.3", @@ -5671,10 +5670,10 @@ }, "packages/protocol": { "name": "@earendil-works/pi-protocol", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", "typebox": "1.3.7" }, "devDependencies": { @@ -5687,11 +5686,11 @@ }, "packages/server": { "name": "@earendil-works/pi-server", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1" + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2" }, "devDependencies": { "shx": "0.4.0", @@ -5703,11 +5702,11 @@ }, "packages/session-backends/sqlite-node": { "name": "@earendil-works/pi-session-backend-sqlite-node", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-ai": "^0.84.1" + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2" }, "devDependencies": { "@vitest/coverage-v8": "4.1.9", @@ -5719,7 +5718,7 @@ }, "packages/telemetry": { "name": "@earendil-works/pi-telemetry", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "devDependencies": { "@types/node": "24.12.4", @@ -5748,7 +5747,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 7bd04be3a..679110f18 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,13 +2,7 @@ ## [Unreleased] -### Breaking Changes - -- Replaced the unfinished harness runtime and record-log session model with the shared entry/register/usage, operation-state, session, search, and public harness contracts specified for the durable harness. - -### Added - -- Added isolated harness execution primitives for assistant streaming and durable-boundary tool phases. +## [0.84.2] - 2026-08-14 ### Fixed diff --git a/packages/agent/package.json b/packages/agent/package.json index 3f79af988..4dc173d31 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.84.1", + "version": "0.84.2", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -39,8 +39,8 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-telemetry": "^0.84.2", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 87ab7b685..182e1d8a3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + +### Added + +- Added `createGatewayBindingFetch()` for routing Cloudflare AI Gateway requests through a Workers AI binding without an API token ([#7901](https://github.com/earendil-works/pi/pull/7901) by [@Maximo-Guk](https://github.com/Maximo-Guk)). +- Added `AssistantMessage.endTurn` to preserve OpenAI Codex's terminal `end_turn` signal for diagnostics ([#7766](https://github.com/earendil-works/pi/pull/7766)). + ### Changed - Changed Kimi Coding requests to use pi's runtime `User-Agent` header. @@ -15,6 +22,9 @@ - Fixed upstream request buffer limit failures to trigger automatic assistant retries. - Fixed OpenAI Responses function and custom tool calls to preserve namespaces during streaming, proxying, and replay ([#7709](https://github.com/earendil-works/pi/issues/7709)). - Fixed built-in and custom DeepSeek API models to send output limits through the supported `max_tokens` field. +- Fixed Google Generative AI and Vertex AI responses with tool calls incorrectly treating output-limit or provider-error stops as normal tool use ([#8059](https://github.com/earendil-works/pi/issues/8059)). +- Fixed Amazon Bedrock replay rejecting tool arguments that contain empty object keys while preserving all valid nested values ([#7882](https://github.com/earendil-works/pi/pull/7882) by [@muyiyr](https://github.com/muyiyr)). +- Fixed DeepSeek compatibility detection for base URLs whose hostname contains uppercase letters ([#7933](https://github.com/earendil-works/pi/pull/7933) by [@yearth](https://github.com/yearth)). ## [0.84.1] - 2026-08-07 diff --git a/packages/ai/package.json b/packages/ai/package.json index 552876250..fe1f336d8 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.84.1", + "version": "0.84.2", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", @@ -62,7 +62,7 @@ "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", diff --git a/packages/ai/src/api/google-generative-ai.ts b/packages/ai/src/api/google-generative-ai.ts index 30da47cd8..b0c5a09ed 100644 --- a/packages/ai/src/api/google-generative-ai.ts +++ b/packages/ai/src/api/google-generative-ai.ts @@ -215,7 +215,7 @@ export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = ( if (candidate?.finishReason) { output.rawStopReason = candidate.finishReason; output.stopReason = mapStopReason(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { + if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") { output.stopReason = "toolUse"; } } diff --git a/packages/ai/src/api/google-vertex.ts b/packages/ai/src/api/google-vertex.ts index 5bb90663e..17ea6c03c 100644 --- a/packages/ai/src/api/google-vertex.ts +++ b/packages/ai/src/api/google-vertex.ts @@ -232,7 +232,7 @@ export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = ( if (candidate?.finishReason) { output.rawStopReason = candidate.finishReason; output.stopReason = mapStopReason(candidate.finishReason); - if (output.content.some((b) => b.type === "toolCall")) { + if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") { output.stopReason = "toolUse"; } } diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 267989728..80bb8f77d 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -80,6 +80,36 @@ export const IMAGE_MODELS = { cacheWrite: 0, }, } satisfies ImagesModel<"openrouter-images">, + "bytedance-seed/seedream-5-0-lite": { + id: "bytedance-seed/seedream-5-0-lite", + name: "ByteDance Seed: Seedream 5.0 Lite", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "bytedance-seed/seedream-5-0-pro": { + id: "bytedance-seed/seedream-5-0-pro", + name: "ByteDance Seed: Seedream 5.0 Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-2.5-flash-image": { id: "google/gemini-2.5-flash-image", name: "Google: Nano Banana (Gemini 2.5 Flash Image)", @@ -620,6 +650,21 @@ export const IMAGE_MODELS = { cacheWrite: 0, }, } satisfies ImagesModel<"openrouter-images">, + "x-ai/grok-imagine-image-2.0": { + id: "x-ai/grok-imagine-image-2.0", + name: "xAI: Grok Imagine Image 2.0", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "x-ai/grok-imagine-image-quality": { id: "x-ai/grok-imagine-image-quality", name: "SpaceXAI: Grok Imagine Image Quality", diff --git a/packages/ai/test/google-raw-stop-reason.test.ts b/packages/ai/test/google-raw-stop-reason.test.ts index b485088ea..ccd85b0a4 100644 --- a/packages/ai/test/google-raw-stop-reason.test.ts +++ b/packages/ai/test/google-raw-stop-reason.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; const googleGenAiMock = vi.hoisted(() => ({ finishReason: "MALFORMED_FUNCTION_CALL", + includeFunctionCall: false, })); vi.mock("@google/genai", () => { @@ -13,6 +14,19 @@ vi.mock("@google/genai", () => { candidates: [ { finishReason: googleGenAiMock.finishReason, + ...(googleGenAiMock.includeFunctionCall && { + content: { + parts: [ + { + functionCall: { + id: "call-1", + name: "echo", + args: { value: "truncated" }, + }, + }, + ], + }, + }), }, ], usageMetadata: { @@ -77,6 +91,7 @@ const context: Context = { describe("Google raw stop reasons", () => { it("preserves raw Gemini finish reasons for Google Generative AI errors", async () => { googleGenAiMock.finishReason = "MALFORMED_FUNCTION_CALL"; + googleGenAiMock.includeFunctionCall = false; const stream = streamGoogleGenerativeAi(getModel("google", "gemini-2.5-flash"), context, { apiKey: "test-api-key", @@ -91,6 +106,7 @@ describe("Google raw stop reasons", () => { it("preserves raw Gemini finish reasons for Google Vertex errors", async () => { googleGenAiMock.finishReason = "SAFETY"; + googleGenAiMock.includeFunctionCall = false; const stream = streamGoogleVertex(getModel("google-vertex", "gemini-3-flash-preview"), context, { project: "test-project", @@ -103,4 +119,44 @@ describe("Google raw stop reasons", () => { expect(message.rawStopReason).toBe("SAFETY"); expect(message.errorMessage).toBe("Provider stopped with: SAFETY"); }); + + const adapters = [ + { + name: "Google Generative AI", + createStream: () => + streamGoogleGenerativeAi(getModel("google", "gemini-2.5-flash"), context, { + apiKey: "test-api-key", + }), + }, + { + name: "Google Vertex", + createStream: () => + streamGoogleVertex(getModel("google-vertex", "gemini-3-flash-preview"), context, { + project: "test-project", + location: "us-central1", + }), + }, + ]; + + it.each(adapters)("preserves MAX_TOKENS with a tool call as length for $name", async ({ createStream }) => { + googleGenAiMock.finishReason = "MAX_TOKENS"; + googleGenAiMock.includeFunctionCall = true; + + const message = await createStream().result(); + + expect(message.stopReason).toBe("length"); + expect(message.rawStopReason).toBe("MAX_TOKENS"); + expect(message.content.some((block) => block.type === "toolCall")).toBe(true); + }); + + it.each(adapters)("maps STOP with a tool call to toolUse for $name", async ({ createStream }) => { + googleGenAiMock.finishReason = "STOP"; + googleGenAiMock.includeFunctionCall = true; + + const message = await createStream().result(); + + expect(message.stopReason).toBe("toolUse"); + expect(message.rawStopReason).toBe("STOP"); + expect(message.content.some((block) => block.type === "toolCall")).toBe(true); + }); }); diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 396ee220b..49ecd548f 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + ## [0.84.1] - 2026-08-07 ## [0.84.0] - 2026-08-06 diff --git a/packages/client/package.json b/packages/client/package.json index 81efdead8..45d5a103f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-client", - "version": "0.84.1", + "version": "0.84.2", "description": "Transport-neutral client for remote pi sessions over framed CBOR bytes", "type": "module", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-protocol": "^0.84.1" + "@earendil-works/pi-protocol": "^0.84.2" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5d16436f4..0b7de0847 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + +### New Features + +- **Fullscreen transcript search** — Search and navigate matches in fullscreen mode. See [TUI Fullscreen Viewport](docs/keybindings.md#tui-fullscreen-viewport). +- **Configurable default tools** — Choose startup built-in tools globally or per project. See [Tools](docs/settings.md#tools). +- **Configurable fullscreen exit output** — Print the transcript or only a resume hint on exit. See [Interactive Mode](docs/usage.md#interactive-mode). + ### Added - Added fullscreen transcript search with `Ctrl+Shift+F`, incremental match highlighting, configurable search match theme colors, and next/previous navigation with `Enter`/`Ctrl+G` and `Shift+Enter`/`Ctrl+Shift+G`. @@ -9,12 +17,18 @@ - Added a fullscreen exit output setting to choose between printing the final transcript and only a session resume hint. - Added the `defaultTools` setting for configuring the initial built-in tool selection globally or per project. - Added `--use-theme ` to choose an initial per-run interactive theme without changing saved settings ([#7722](https://github.com/earendil-works/pi/pull/7722) by [@rwachtler](https://github.com/rwachtler)). +- Added `expandPromptTemplates` to extension `pi.sendUserMessage()` options for explicitly dispatching commands and expanding skills and prompt templates. See [`pi.sendUserMessage()`](docs/extensions.md#pisendusermessagecontent-options) ([#7857](https://github.com/earendil-works/pi/pull/7857) by [@mrexodia](https://github.com/mrexodia)). +- Added inherited `createGatewayBindingFetch()` for routing Cloudflare AI Gateway requests through a Workers AI binding without an API token ([#7901](https://github.com/earendil-works/pi/pull/7901) by [@Maximo-Guk](https://github.com/Maximo-Guk)). +- Added inherited `AssistantMessage.endTurn` to preserve OpenAI Codex's terminal `end_turn` signal for diagnostics ([#7766](https://github.com/earendil-works/pi/pull/7766)). +- Added inherited unbound single-line transcript scrolling actions for fullscreen mode. See [TUI Fullscreen Viewport](docs/keybindings.md#tui-fullscreen-viewport) ([#7903](https://github.com/earendil-works/pi/pull/7903) by [@midastruth](https://github.com/midastruth)). ### Changed - Changed inherited Kimi Coding requests to use pi's runtime `User-Agent` header. - Replaced the inherited Mistral SDK transport with a native Chat Completions HTTP stream, eliminating its generated client and schema runtime overhead. - Documented the generic `AI_AGENT=pi` process marker and how it differs from `PI_CODING_AGENT=true` ([#7747](https://github.com/earendil-works/pi/issues/7747)). +- Changed inherited OpenAI Responses deferred tool loading to prefer message-anchored `additional_tools` where supported while retaining tool-search and top-level fallbacks ([#7709](https://github.com/earendil-works/pi/issues/7709)). +- Reduced inherited fullscreen rendering allocation churn by painting full-width layout rows directly instead of recompositing them on every frame. ### Fixed @@ -23,6 +37,26 @@ - Fixed inherited GitHub Copilot login triggering API rate limits while enabling model policies by limiting concurrent policy updates ([#6187](https://github.com/earendil-works/pi/issues/6187)). - Fixed fullscreen transcript search snapping back to the current match during manual scrolling and fragmented mouse input leaking into the search query. - Fixed inherited required LaTeX arguments starting on a new line being parsed as empty ([#7760](https://github.com/earendil-works/pi/issues/7760)). +- Updated the transitive `nanoid` development dependency to address a denial-of-service vulnerability. +- Fixed fallback rendering for extension tool results to collapse long output and honor tool expansion ([#7979](https://github.com/earendil-works/pi/issues/7979)). +- Fixed JSON and RPC `message_update` events dropping cumulative usage during streaming. See [JSON Event Mode](docs/json.md) and [RPC `message_update`](docs/rpc.md#message_update-streaming) ([#7982](https://github.com/earendil-works/pi/pull/7982) by [@christianklotz](https://github.com/christianklotz)). +- Fixed `pi.sendMessage(..., { triggerTurn: false })` steering an active run instead of only recording the custom message ([#8022](https://github.com/earendil-works/pi/pull/8022) by [@cristinaponcela](https://github.com/cristinaponcela)). +- Fixed the `defaultTools` setting dropping extension and SDK custom tools when selecting built-in defaults. +- Fixed the subagent example rejecting YAML array syntax for the `tools` frontmatter field ([#7598](https://github.com/earendil-works/pi/pull/7598) by [@alexsavio](https://github.com/alexsavio)). +- Fixed the subagent example dropping parent session model, thinking, and tool configuration ([#7897](https://github.com/earendil-works/pi/pull/7897) by [@virtuald](https://github.com/virtuald)). +- Fixed custom system prompts concatenating the current working directory with later appended prompt content ([#7887](https://github.com/earendil-works/pi/pull/7887) by [@distributedlock](https://github.com/distributedlock)). +- Fixed inherited OpenAI Responses function and custom tool calls losing namespaces during streaming, proxying, and replay ([#7709](https://github.com/earendil-works/pi/issues/7709)). +- Fixed inherited upstream request buffer failures not triggering automatic assistant retries. +- Fixed inherited built-in and custom DeepSeek API models sending output limits through an unsupported field. +- Fixed inherited Amazon Bedrock replay rejecting tool arguments that contain empty object keys while preserving all valid nested values ([#7882](https://github.com/earendil-works/pi/pull/7882) by [@muyiyr](https://github.com/muyiyr)). +- Fixed inherited DeepSeek compatibility detection for base URLs whose hostname contains uppercase letters ([#7933](https://github.com/earendil-works/pi/pull/7933) by [@yearth](https://github.com/yearth)). +- Fixed inherited Google Generative AI and Vertex AI responses with tool calls incorrectly treating output-limit or provider-error stops as normal tool use ([#8059](https://github.com/earendil-works/pi/issues/8059)). +- Fixed inherited fullscreen mouse drag selection and OSC 8 link activation in terminals that report generic SGR mouse release button codes ([#7963](https://github.com/earendil-works/pi/issues/7963)). +- Fixed inherited focused fullscreen overlays not receiving mouse wheel or viewport scroll keys such as PageUp and PageDown ([#7894](https://github.com/earendil-works/pi/issues/7894)). +- Fixed inherited LaTeX control spaces split across line endings causing complete expressions to fall back to raw source. +- Fixed split `Alt+Enter` input over SSH being misread as Escape, added `PI_TUI_ESC_TIMEOUT` for high-latency terminals, and limited that timeout to lone Escape input ([#7899](https://github.com/earendil-works/pi/pull/7899) by [@powerfooI](https://github.com/powerfooI)). +- Fixed inherited idle fullscreen sessions repainting and clearing text selection when the terminal loses focus ([#7892](https://github.com/earendil-works/pi/pull/7892) by [@terrorobe](https://github.com/terrorobe)). +- Fixed fullscreen selection copy to use the host clipboard and report failure instead of claiming success when OSC 52 is unsupported ([#8110](https://github.com/earendil-works/pi/pull/8110) by [@Panoplos](https://github.com/Panoplos)). ## [0.84.1] - 2026-08-07 diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 40ac536ed..272f0fcb7 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.84.1", + "version": "0.84.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index e4760cc11..7c8231631 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.84.1", + "version": "0.84.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index a13a3a8cb..9a8e14ff5 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.84.1", + "version": "0.84.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 5ba0c2d84..df80c87fd 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.84.1", + "version": "0.84.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 83019c1d3..450c74f69 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.84.1", + "version": "0.84.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 450c0460d..22d485114 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.14.1", + "version": "1.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.14.1", + "version": "1.14.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 7e028f284..57ba99af5 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.14.1", + "version": "1.14.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/subagent/agents.ts b/packages/coding-agent/examples/extensions/subagent/agents.ts index c41ef579c..b8a36598c 100644 --- a/packages/coding-agent/examples/extensions/subagent/agents.ts +++ b/packages/coding-agent/examples/extensions/subagent/agents.ts @@ -23,6 +23,42 @@ export interface AgentDiscoveryResult { projectAgentsDir: string | null; } +/** + * Raw agent frontmatter. Values are `unknown` because `parseFrontmatter` runs a + * real YAML parser, so any scalar or collection can appear here. + * + * A type alias rather than an interface: `parseFrontmatter` constrains its + * parameter to `Record`, and only an alias picks up the + * implicit index signature that satisfies it. + */ +type AgentFrontmatter = { + name?: unknown; + description?: unknown; + tools?: unknown; + model?: unknown; +}; + +/** + * Normalize a frontmatter `tools` value to a list of tool names. + * + * Both spellings are valid YAML and both are in use: + * + * tools: read, bash # string + * tools: [read, bash] # array + * + * so accept either. Anything else (a number, a map, a nested list) yields no + * tools rather than throwing: this runs inside agent discovery, where a single + * bad file must not take down every other agent in the same directory. + */ +function parseToolList(value: unknown): string[] | undefined { + const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : []; + const tools = raw + .filter((t): t is string => typeof t === "string") + .map((t) => t.trim()) + .filter(Boolean); + return tools.length > 0 ? tools : undefined; +} + function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] { const agents: AgentConfig[] = []; @@ -49,22 +85,17 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig continue; } - const { frontmatter, body } = parseFrontmatter>(content); + const { frontmatter, body } = parseFrontmatter(content); - if (!frontmatter.name || !frontmatter.description) { + if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") { continue; } - const tools = frontmatter.tools - ?.split(",") - .map((t: string) => t.trim()) - .filter(Boolean); - agents.push({ name: frontmatter.name, description: frontmatter.description, - tools: tools && tools.length > 0 ? tools : undefined, - model: frontmatter.model, + tools: parseToolList(frontmatter.tools), + model: typeof frontmatter.model === "string" ? frontmatter.model : undefined, systemPrompt: body, source, filePath, diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 963fb84a8..fc39ce641 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.84.1", + "version": "0.84.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 08a30f330..907d96bc1 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.84.1", + "version": "0.84.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/install-lock/package-lock.json b/packages/coding-agent/install-lock/package-lock.json index cdd894e5b..fa092d94c 100644 --- a/packages/coding-agent/install-lock/package-lock.json +++ b/packages/coding-agent/install-lock/package-lock.json @@ -1,14 +1,14 @@ { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.84.1", + "version": "0.84.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.84.1", + "version": "0.84.2", "dependencies": { - "@earendil-works/pi-coding-agent": "0.84.1" + "@earendil-works/pi-coding-agent": "0.84.2" }, "engines": { "node": ">=22.19.0" @@ -450,12 +450,12 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-telemetry": "^0.84.2", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", @@ -466,13 +466,13 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", @@ -490,27 +490,26 @@ } }, "node_modules/@earendil-works/pi-client": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-protocol": "^0.84.1" + "@earendil-works/pi-protocol": "^0.84.2" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-client": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1", - "@earendil-works/pi-server": "^0.84.1", - "@earendil-works/pi-tui": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-client": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2", + "@earendil-works/pi-tui": "^0.84.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -539,40 +538,28 @@ } }, "node_modules/@earendil-works/pi-protocol": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", "typebox": "1.3.7" }, "engines": { "node": ">=22.19.0" } }, - "node_modules/@earendil-works/pi-server": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-server/-/pi-server-0.84.1.tgz", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1" - }, - "engines": { - "node": ">=22.19.0" - } - }, "node_modules/@earendil-works/pi-telemetry": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.2.tgz", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/install-lock/package.json b/packages/coding-agent/install-lock/package.json index a761b0bdc..4c35a51ec 100644 --- a/packages/coding-agent/install-lock/package.json +++ b/packages/coding-agent/install-lock/package.json @@ -1,10 +1,10 @@ { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.84.1", + "version": "0.84.2", "private": true, "description": "Lockfile root used by the Pi installer and updater.", "dependencies": { - "@earendil-works/pi-coding-agent": "0.84.1" + "@earendil-works/pi-coding-agent": "0.84.2" }, "overrides": { "protobufjs": "7.6.5", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index de194040d..bc53409b5 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,20 +1,19 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.1", + "version": "0.84.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.1", + "version": "0.84.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-client": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1", - "@earendil-works/pi-server": "^0.84.1", - "@earendil-works/pi-tui": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-client": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2", + "@earendil-works/pi-tui": "^0.84.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -478,12 +477,12 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-telemetry": "^0.84.2", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", @@ -494,13 +493,13 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@earendil-works/pi-telemetry": "^0.84.1", + "@earendil-works/pi-telemetry": "^0.84.2", "@google/genai": "1.52.0", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", @@ -518,51 +517,39 @@ } }, "node_modules/@earendil-works/pi-client": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-protocol": "^0.84.1" + "@earendil-works/pi-protocol": "^0.84.2" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-protocol": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", "typebox": "1.3.7" }, "engines": { "node": ">=22.19.0" } }, - "node_modules/@earendil-works/pi-server": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-server/-/pi-server-0.84.1.tgz", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1" - }, - "engines": { - "node": ">=22.19.0" - } - }, "node_modules/@earendil-works/pi-telemetry": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.2.tgz", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.1.tgz", + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5478cf416..791ad3ae1 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.84.1", + "version": "0.84.2", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -39,12 +39,11 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-client": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1", - "@earendil-works/pi-server": "^0.84.1", - "@earendil-works/pi-tui": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-client": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2", + "@earendil-works/pi-tui": "^0.84.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/coding-agent/src/cli/auth-command.ts b/packages/coding-agent/src/cli/auth-command.ts index a0e58d142..9ee804602 100644 --- a/packages/coding-agent/src/cli/auth-command.ts +++ b/packages/coding-agent/src/cli/auth-command.ts @@ -1,4 +1,5 @@ import type { AuthResult } from "@earendil-works/pi-ai"; +import { APP_NAME } from "../config.ts"; import type { Args } from "./args.ts"; export type AuthCommandKind = "check" | "api_key" | "bearer_token"; @@ -15,9 +16,9 @@ export interface AuthCommand { export class AuthCommandError extends Error {} const AUTH_COMMAND_USAGE: Record = { - check: "pi auth check --provider [--json] [--credentials] [--no-refresh]", - api_key: "pi auth print-api-key --provider [--model ]", - bearer_token: "pi auth print-bearer-token --provider [--model ] [--min-expiry ]", + check: `${APP_NAME} auth check --provider [--json] [--credentials] [--no-refresh]`, + api_key: `${APP_NAME} auth print-api-key --provider [--model ]`, + bearer_token: `${APP_NAME} auth print-bearer-token --provider [--model ] [--min-expiry ]`, }; export function getAuthCommandName(kind: AuthCommandKind): string { @@ -57,7 +58,7 @@ export function parseAuthCommand(args: string[]): AuthCommand | undefined { : undefined; if (!kind) { throw new AuthCommandError( - `Unknown auth command "${args[1] ?? ""}". Use "pi auth print-api-key", "pi auth print-bearer-token", or "pi auth check".`, + `Unknown auth command "${args[1] ?? ""}". Use "${APP_NAME} auth print-api-key", "${APP_NAME} auth print-bearer-token", or "${APP_NAME} auth check".`, ); } diff --git a/packages/coding-agent/src/core/project-trust.ts b/packages/coding-agent/src/core/project-trust.ts index 2521ad5db..b9ece17e4 100644 --- a/packages/coding-agent/src/core/project-trust.ts +++ b/packages/coding-agent/src/core/project-trust.ts @@ -1,4 +1,4 @@ -import { CONFIG_DIR_NAME } from "../config.ts"; +import { APP_NAME, CONFIG_DIR_NAME } from "../config.ts"; import { emitProjectTrustEvent } from "./extensions/runner.ts"; import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/types.ts"; import type { DefaultProjectTrust } from "./settings-manager.ts"; @@ -22,7 +22,7 @@ export interface ResolveProjectTrustedOptions { } function formatProjectTrustPrompt(cwd: string): string { - return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`; + return `Trust project folder?\n${cwd}\n\nThis allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`; } async function selectProjectTrustOption( diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 70d392164..cd9d2f437 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -17,7 +17,7 @@ import { readdir, stat } from "fs/promises"; import { join, resolve } from "path"; import { createInterface } from "readline"; import { StringDecoder } from "string_decoder"; -import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts"; +import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts"; import { normalizePath, resolvePath } from "../utils/paths.ts"; import { type BashExecutionMessage, @@ -902,7 +902,7 @@ export class SessionManager { if (this.fileEntries.length === 0) { const explicitPath = this.sessionFile; if (statSync(explicitPath).size > 0) { - throw new Error(`Session file is not a valid pi session: ${explicitPath}`); + throw new Error(`Session file is not a valid ${APP_NAME} session: ${explicitPath}`); } this.newSession(); this.sessionFile = explicitPath; diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 2add05d69..85ed6af4c 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -72,7 +72,7 @@ import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts"; import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts"; -const EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using "pi -ne".'; +const EXTENSION_LOAD_FAILURE_HINT = `Hint: Start without extensions using "${APP_NAME} -ne".`; /** * Read all content from piped stdin. diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index ad84f4413..b1420b36c 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -4,6 +4,9 @@ import { createAllToolDefinitions, type ToolName } from "../../../core/tools/ind import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.ts"; import { convertToPng } from "../../../utils/image-convert.ts"; import { theme } from "../theme/theme.ts"; +import { keyHint } from "./keybinding-hints.ts"; + +const FALLBACK_PREVIEW_LINES = 10; export interface ToolExecutionOptions { showImages?: boolean; @@ -141,7 +144,15 @@ export class ToolExecutionComponent extends Container { if (!output) { return undefined; } - return new Text(theme.fg("toolOutput", output), 0, 0); + + const lines = output.split("\n"); + const displayLines = this.expanded ? lines : lines.slice(0, FALLBACK_PREVIEW_LINES); + const remaining = lines.length - displayLines.length; + let text = displayLines.map((line) => theme.fg("toolOutput", line)).join("\n"); + if (remaining > 0) { + text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`; + } + return new Text(text, 0, 0); } updateArgs(args: any): void { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 4e64341b2..6d5e6b884 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -352,6 +352,14 @@ export function createInteractiveTui(options: InteractiveTuiOptions): TuiMainScr searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))), openUrl: openBrowser, onRightClickPaste: options.onRightClickPaste, + copySelection: async (text) => { + try { + await copyToClipboard(text); + return true; + } catch { + return false; + } + }, }); } return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory); @@ -3883,7 +3891,7 @@ export class InteractiveMode { try { this.ui.stop(); } catch {} - console.error("pi exiting due to uncaughtException:"); + console.error(`${APP_NAME} exiting due to uncaughtException:`); console.error(error); process.exit(1); } @@ -4759,7 +4767,7 @@ export class InteractiveMode { trustStore.setMany(selection.updates); done(); this.showStatus( - `Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`, + `Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart ${APP_NAME} for this to take effect.`, ); }, onCancel: () => { @@ -5537,7 +5545,11 @@ export class InteractiveMode { providerOption.name, `${providerOption.name} setup`, ); - dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true); + dialog.showInfo( + `${providerOption.method?.name ?? "Authentication"} is configured outside ${APP_NAME}.`, + [], + true, + ); this.editorContainer.clear(); this.editorContainer.addChild(dialog); diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index aedfdf0b9..0a05f3159 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -432,7 +432,7 @@ function printSelfUpdateUnavailable( const entrypoint = process.argv[1]; if (entrypoint) { console.error(""); - console.error(`Location of pi executable: ${entrypoint}`); + console.error(`Location of ${APP_NAME} executable: ${entrypoint}`); } } diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 49b3754ae..14d3b3f04 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -344,7 +344,7 @@ describe("ToolExecutionComponent parity", () => { expect(rendered).toContain("arg:bar"); }); - test("falls back when custom renderers are absent", () => { + test("collapses fallback results until expanded", () => { const toolDefinition: ToolDefinition = { ...createBaseToolDefinition(), }; @@ -358,10 +358,20 @@ describe("ToolExecutionComponent parity", () => { createFakeTui(), process.cwd(), ); - component.updateResult({ content: [{ type: "text", text: "done" }], details: {}, isError: false }, false); - const rendered = stripAnsi(component.render(120).join("\n")); - expect(rendered).toContain("custom_tool"); - expect(rendered).toContain("done"); + const output = Array.from({ length: 15 }, (_, index) => `line-${index + 1}`).join("\n"); + component.updateResult({ content: [{ type: "text", text: output }], details: {}, isError: false }, false); + + const collapsed = stripAnsi(component.render(120).join("\n")); + expect(collapsed).toContain("custom_tool"); + expect(collapsed).toContain("line-10"); + expect(collapsed).not.toContain("line-11"); + expect(collapsed).toContain("5 more lines"); + expect(collapsed).toContain("to expand"); + + component.setExpanded(true); + const expanded = stripAnsi(component.render(120).join("\n")); + expect(expanded).toContain("line-15"); + expect(expanded).not.toContain("more lines"); }); test("trims trailing blank display lines from write previews", () => { diff --git a/packages/evals/package.json b/packages/evals/package.json index 087ad17bf..3acd48e24 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-evals", - "version": "0.84.1", + "version": "0.84.2", "private": true, "type": "module", "scripts": { @@ -9,8 +9,8 @@ "test": "vitest run --config vitest.test.config.ts" }, "devDependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-coding-agent": "^0.84.1", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-coding-agent": "^0.84.2", "@types/node": "24.12.4", "shx": "0.4.0", "typescript": "5.9.3", diff --git a/packages/protocol/CHANGELOG.md b/packages/protocol/CHANGELOG.md index 3e32c6a57..d4328d9ea 100644 --- a/packages/protocol/CHANGELOG.md +++ b/packages/protocol/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + ## [0.84.1] - 2026-08-07 ## [0.84.0] - 2026-08-06 diff --git a/packages/protocol/package.json b/packages/protocol/package.json index a54029050..4147ff752 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-protocol", - "version": "0.84.1", + "version": "0.84.2", "description": "Transport-neutral CBOR protocol for remote pi sessions", "type": "module", "main": "./dist/index.js", @@ -39,7 +39,7 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", + "@earendil-works/pi-agent-core": "^0.84.2", "typebox": "1.3.7" }, "devDependencies": { diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index 7dcb6e9e1..1725d40d7 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + ## [0.84.1] - 2026-08-07 ## [0.84.0] - 2026-08-06 diff --git a/packages/server/package.json b/packages/server/package.json index f624969b4..a97389f5b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-server", - "version": "0.84.1", + "version": "0.84.2", "description": "experimental server package for pi", "type": "module", "main": "./dist/index.js", @@ -47,8 +47,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.1", - "@earendil-works/pi-protocol": "^0.84.1" + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/session-backends/sqlite-node/CHANGELOG.md b/packages/session-backends/sqlite-node/CHANGELOG.md index 97409302a..30064cb53 100644 --- a/packages/session-backends/sqlite-node/CHANGELOG.md +++ b/packages/session-backends/sqlite-node/CHANGELOG.md @@ -2,9 +2,7 @@ ## [Unreleased] -### Breaking Changes - -- Removed the incompatible work-in-progress session backend while the one-file-per-session backend is rebuilt against the durable storage contract; the Node SQLite adapter and SQL helpers remain available. +## [0.84.2] - 2026-08-14 ## [0.84.1] - 2026-08-07 diff --git a/packages/session-backends/sqlite-node/package.json b/packages/session-backends/sqlite-node/package.json index a52d9fa67..5d4af8ad9 100644 --- a/packages/session-backends/sqlite-node/package.json +++ b/packages/session-backends/sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-session-backend-sqlite-node", - "version": "0.84.1", + "version": "0.84.2", "description": "Node sqlite session backend for @earendil-works/pi-agent-core sessions", "type": "module", "main": "./dist/index.js", @@ -34,8 +34,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-ai": "^0.84.1", - "@earendil-works/pi-agent-core": "^0.84.1" + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-agent-core": "^0.84.2" }, "devDependencies": { "@vitest/coverage-v8": "4.1.9", diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md index 9dc96f4f8..e9de35b64 100644 --- a/packages/telemetry/CHANGELOG.md +++ b/packages/telemetry/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + ## [0.84.1] - 2026-08-07 ## [0.84.0] - 2026-08-06 diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index a06674100..37e115322 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-telemetry", - "version": "0.84.1", + "version": "0.84.2", "description": "Vendor-neutral telemetry contracts and typed schema utilities for pi", "type": "module", "main": "./dist/index.js", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index fd1257cc9..ae7c20028 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,9 +2,11 @@ ## [Unreleased] +## [0.84.2] - 2026-08-14 + ### Added -- Added unbound single-line transcript scrolling actions, `tui.altScreen.lineUp` and `tui.altScreen.lineDown`, for fullscreen TUI keybindings ([#7830](https://github.com/earendil-works/pi/issues/7830)). +- Added unbound single-line transcript scrolling actions, `tui.altScreen.lineUp` and `tui.altScreen.lineDown`, for fullscreen TUI keybindings ([#7903](https://github.com/earendil-works/pi/pull/7903) by [@midastruth](https://github.com/midastruth)). - Added incremental primary-scroll-view search to the fullscreen TUI with configurable match styles, `Ctrl+Shift+F`, and next/previous navigation with `Enter`/`Ctrl+G` and `Shift+Enter`/`Ctrl+Shift+G`. ### Changed @@ -13,10 +15,14 @@ ### Fixed +- Fixed fullscreen mouse drag selection and OSC 8 link activation in terminals that report generic SGR mouse release button codes ([#7963](https://github.com/earendil-works/pi/issues/7963)). - Fixed fullscreen transcript search snapping back to the current match during manual scrolling and fragmented SGR mouse input leaking into the search query. - Fixed required LaTeX arguments starting on a new line being parsed as empty ([#7760](https://github.com/earendil-works/pi/issues/7760)). - Fixed LaTeX control spaces split across line endings causing complete expressions to fall back to raw source. - Fixed focused fullscreen overlays not receiving mouse wheel or viewport scroll keys such as PageUp and PageDown ([#7894](https://github.com/earendil-works/pi/issues/7894)). +- Fixed split `Alt+Enter` input over SSH being misread as Escape, added `PI_TUI_ESC_TIMEOUT` for high-latency terminals, and limited that timeout to lone Escape input ([#7899](https://github.com/earendil-works/pi/pull/7899) by [@powerfooI](https://github.com/powerfooI)). +- Fixed idle fullscreen sessions repainting and clearing text selection when the terminal loses focus ([#7892](https://github.com/earendil-works/pi/pull/7892) by [@terrorobe](https://github.com/terrorobe)). +- Fixed fullscreen selection copy falsely reporting success when OSC 52 is unsupported by allowing host clipboard integration and reporting verified failures ([#8110](https://github.com/earendil-works/pi/pull/8110) by [@Panoplos](https://github.com/Panoplos)). ## [0.84.1] - 2026-08-07 diff --git a/packages/tui/package.json b/packages/tui/package.json index 52cabe212..034d351ab 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.84.1", + "version": "0.84.2", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/tui/src/tui-alt-screen.ts b/packages/tui/src/tui-alt-screen.ts index 25efa42fa..783a92ab8 100644 --- a/packages/tui/src/tui-alt-screen.ts +++ b/packages/tui/src/tui-alt-screen.ts @@ -153,6 +153,11 @@ export interface TuiAltScreenOptions { openUrl?: (url: string) => void; /** Handle an unmodified secondary-button press for clipboard paste. Currently enabled on Windows only. */ onRightClickPaste?: () => void; + /** + * Copy selected text to the system clipboard. Return `true` on success; the caller flashes + * an error otherwise. When omitted, the selection is copied via an OSC 52 write. + */ + copySelection?: (text: string) => Promise; } /** Alternate-screen TUI with a scrollable, application-owned viewport. */ @@ -192,6 +197,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { private readonly searchCurrentMatchStyle: (text: string) => string; private readonly openUrl?: (url: string) => void; private readonly onRightClickPaste?: () => void; + private readonly copySelection?: (text: string) => Promise; constructor( terminal: Terminal, @@ -214,6 +220,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { this.searchCurrentMatchStyle = options.searchCurrentMatchStyle ?? ((text) => `\x1b[1;7m${text}\x1b[22;27m`); this.openUrl = options.openUrl; this.onRightClickPaste = options.onRightClickPaste; + this.copySelection = options.copySelection; this.addInputListener((data) => this.handleViewportInput(data)); } @@ -942,7 +949,8 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { } private handleSelectionMouseEvent(event: SgrMouseEvent): void { - if ((event.button & 3) !== 0) return; + const button = event.button & 3; + if (button !== 0 && !(event.release && button === 3)) return; const anchorScrollView = this.selectionAnchor?.scrollView; const point = this.getSelectionPoint(event, anchorScrollView); if (event.release) { @@ -970,7 +978,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { this.requestRender(); return; } - this.copySelectionToClipboard(); + void this.copySelectionToClipboard(); this.requestRender(); return; } @@ -1046,7 +1054,7 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { return { start: Math.max(minColumn, start), end: Math.min(maxColumn, end) }; } - private copySelectionToClipboard(): void { + private async copySelectionToClipboard(): Promise { const selection = this.getSelectionBounds(); if (!selection) return; let sourceLines: readonly string[] = this.previousScreen; @@ -1068,6 +1076,15 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { } const text = lines.join("\n"); if (text.length === 0) return; + // Prefer an injected clipboard implementation (native clipboard + platform tools with a + // verified success path) when the host app provides one. A bare OSC 52 write can show + // "Copied!" while leaving the system clipboard untouched (e.g. macOS Terminal.app, tmux + // without OSC 52 clipboard passthrough), so only report success when it actually copies. + if (this.copySelection) { + const ok = await this.copySelection(text); + this.flash(ok ? "Copied!" : "Copy failed"); + return; + } this.terminal.write(`\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`); this.flash("Copied!"); } diff --git a/packages/tui/test/tui-alt-screen.test.ts b/packages/tui/test/tui-alt-screen.test.ts index 6c0fe28d4..2166debe7 100644 --- a/packages/tui/test/tui-alt-screen.test.ts +++ b/packages/tui/test/tui-alt-screen.test.ts @@ -890,7 +890,7 @@ describe("TuiAltScreen", () => { } }); - it("opens an OSC 8 hyperlink on click but not on drag", async () => { + it("opens an OSC 8 hyperlink with specific or generic release codes, but not on drag", async () => { const terminal = new RecordingTerminal(20, 3); const openedUrls: string[] = []; const tui = new TuiAltScreen(terminal, undefined, undefined, { @@ -910,7 +910,7 @@ describe("TuiAltScreen", () => { await terminal.waitForRender(); terminal.sendInput("\x1b[<0;2;1M"); - terminal.sendInput("\x1b[<0;2;1m"); + terminal.sendInput("\x1b[<3;2;1m"); await terminal.waitForRender(); assert.deepStrictEqual(openedUrls, [url]); @@ -933,7 +933,7 @@ describe("TuiAltScreen", () => { tui.stop(); }); - it("selects visible text with the mouse and copies it with OSC 52", async () => { + it("selects visible text with the mouse and copies it with OSC 52 after a generic release", async () => { const terminal = new RecordingTerminal(20, 4); const tui = new TuiAltScreen(terminal); tui.addChild(new Text("\x1b[1mal\x1b[0mpha\nbeta\ngamma\ndelta", 0, 0)); @@ -942,7 +942,7 @@ describe("TuiAltScreen", () => { terminal.sendInput("\x1b[<0;1;1M"); terminal.sendInput("\x1b[<32;4;2M"); - terminal.sendInput("\x1b[<0;4;2m"); + terminal.sendInput("\x1b[<3;4;2m"); await terminal.waitForRender(); const expectedClipboardSequence = `\x1b]52;c;${Buffer.from("alpha\nbeta").toString("base64")}\x07`; @@ -963,6 +963,57 @@ describe("TuiAltScreen", () => { tui.stop(); }); + it("uses an injected copySelection handler instead of OSC 52 and reports success", async () => { + const terminal = new RecordingTerminal(20, 4); + const copied: string[] = []; + const tui = new TuiAltScreen(terminal, undefined, undefined, { + copySelection: async (text) => { + copied.push(text); + return true; + }, + }); + tui.addChild(new Text("alpha\nbeta\ngamma\ndelta", 0, 0)); + tui.start(); + await terminal.waitForRender(); + + terminal.sendInput("\x1b[<0;1;1M"); + terminal.sendInput("\x1b[<32;4;2M"); + terminal.sendInput("\x1b[<0;4;2m"); + await terminal.waitForRender(); + + assert.deepStrictEqual(copied, ["alpha\nbeta"]); + assert.ok( + terminal.events.every((event) => event.type !== "write" || !event.data.includes("\x1b]52;c;")), + "must not emit OSC 52 when a copySelection handler is provided", + ); + assert.ok(terminal.getViewport().some((line) => line.includes("Copied!"))); + + tui.stop(); + }); + + it("flashes an error when the injected copySelection handler fails", async () => { + const terminal = new RecordingTerminal(20, 4); + const tui = new TuiAltScreen(terminal, undefined, undefined, { + copySelection: async () => false, + }); + tui.addChild(new Text("alpha\nbeta\ngamma\ndelta", 0, 0)); + tui.start(); + await terminal.waitForRender(); + + terminal.sendInput("\x1b[<0;1;1M"); + terminal.sendInput("\x1b[<32;4;2M"); + terminal.sendInput("\x1b[<0;4;2m"); + await terminal.waitForRender(); + + assert.ok(terminal.getViewport().some((line) => line.includes("Copy failed"))); + assert.ok( + terminal.events.every((event) => event.type !== "write" || !event.data.includes("\x1b]52;c;")), + "must not emit OSC 52 when a copySelection handler is provided", + ); + + tui.stop(); + }); + it("does not append whitespace to double-click word highlighting", async () => { const terminal = new RecordingTerminal(20, 1); const tui = new TuiAltScreen(terminal);