From 3cd1554c8f7abe0510ab2962b87594bd63521717 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 May 2026 16:42:07 +0000 Subject: [PATCH 01/30] chore: approve contributor npupko --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index fbbea170..5dcdd534 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -198,3 +198,5 @@ louis030195 pr technocidal pr pandada8 pr + +npupko issue From 6b18cdbac12edb1d6156b1307cb5133fa0ad1a41 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 4 May 2026 19:06:07 +0200 Subject: [PATCH 02/30] fix(coding-agent): stream bash output incrementally (#4165) fixes #4145 --- packages/coding-agent/src/core/tools/bash.ts | 228 +++++++++--------- .../src/core/tools/output-accumulator.ts | 216 +++++++++++++++++ packages/coding-agent/test/tools.test.ts | 74 +++++- 3 files changed, 399 insertions(+), 119 deletions(-) create mode 100644 packages/coding-agent/src/core/tools/output-accumulator.ts diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 18762371..c49e8996 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -1,7 +1,4 @@ -import { randomBytes } from "node:crypto"; -import { createWriteStream, existsSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { existsSync } from "node:fs"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui"; import { spawn } from "child_process"; @@ -18,17 +15,10 @@ import { untrackDetachedChildPid, } from "../../utils/shell.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { OutputAccumulator } from "./output-accumulator.js"; import { getTextOutput, invalidArgText, str } from "./render-utils.js"; import { wrapToolDefinition } from "./tool-definition-wrapper.js"; -import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js"; - -/** - * Generate a unique temp file path for bash output. - */ -function getTempFilePath(): string { - const id = randomBytes(8).toString("hex"); - return join(tmpdir(), `pi-bash-${id}.log`); -} +import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.js"; const bashSchema = Type.Object({ command: Type.String({ description: "Bash command to execute" }), @@ -161,6 +151,7 @@ export interface BashToolOptions { } const BASH_PREVIEW_LINES = 5; +const BASH_UPDATE_THROTTLE_MS = 100; type BashRenderState = { startedAt: number | undefined; @@ -292,118 +283,119 @@ export function createBashToolDefinition( ) { const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command; const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook); + const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" }); + let updateTimer: NodeJS.Timeout | undefined; + let updateDirty = false; + let lastUpdateAt = 0; + + const emitOutputUpdate = () => { + if (!onUpdate || !updateDirty) return; + updateDirty = false; + lastUpdateAt = Date.now(); + const snapshot = output.snapshot({ persistIfTruncated: true }); + onUpdate({ + content: [{ type: "text", text: snapshot.content || "" }], + details: { + truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined, + fullOutputPath: snapshot.fullOutputPath, + }, + }); + }; + + const clearUpdateTimer = () => { + if (updateTimer) { + clearTimeout(updateTimer); + updateTimer = undefined; + } + }; + + const scheduleOutputUpdate = () => { + if (!onUpdate) return; + updateDirty = true; + const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt); + if (delay <= 0) { + clearUpdateTimer(); + emitOutputUpdate(); + return; + } + updateTimer ??= setTimeout(() => { + updateTimer = undefined; + emitOutputUpdate(); + }, delay); + }; + if (onUpdate) { onUpdate({ content: [], details: undefined }); } - return new Promise((resolve, reject) => { - let tempFilePath: string | undefined; - let tempFileStream: ReturnType | undefined; - let totalBytes = 0; - const chunks: Buffer[] = []; - let chunksBytes = 0; - const maxChunksBytes = DEFAULT_MAX_BYTES * 2; - const ensureTempFile = () => { - if (tempFilePath) return; - tempFilePath = getTempFilePath(); - tempFileStream = createWriteStream(tempFilePath); - for (const chunk of chunks) tempFileStream.write(chunk); - }; + const handleData = (data: Buffer) => { + output.append(data); + scheduleOutputUpdate(); + }; - const handleData = (data: Buffer) => { - totalBytes += data.length; - // Start writing to a temp file once output exceeds the in-memory threshold. - if (totalBytes > DEFAULT_MAX_BYTES) { - ensureTempFile(); - } - // Write to temp file if we have one. - if (tempFileStream) tempFileStream.write(data); - // Keep a rolling buffer of recent output for tail truncation. - chunks.push(data); - chunksBytes += data.length; - // Trim old chunks if the rolling buffer grows too large. - while (chunksBytes > maxChunksBytes && chunks.length > 1) { - const removed = chunks.shift()!; - chunksBytes -= removed.length; - } - // Stream partial output using the rolling tail buffer. - if (onUpdate) { - const fullBuffer = Buffer.concat(chunks); - const fullText = fullBuffer.toString("utf-8"); - const truncation = truncateTail(fullText); - if (truncation.truncated) { - ensureTempFile(); - } - onUpdate({ - content: [{ type: "text", text: truncation.content || "" }], - details: { - truncation: truncation.truncated ? truncation : undefined, - fullOutputPath: tempFilePath, - }, - }); - } - }; + const finishOutput = async () => { + output.finish(); + clearUpdateTimer(); + emitOutputUpdate(); + const snapshot = output.snapshot({ persistIfTruncated: true }); + await output.closeTempFile(); + return snapshot; + }; - ops.exec(spawnContext.command, spawnContext.cwd, { - onData: handleData, - signal, - timeout, - env: spawnContext.env, - }) - .then(({ exitCode }) => { - // Combine the rolling buffer chunks. - const fullBuffer = Buffer.concat(chunks); - const fullOutput = fullBuffer.toString("utf-8"); - // Apply tail truncation for the final display payload. - const truncation = truncateTail(fullOutput); - if (truncation.truncated) { - ensureTempFile(); - } - // Close temp file stream before building the final result. - if (tempFileStream) tempFileStream.end(); - let outputText = truncation.content || "(no output)"; - let details: BashToolDetails | undefined; - if (truncation.truncated) { - // Build truncation details and an actionable notice. - details = { truncation, fullOutputPath: tempFilePath }; - const startLine = truncation.totalLines - truncation.outputLines + 1; - const endLine = truncation.totalLines; - if (truncation.lastLinePartial) { - // Edge case: the last line alone is larger than the byte limit. - const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8")); - outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`; - } else if (truncation.truncatedBy === "lines") { - outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`; - } else { - outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`; - } - } - if (exitCode !== 0 && exitCode !== null) { - outputText += `\n\nCommand exited with code ${exitCode}`; - reject(new Error(outputText)); - } else { - resolve({ content: [{ type: "text", text: outputText }], details }); - } - }) - .catch((err: Error) => { - // Close temp file stream and include buffered output in the error message. - if (tempFileStream) tempFileStream.end(); - const fullBuffer = Buffer.concat(chunks); - let output = fullBuffer.toString("utf-8"); - if (err.message === "aborted") { - if (output) output += "\n\n"; - output += "Command aborted"; - reject(new Error(output)); - } else if (err.message.startsWith("timeout:")) { - const timeoutSecs = err.message.split(":")[1]; - if (output) output += "\n\n"; - output += `Command timed out after ${timeoutSecs} seconds`; - reject(new Error(output)); - } else { - reject(err); - } + const formatOutput = (snapshot: Awaited>, emptyText = "(no output)") => { + const truncation = snapshot.truncation; + let text = snapshot.content || emptyText; + let details: BashToolDetails | undefined; + if (truncation.truncated) { + details = { truncation, fullOutputPath: snapshot.fullOutputPath }; + const startLine = truncation.totalLines - truncation.outputLines + 1; + const endLine = truncation.totalLines; + if (truncation.lastLinePartial) { + const lastLineSize = formatSize(output.getLastLineBytes()); + text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`; + } else if (truncation.truncatedBy === "lines") { + text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`; + } else { + text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`; + } + } + return { text, details }; + }; + + const appendStatus = (text: string, status: string) => `${text ? `${text}\n\n` : ""}${status}`; + + try { + let exitCode: number | null; + try { + const result = await ops.exec(spawnContext.command, spawnContext.cwd, { + onData: handleData, + signal, + timeout, + env: spawnContext.env, }); - }); + exitCode = result.exitCode; + } catch (err) { + const snapshot = await finishOutput(); + const { text } = formatOutput(snapshot, ""); + if (err instanceof Error && err.message === "aborted") { + throw new Error(appendStatus(text, "Command aborted")); + } + if (err instanceof Error && err.message.startsWith("timeout:")) { + const timeoutSecs = err.message.split(":")[1]; + throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`)); + } + throw err; + } + + const snapshot = await finishOutput(); + const { text: outputText, details } = formatOutput(snapshot); + if (exitCode !== 0 && exitCode !== null) { + throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`)); + } + return { content: [{ type: "text", text: outputText }], details }; + } finally { + clearUpdateTimer(); + } }, renderCall(args, _theme, context) { const state = context.state; diff --git a/packages/coding-agent/src/core/tools/output-accumulator.ts b/packages/coding-agent/src/core/tools/output-accumulator.ts new file mode 100644 index 00000000..30f4d663 --- /dev/null +++ b/packages/coding-agent/src/core/tools/output-accumulator.ts @@ -0,0 +1,216 @@ +import { randomBytes } from "node:crypto"; +import { createWriteStream, type WriteStream } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.js"; + +export interface OutputAccumulatorOptions { + maxLines?: number; + maxBytes?: number; + tempFilePrefix?: string; +} + +export interface OutputSnapshot { + content: string; + truncation: TruncationResult; + fullOutputPath?: string; +} + +function defaultTempFilePath(prefix: string): string { + const id = randomBytes(8).toString("hex"); + return join(tmpdir(), `${prefix}-${id}.log`); +} + +function byteLength(text: string): number { + return Buffer.byteLength(text, "utf-8"); +} + +/** + * Incrementally tracks streaming output with bounded memory. + * + * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded + * tail for display snapshots, and opens a temp file when the full output needs + * to be preserved. + */ +export class OutputAccumulator { + private readonly maxLines: number; + private readonly maxBytes: number; + private readonly maxRollingBytes: number; + private readonly tempFilePrefix: string; + private readonly decoder = new TextDecoder(); + + private rawChunks: Buffer[] = []; + private tailText = ""; + private tailBytes = 0; + private tailStartsAtLineBoundary = true; + private totalRawBytes = 0; + private totalDecodedBytes = 0; + private totalLines = 1; + private currentLineBytes = 0; + private finished = false; + + private tempFilePath: string | undefined; + private tempFileStream: WriteStream | undefined; + + constructor(options: OutputAccumulatorOptions = {}) { + this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES; + this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + this.maxRollingBytes = Math.max(this.maxBytes * 2, 1); + this.tempFilePrefix = options.tempFilePrefix ?? "pi-output"; + } + + append(data: Buffer): void { + if (this.finished) { + throw new Error("Cannot append to a finished output accumulator"); + } + + this.totalRawBytes += data.length; + this.appendDecodedText(this.decoder.decode(data, { stream: true })); + + if (this.tempFileStream || this.shouldUseTempFile()) { + this.ensureTempFile(); + this.tempFileStream?.write(data); + } else if (data.length > 0) { + this.rawChunks.push(data); + } + } + + finish(): void { + if (this.finished) { + return; + } + this.finished = true; + this.appendDecodedText(this.decoder.decode()); + if (this.shouldUseTempFile()) { + this.ensureTempFile(); + } + } + + snapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot { + const tailTruncation = truncateTail(this.getSnapshotText(), { + maxLines: this.maxLines, + maxBytes: this.maxBytes, + }); + const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes; + const truncatedBy = truncated + ? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines")) + : null; + const truncation: TruncationResult = { + ...tailTruncation, + truncated, + truncatedBy, + totalLines: this.totalLines, + totalBytes: this.totalDecodedBytes, + maxLines: this.maxLines, + maxBytes: this.maxBytes, + }; + + if (options.persistIfTruncated && truncation.truncated) { + this.ensureTempFile(); + } + + return { + content: truncation.content, + truncation, + fullOutputPath: this.tempFilePath, + }; + } + + async closeTempFile(): Promise { + if (!this.tempFileStream) { + return; + } + + const stream = this.tempFileStream; + this.tempFileStream = undefined; + + await new Promise((resolve, reject) => { + const onError = (error: Error) => { + stream.off("finish", onFinish); + reject(error); + }; + const onFinish = () => { + stream.off("error", onError); + resolve(); + }; + stream.once("error", onError); + stream.once("finish", onFinish); + stream.end(); + }); + } + + getLastLineBytes(): number { + return this.currentLineBytes; + } + + private appendDecodedText(text: string): void { + if (text.length === 0) { + return; + } + + const bytes = byteLength(text); + this.totalDecodedBytes += bytes; + this.tailText += text; + this.tailBytes += bytes; + if (this.tailBytes > this.maxRollingBytes * 2) { + this.trimTail(); + } + + let newlines = 0; + let lastNewline = -1; + for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) { + newlines++; + lastNewline = i; + } + if (newlines === 0) { + this.currentLineBytes += bytes; + } else { + this.totalLines += newlines; + this.currentLineBytes = byteLength(text.slice(lastNewline + 1)); + } + } + + private trimTail(): void { + const buffer = Buffer.from(this.tailText, "utf-8"); + if (buffer.length <= this.maxRollingBytes) { + this.tailBytes = buffer.length; + return; + } + + let start = buffer.length - this.maxRollingBytes; + while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) { + start++; + } + + this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a; + this.tailText = buffer.subarray(start).toString("utf-8"); + this.tailBytes = byteLength(this.tailText); + } + + private getSnapshotText(): string { + if (this.tailStartsAtLineBoundary) { + return this.tailText; + } + + const firstNewline = this.tailText.indexOf("\n"); + return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1); + } + + private shouldUseTempFile(): boolean { + return ( + this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines + ); + } + + private ensureTempFile(): void { + if (this.tempFilePath) { + return; + } + this.tempFilePath = defaultTempFilePath(this.tempFilePrefix); + this.tempFileStream = createWriteStream(this.tempFilePath); + for (const chunk of this.rawChunks) { + this.tempFileStream.write(chunk); + } + this.rawChunks = []; + } +} diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index df0c8f62..70e4e54f 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { executeBashWithOperations } from "../src/core/bash-executor.js"; -import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js"; +import { type BashOperations, createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js"; import { computeEditsDiff } from "../src/core/tools/edit-diff.js"; import { createEditTool, @@ -449,6 +449,42 @@ describe("Coding Agent Tools", () => { ); }); + it("should include full output path for truncated timeout and abort errors", async () => { + for (const testCase of [ + { error: "timeout:5", expected: "Command timed out after 5 seconds" }, + { error: "aborted", expected: "Command aborted" }, + ]) { + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + for (let i = 1; i <= 3000; i++) { + onData(Buffer.from(`${i}\n`, "utf-8")); + } + throw new Error(testCase.error); + }, + }; + const bash = createBashTool(testDir, { operations }); + + let error: unknown; + try { + await bash.execute(`test-call-${testCase.error}`, { command: "chatty-fail" }); + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(Error); + const message = (error as Error).message; + expect(message).toContain(testCase.expected); + expect(message).toMatch(/\[Showing lines \d+-\d+ of \d+\. Full output: /); + expect(message).not.toContain("Full output: undefined"); + const fullOutputPath = message.match(/Full output: ([^\]\n]+)/)?.[1]; + expect(fullOutputPath).toBeDefined(); + expect(existsSync(fullOutputPath!)).toBe(true); + const fullOutput = readFileSync(fullOutputPath!, "utf-8"); + expect(fullOutput).toContain("1\n2\n3"); + expect(fullOutput).toContain("2998\n2999\n3000"); + } + }); + it("should throw error when cwd does not exist", async () => { const nonexistentCwd = "/this/directory/definitely/does/not/exist/12345"; @@ -517,6 +553,42 @@ describe("Coding Agent Tools", () => { expect(getTextOutput(result).trim()).toBe("no-prefix"); }); + it("should coalesce streaming updates for chatty output", async () => { + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + for (let i = 0; i < 5000; i++) { + onData(Buffer.from(`line ${i}\n`, "utf-8")); + } + return { exitCode: 0 }; + }, + }; + const updates: Array<{ content: Array<{ type: string; text?: string }>; details?: unknown }> = []; + const bash = createBashTool(testDir, { operations }); + + const result = await bash.execute("test-call-chatty-updates", { command: "chatty" }, undefined, (update) => + updates.push(update), + ); + + expect(updates.length).toBeLessThan(25); + expect(getTextOutput(result)).toContain("line 4999"); + }); + + it("should decode UTF-8 characters split across output chunks", async () => { + const euro = Buffer.from("€\n", "utf-8"); + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + onData(euro.subarray(0, 1)); + onData(euro.subarray(1)); + return { exitCode: 0 }; + }, + }; + const bash = createBashTool(testDir, { operations }); + + const result = await bash.execute("test-call-split-utf8", { command: "split-utf8" }); + + expect(getTextOutput(result).trim()).toBe("€"); + }); + it("should expose local bash operations for extension reuse", async () => { const ops = createLocalBashOperations(); const chunks: Buffer[] = []; From 1bc640e53a9844253e1bb0a77f85125797fa380a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 4 May 2026 20:24:13 +0200 Subject: [PATCH 03/30] fix(tui): prioritize exact fuzzy matches --- packages/tui/src/fuzzy.ts | 4 ++++ packages/tui/test/fuzzy.test.ts | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/packages/tui/src/fuzzy.ts b/packages/tui/src/fuzzy.ts index 09410238..35df468a 100644 --- a/packages/tui/src/fuzzy.ts +++ b/packages/tui/src/fuzzy.ts @@ -60,6 +60,10 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch { return { matches: false, score: 0 }; } + if (normalizedQuery === textLower) { + score -= 100; + } + return { matches: true, score }; }; diff --git a/packages/tui/test/fuzzy.test.ts b/packages/tui/test/fuzzy.test.ts index e3e7d1e8..e784e49c 100644 --- a/packages/tui/test/fuzzy.test.ts +++ b/packages/tui/test/fuzzy.test.ts @@ -83,6 +83,13 @@ describe("fuzzyFilter", () => { assert.strictEqual(result[0], "app"); }); + it("prioritizes exact matches over longer prefix matches", () => { + const items = ["clone", "cl"]; + const result = fuzzyFilter(items, "cl", (x: string) => x); + + assert.deepStrictEqual(result, ["cl", "clone"]); + }); + it("works with custom getText function", () => { const items = [ { name: "foo", id: 1 }, From 299dc70abe17f950ac4ce6371a6bdfd77a64a107 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 4 May 2026 20:27:27 +0200 Subject: [PATCH 04/30] docs(changelog): audit unreleased entries --- packages/ai/CHANGELOG.md | 11 +++++++++-- packages/coding-agent/CHANGELOG.md | 17 +++++++++++++++-- packages/tui/CHANGELOG.md | 4 ++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1be5a737..40ac4f2b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,11 +4,18 @@ ### Breaking Changes -- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var. +- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)). ### Added -- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). +- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`) ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)). +- Added `registerSessionResourceCleanup()` and `cleanupSessionResources()` so providers can register cleanup hooks for session-scoped resources. + +### Fixed + +- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7 to match models.dev and OpenCode Go ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)). +- Fixed Bedrock Converse thinking effort mapping to preserve native `xhigh` for Claude Opus 4.7. +- Fixed OpenAI Codex Responses WebSocket transport to fall back to SSE when setup fails before streaming starts, and attach transport diagnostics to the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)). ## [0.72.1] - 2026-05-02 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c9dbcd3d..fa5b11e2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,13 +2,19 @@ ## [Unreleased] +### New Features + +- **Xiaomi MiMo API billing and regional Token Plan providers** - `xiaomi` now uses API billing, with separate `xiaomi-token-plan-{cn,ams,sgp}` providers. See [docs/providers.md#api-keys](docs/providers.md#api-keys) and [README.md#providers--models](README.md#providers--models). ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)) +- **Incremental bash output streaming** - Bash tool output now appears while commands run instead of only after completion. ([#4145](https://github.com/badlogic/pi-mono/issues/4145)) +- **Compact read rendering** - Interactive `read` output for Pi docs, context files, and skills is collapsed by default and shows selected line ranges. + ### Breaking Changes -- Switched the built-in `xiaomi` provider from Token Plan AMS to Xiaomi's API billing endpoint, and renamed its `/login` display from "Xiaomi MiMo Token Plan" to "Xiaomi MiMo". `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users on Token Plan should switch to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var. +- Switched the built-in `xiaomi` provider from Token Plan AMS to Xiaomi's API billing endpoint, and renamed its `/login` display from "Xiaomi MiMo Token Plan" to "Xiaomi MiMo". `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users on Token Plan should switch to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)). ### Added -- Added three Xiaomi MiMo Token Plan regional providers visible in `/login`: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). Each defaults to `mimo-v2.5-pro`. +- Added three Xiaomi MiMo Token Plan regional providers visible in `/login`: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). Each defaults to `mimo-v2.5-pro` ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)). ### Changed @@ -16,7 +22,14 @@ ### Fixed +- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7, so those models work through the built-in provider catalog ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)). +- Fixed Bedrock Claude Opus 4.7 `xhigh` thinking requests by preserving the provider's native effort value. +- Fixed OpenAI Codex WebSocket transport to fall back to SSE when setup fails before streaming starts, and surface transport diagnostics in the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)). - Fixed OpenAI Codex WebSocket transport keeping `--print` and JSON mode processes alive after the response by closing cached WebSocket sessions during session shutdown ([#4103](https://github.com/badlogic/pi-mono/issues/4103)). +- Fixed compact `read` tool calls to render directly and include selected line ranges in interactive output. +- Fixed interactive sessions to exit when terminal input is lost instead of continuing in a broken state. +- Fixed bash tool output to stream incrementally while commands run instead of waiting for command completion ([#4145](https://github.com/badlogic/pi-mono/issues/4145)). +- Fixed selector and autocomplete fuzzy ranking to prioritize exact matches. ## [0.72.1] - 2026-05-02 diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 22c97e0a..9b620e4f 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed fuzzy ranking to prioritize exact matches in selector and autocomplete results. + ## [0.72.1] - 2026-05-02 ## [0.72.0] - 2026-05-01 From dbcb473d6fdb96f60570b9ebe73e7aa6316fa8fb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 4 May 2026 20:32:03 +0200 Subject: [PATCH 05/30] Release v0.73.0 --- package-lock.json | 411 ++++++++++++------ packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/CHANGELOG.md | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 +- 19 files changed, 294 insertions(+), 171 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fa4f1d2..05e2b28c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,15 +55,6 @@ "node": ">=18.0.0" } }, - "node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@anthropic-ai/sandbox-runtime/node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -923,6 +914,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -940,6 +934,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -957,6 +954,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -974,6 +974,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -1660,6 +1663,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1676,6 +1682,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1692,6 +1701,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1708,6 +1720,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1724,6 +1739,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1968,6 +1986,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1988,6 +2009,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2008,6 +2032,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2028,6 +2055,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2048,6 +2078,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2279,6 +2312,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2300,6 +2336,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2321,6 +2360,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2342,6 +2384,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2363,6 +2408,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2384,6 +2432,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2554,9 +2605,9 @@ "license": "BSD-3-Clause" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", "cpu": [ "arm" ], @@ -2567,9 +2618,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", "cpu": [ "arm64" ], @@ -2580,9 +2631,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], @@ -2593,9 +2644,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", "cpu": [ "x64" ], @@ -2606,9 +2657,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", "cpu": [ "arm64" ], @@ -2619,9 +2670,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", "cpu": [ "x64" ], @@ -2632,12 +2683,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2645,12 +2699,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2658,12 +2715,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2671,12 +2731,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2684,12 +2747,15 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2697,12 +2763,15 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2710,12 +2779,15 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2723,12 +2795,15 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2736,12 +2811,15 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2749,12 +2827,15 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2762,12 +2843,15 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2775,12 +2859,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2788,12 +2875,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2801,9 +2891,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", "cpu": [ "x64" ], @@ -2814,9 +2904,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", "cpu": [ "arm64" ], @@ -2827,9 +2917,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", "cpu": [ "arm64" ], @@ -2840,9 +2930,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", "cpu": [ "ia32" ], @@ -2853,9 +2943,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", "cpu": [ "x64" ], @@ -2866,9 +2956,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", "cpu": [ "x64" ], @@ -3666,6 +3756,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3682,6 +3775,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3698,6 +3794,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3714,6 +3813,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4582,12 +4684,12 @@ "license": "MIT" }, "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=18" } }, "node_modules/concurrently": { @@ -5110,9 +5212,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.7.tgz", + "integrity": "sha512-Yh7/7rQuMXICNr0oMYDR2yHP6oUvmQsTToFeOWj/kIDhAwQ+c4Ol/lbcwOmEM5OHYQmh6S6EQSQ1sljCKP36bQ==", "funding": [ { "type": "github", @@ -5795,6 +5897,15 @@ "katex": "cli.js" } }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/koffi": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.1.tgz", @@ -5951,6 +6062,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5971,6 +6085,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5991,6 +6108,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6011,6 +6131,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6115,9 +6238,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -6657,9 +6780,9 @@ } }, "node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -6932,9 +7055,9 @@ } }, "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6947,31 +7070,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" } }, @@ -8156,9 +8279,9 @@ } }, "node_modules/zod": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", - "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -8175,10 +8298,10 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent-core", - "version": "0.72.1", + "version": "0.73.0", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.72.1", + "@mariozechner/pi-ai": "^0.73.0", "typebox": "^1.1.24" }, "devDependencies": { @@ -8209,7 +8332,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.72.1", + "version": "0.73.0", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -8255,13 +8378,13 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.72.1", + "version": "0.73.0", "license": "MIT", "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.72.1", - "@mariozechner/pi-ai": "^0.72.1", - "@mariozechner/pi-tui": "^0.72.1", + "@mariozechner/pi-agent-core": "^0.73.0", + "@mariozechner/pi-ai": "^0.73.0", + "@mariozechner/pi-tui": "^0.73.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", @@ -8302,25 +8425,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.72.1", + "version": "0.73.0", "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.72.1" + "version": "0.73.0" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.2.1", + "version": "1.3.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.72.1", + "version": "0.73.0", "dependencies": { "ms": "^2.1.3" }, @@ -8356,7 +8479,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.72.1", + "version": "0.73.0", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -8378,12 +8501,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.72.1", + "version": "0.73.0", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.72.1", - "@mariozechner/pi-tui": "^0.72.1", + "@mariozechner/pi-ai": "^0.73.0", + "@mariozechner/pi-tui": "^0.73.0", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", @@ -8405,7 +8528,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.72.1", + "version": "0.73.0", "dependencies": { "@mariozechner/mini-lit": "^0.2.0", "@mariozechner/pi-ai": "file:../../ai", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index e983a617..16cd2e3a 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.0] - 2026-05-04 ## [0.72.1] - 2026-05-02 diff --git a/packages/agent/package.json b/packages/agent/package.json index fb8e6bf5..6fc8ec8f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent-core", - "version": "0.72.1", + "version": "0.73.0", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.72.1", + "@mariozechner/pi-ai": "^0.73.0", "typebox": "^1.1.24" }, "keywords": [ diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 40ac4f2b..b72ff3f7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.0] - 2026-05-04 ### Breaking Changes diff --git a/packages/ai/package.json b/packages/ai/package.json index eb75ba23..f05b48cf 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.72.1", + "version": "0.73.0", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fa5b11e2..17578c71 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.0] - 2026-05-04 ### New Features 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 7cefabb7..ac299123 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.72.1", + "version": "0.73.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.72.1", + "version": "0.73.0", "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 8db7e0bc..7f1f0088 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.72.1", + "version": "0.73.0", "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 3ff6671e..b74591d2 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.72.1", + "version": "0.73.0", "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 2c081e21..fbe7ec7c 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.2.1", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.2.1", + "version": "1.3.0", "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 7e3672fd..e9190db9 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.2.1", + "version": "1.3.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", 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 f8f490e2..98a99292 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.72.1", + "version": "0.73.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.72.1", + "version": "0.73.0", "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 5607b6db..5825a89a 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.72.1", + "version": "0.73.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 97fd1097..635de12e 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-coding-agent", - "version": "0.72.1", + "version": "0.73.0", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -39,9 +39,9 @@ }, "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.72.1", - "@mariozechner/pi-ai": "^0.72.1", - "@mariozechner/pi-tui": "^0.72.1", + "@mariozechner/pi-agent-core": "^0.73.0", + "@mariozechner/pi-ai": "^0.73.0", + "@mariozechner/pi-tui": "^0.73.0", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9b620e4f..e0964afd 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.0] - 2026-05-04 ### Fixed diff --git a/packages/tui/package.json b/packages/tui/package.json index ae4617a6..bd7e17c8 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-tui", - "version": "0.72.1", + "version": "0.73.0", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 44fdb19f..f8790338 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.0] - 2026-05-04 ## [0.72.1] - 2026-05-02 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 13432d29..afe8c076 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.72.1", + "version": "0.73.0", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 488efc07..38e89871 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-web-ui", - "version": "0.72.1", + "version": "0.73.0", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.72.1", - "@mariozechner/pi-tui": "^0.72.1", + "@mariozechner/pi-ai": "^0.73.0", + "@mariozechner/pi-tui": "^0.73.0", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 3c9c54d51bf4ad8b4bd0f81208255810388bd922 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 4 May 2026 20:33:08 +0200 Subject: [PATCH 06/30] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 + packages/ai/CHANGELOG.md | 2 + packages/ai/src/models.generated.ts | 66 ++++++++++++++--------------- packages/coding-agent/CHANGELOG.md | 2 + packages/tui/CHANGELOG.md | 2 + packages/web-ui/CHANGELOG.md | 2 + 6 files changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 16cd2e3a..972c0c51 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.0] - 2026-05-04 ## [0.72.1] - 2026-05-02 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b72ff3f7..365f4647 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.0] - 2026-05-04 ### Breaking Changes diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index c4c41bec..73a41499 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -9207,8 +9207,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.21, - output: 0.7899999999999999, + input: 0.27, + output: 0.95, cacheRead: 0.13, cacheWrite: 0, }, @@ -9279,13 +9279,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { - input: 0.435, - output: 0.87, - cacheRead: 0.003625, + input: 0, + output: 0, + cacheRead: 0, cacheWrite: 0, }, - contextWindow: 1048576, - maxTokens: 384000, + contextWindow: 131000, + maxTokens: 131000, } satisfies Model<"openai-completions">, "essentialai/rnj-1-instruct": { id: "essentialai/rnj-1-instruct", @@ -9318,7 +9318,7 @@ export const MODELS = { cacheRead: 0.024999999999999998, cacheWrite: 0.08333333333333334, }, - contextWindow: 1000000, + contextWindow: 1048576, maxTokens: 8192, } satisfies Model<"openai-completions">, "google/gemini-2.0-flash-lite-001": { @@ -11807,13 +11807,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.08, - output: 0.28, + input: 0.09, + output: 0.44999999999999996, cacheRead: 0, cacheWrite: 0, }, contextWindow: 40960, - maxTokens: 16384, + maxTokens: 20000, } satisfies Model<"openai-completions">, "qwen/qwen3-30b-a3b-instruct-2507": { id: "qwen/qwen3-30b-a3b-instruct-2507", @@ -12232,13 +12232,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.1625, - output: 1.3, - cacheRead: 0, + input: 0.15, + output: 1, + cacheRead: 0.049999999999999996, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 65536, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12342,6 +12342,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 81920, } satisfies Model<"openai-completions">, + "qwen/qwen3.6-35b-a3b": { + id: "qwen/qwen3.6-35b-a3b", + name: "Qwen: Qwen3.6 35B A3B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 1, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, "qwen/qwen3.6-flash": { id: "qwen/qwen3.6-flash", name: "Qwen: Qwen3.6 Flash", @@ -14573,23 +14590,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.6, - output: 2.5, - cacheRead: 0.3, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, "moonshotai/kimi-k2-thinking": { id: "moonshotai/kimi-k2-thinking", name: "Kimi K2 Thinking", @@ -16813,7 +16813,7 @@ export const MODELS = { } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", - name: "glm-5v-turbo", + name: "GLM-5V-Turbo", api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 17578c71..52a80409 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.0] - 2026-05-04 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index e0964afd..1001d5bd 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.0] - 2026-05-04 ### Fixed diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index f8790338..b03de5c9 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.0] - 2026-05-04 ## [0.72.1] - 2026-05-02 From bac2df36b9c4b01c9b17426d149b7cdc81a7daa4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 4 May 2026 22:37:01 +0200 Subject: [PATCH 07/30] fix(coding-agent): handle frontmatter prompts in print mode closes #4163 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/cli/args.ts | 5 +++++ packages/coding-agent/test/args.test.ts | 15 +++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 52a80409..b7eeb7e7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)). + ## [0.73.0] - 2026-05-04 ### New Features diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 6c451ccb..ad66b9e2 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -122,6 +122,11 @@ export function parseArgs(args: string[]): Args { } } else if (arg === "--print" || arg === "-p") { result.print = true; + const next = args[i + 1]; + if (next !== undefined && !next.startsWith("@") && (!next.startsWith("-") || next.startsWith("---"))) { + result.messages.push(next); + i++; + } } else if (arg === "--export" && i + 1 < args.length) { result.export = args[++i]; } else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) { diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index 3e53d7f6..ab3ce2a5 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -43,6 +43,21 @@ describe("parseArgs", () => { const result = parseArgs(["-p"]); expect(result.print).toBe(true); }); + + test("parses prompt after -p even when it starts with YAML frontmatter", () => { + const prompt = "---\ntitle: hello\n---\nSay hi."; + const result = parseArgs(["-p", prompt]); + expect(result.print).toBe(true); + expect(result.messages).toEqual([prompt]); + expect(result.unknownFlags.size).toBe(0); + }); + + test("does not consume options after -p as prompts", () => { + const result = parseArgs(["-p", "--provider", "openai", "Say hi."]); + expect(result.print).toBe(true); + expect(result.provider).toBe("openai"); + expect(result.messages).toEqual(["Say hi."]); + }); }); describe("--continue flag", () => { From bb25a3944c0a8747c99d4360d71eb659554d65a1 Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Mon, 4 May 2026 22:44:35 +0200 Subject: [PATCH 08/30] feat(coding-agent): allow comments and trailing commas in models.json (#4162) * feat(coding-agent): allow comments and trailing commas in models.json Run user-supplied models.json through a small `stripJsonComments` helper before JSON.parse so users can annotate their config and leave trailing commas without breaking the loader. Co-Authored-By: julien-agent * fix(coding-agent): strip comments before trailing commas in models.json The single-pass regex couldn't see a trailing comma when a `//` comment sat between the comma and its closer. Split into two passes: strip comments first, then strip trailing commas on the cleaned input. Co-Authored-By: julien-agent --------- Co-authored-by: julien-agent --- packages/coding-agent/src/core/model-registry.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 798b8f8c..f2584aa9 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -213,6 +213,13 @@ function formatValidationPath(error: TLocalizedValidationError): string { return path || "root"; } +/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */ +function stripJsonComments(input: string): string { + return input + .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : "")) + .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : "")); +} + /** Provider override config (baseUrl, compat) without request auth/headers */ interface ProviderOverride { baseUrl?: string; @@ -450,7 +457,7 @@ export class ModelRegistry { try { const content = readFileSync(modelsJsonPath, "utf-8"); - const parsed = JSON.parse(content) as unknown; + const parsed = JSON.parse(stripJsonComments(content)) as unknown; if (!validateModelsConfig.Check(parsed)) { const errors = From b5755fd27d115910249e8e29d48685569bfc8498 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 5 May 2026 13:16:56 +0200 Subject: [PATCH 09/30] feat(oauth): support interactive login selection (#4190) --- packages/ai/src/index.ts | 2 ++ packages/ai/src/utils/oauth/types.ts | 12 +++++++ .../interactive/components/login-dialog.ts | 3 +- .../src/modes/interactive/interactive-mode.ts | 31 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 9f996059..2502008a 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -34,6 +34,8 @@ export type { OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface, + OAuthSelectOption, + OAuthSelectPrompt, } from "./utils/oauth/types.js"; export * from "./utils/overflow.js"; export * from "./utils/typebox-helpers.js"; diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index e3520342..03cbf101 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -23,11 +23,23 @@ export type OAuthAuthInfo = { instructions?: string; }; +export type OAuthSelectOption = { + id: string; + label: string; +}; + +export type OAuthSelectPrompt = { + message: string; + options: OAuthSelectOption[]; +}; + export interface OAuthLoginCallbacks { onAuth: (info: OAuthAuthInfo) => void; onPrompt: (prompt: OAuthPrompt) => Promise; onProgress?: (message: string) => void; onManualCodeInput?: () => Promise; + /** Show an interactive selector and return the selected option id, or undefined on cancel. */ + onSelect?: (prompt: OAuthSelectPrompt) => Promise; signal?: AbortSignal; } diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 98d2096b..f12bd417 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -87,7 +87,8 @@ export class LoginDialogComponent extends Container implements Focusable { showAuth(url: string, instructions?: string): void { this.contentContainer.clear(); this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0)); + const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`; + this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 1, 0)); const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open"; const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 2fab6f56..05a01148 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -15,6 +15,7 @@ import { type Message, type Model, type OAuthProviderId, + type OAuthSelectPrompt, } from "@mariozechner/pi-ai"; import type { AutocompleteItem, @@ -4645,6 +4646,34 @@ export class InteractiveMode { } } + private showOAuthLoginSelect(dialog: LoginDialogComponent, prompt: OAuthSelectPrompt): Promise { + return new Promise((resolve) => { + const restoreDialog = () => { + this.editorContainer.clear(); + this.editorContainer.addChild(dialog); + this.ui.setFocus(dialog); + this.ui.requestRender(); + }; + const labels = prompt.options.map((option) => option.label); + const selector = new ExtensionSelectorComponent( + prompt.message, + labels, + (optionLabel) => { + restoreDialog(); + resolve(prompt.options.find((option) => option.label === optionLabel)?.id); + }, + () => { + restoreDialog(); + resolve(undefined); + }, + ); + this.editorContainer.clear(); + this.editorContainer.addChild(selector); + this.ui.setFocus(selector); + this.ui.requestRender(); + }); + } + private async showLoginDialog(providerId: string, providerName: string): Promise { const providerInfo = this.session.modelRegistry.authStorage .getOAuthProviders() @@ -4722,6 +4751,8 @@ export class InteractiveMode { dialog.showProgress(message); }, + onSelect: (prompt: OAuthSelectPrompt) => this.showOAuthLoginSelect(dialog, prompt), + onManualCodeInput: () => manualCodePromise, signal: dialog.signal, From 755da309dd3a1115abc7073c061da8229832acb0 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 5 May 2026 13:17:48 +0200 Subject: [PATCH 10/30] fix(coding-agent): keep pending tool renders after thinking toggle closes #4167 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/interactive/interactive-mode.ts | 12 +- ...hinking-toggle-pending-tool-render.test.ts | 164 ++++++++++++++++++ 3 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b7eeb7e7..fc1b6bca 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)). +- Fixed pending tool results not updating in the live TUI after toggling thinking block visibility while the tool is running ([#4167](https://github.com/badlogic/pi-mono/issues/4167)). ## [0.73.0] - 2026-05-04 diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 05a01148..4eecad9f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2633,6 +2633,7 @@ export class InteractiveMode { switch (event.type) { case "agent_start": + this.pendingTools.clear(); if (this.settingsManager.getShowTerminalProgress()) { this.ui.terminal.setProgress(true); } @@ -3100,6 +3101,7 @@ export class InteractiveMode { options: { updateFooter?: boolean; populateHistory?: boolean } = {}, ): void { this.pendingTools.clear(); + const renderedPendingTools = new Map(); if (options.updateFooter) { this.footer.invalidate(); @@ -3141,16 +3143,16 @@ export class InteractiveMode { } component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true }); } else { - this.pendingTools.set(content.id, component); + renderedPendingTools.set(content.id, component); } } } } else if (message.role === "toolResult") { // Match tool results to pending tool components - const component = this.pendingTools.get(message.toolCallId); + const component = renderedPendingTools.get(message.toolCallId); if (component) { component.updateResult(message); - this.pendingTools.delete(message.toolCallId); + renderedPendingTools.delete(message.toolCallId); } } else { // All other messages use standard rendering @@ -3158,7 +3160,9 @@ export class InteractiveMode { } } - this.pendingTools.clear(); + for (const [toolCallId, component] of renderedPendingTools) { + this.pendingTools.set(toolCallId, component); + } this.ui.requestRender(); } diff --git a/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts new file mode 100644 index 00000000..21a0114e --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts @@ -0,0 +1,164 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { AssistantMessage, ToolResultMessage, Usage } from "@mariozechner/pi-ai"; +import { Container, Text, type TUI } from "@mariozechner/pi-tui"; +import stripAnsi from "strip-ansi"; +import { beforeAll, describe, expect, test, vi } from "vitest"; +import type { AgentSessionEvent } from "../../../src/core/agent-session.js"; +import type { SessionContext } from "../../../src/core/session-manager.js"; +import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.js"; +import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.js"; +import { initTheme } from "../../../src/modes/interactive/theme/theme.js"; + +const TOOL_CALL_ID = "tool-4167"; +const TOOL_NAME = "slow_tool"; + +const EMPTY_USAGE: Usage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, +}; + +type RenderSessionContextThis = { + pendingTools: Map; + chatContainer: Container; + footer: { invalidate(): void }; + ui: TUI; + settingsManager: { + getShowImages(): boolean; + getImageWidthCells(): number; + }; + sessionManager: { getCwd(): string }; + session: { retryAttempt: number }; + toolOutputExpanded: boolean; + isInitialized: boolean; + updateEditorBorderColor(): void; + getRegisteredToolDefinition(toolName: string): undefined; + addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void; +}; + +type RenderSessionContext = ( + this: RenderSessionContextThis, + sessionContext: SessionContext, + options?: { updateFooter?: boolean; populateHistory?: boolean }, +) => void; + +type HandleEvent = (this: RenderSessionContextThis, event: AgentSessionEvent) => Promise; + +function createFakeInteractiveModeThis(): RenderSessionContextThis { + const chatContainer = new Container(); + return { + pendingTools: new Map(), + chatContainer, + footer: { invalidate: vi.fn() }, + ui: { requestRender: vi.fn() } as unknown as TUI, + settingsManager: { + getShowImages: () => false, + getImageWidthCells: () => 60, + }, + sessionManager: { getCwd: () => process.cwd() }, + session: { retryAttempt: 0 }, + toolOutputExpanded: false, + isInitialized: true, + updateEditorBorderColor: vi.fn(), + getRegisteredToolDefinition: (_toolName: string) => undefined, + addMessageToChat(message: AgentMessage) { + chatContainer.addChild(new Text(message.role, 0, 0)); + }, + }; +} + +function createAssistantToolCallMessage(): AssistantMessage { + return { + role: "assistant", + content: [ + { + type: "toolCall", + id: TOOL_CALL_ID, + name: TOOL_NAME, + arguments: { delayMs: 10_000 }, + }, + ], + api: "test-api", + provider: "test-provider", + model: "test-model", + usage: EMPTY_USAGE, + stopReason: "toolUse", + timestamp: Date.now(), + }; +} + +function createToolResultMessage(text: string): ToolResultMessage { + return { + role: "toolResult", + toolCallId: TOOL_CALL_ID, + toolName: TOOL_NAME, + content: [{ type: "text", text }], + isError: false, + timestamp: Date.now(), + }; +} + +function createSessionContext(messages: AgentMessage[]): SessionContext { + return { + messages, + thinkingLevel: "off", + model: null, + }; +} + +function renderChat(container: Container): string { + return stripAnsi(container.render(120).join("\n")); +} + +describe("InteractiveMode.renderSessionContext", () => { + beforeAll(() => { + initTheme("dark"); + }); + + test("keeps unresolved rendered tool calls registered for live completion events", async () => { + const fakeThis = createFakeInteractiveModeThis(); + const renderSessionContext = ( + InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext } + ).renderSessionContext; + const handleEvent = (InteractiveMode.prototype as unknown as { handleEvent: HandleEvent }).handleEvent; + + renderSessionContext.call(fakeThis, createSessionContext([createAssistantToolCallMessage()])); + + expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(true); + + await handleEvent.call(fakeThis, { + type: "tool_execution_end", + toolCallId: TOOL_CALL_ID, + toolName: TOOL_NAME, + result: { content: [{ type: "text", text: "FINAL_RESULT" }], details: undefined }, + isError: false, + }); + + expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(false); + expect(renderChat(fakeThis.chatContainer)).toContain("FINAL_RESULT"); + }); + + test("does not keep completed historical tool calls registered as pending", () => { + const fakeThis = createFakeInteractiveModeThis(); + const renderSessionContext = ( + InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext } + ).renderSessionContext; + + renderSessionContext.call( + fakeThis, + createSessionContext([createAssistantToolCallMessage(), createToolResultMessage("HISTORICAL_RESULT")]), + ); + + expect(fakeThis.pendingTools.size).toBe(0); + expect(renderChat(fakeThis.chatContainer)).toContain("HISTORICAL_RESULT"); + }); +}); From c806dea15ebfcf4144e794ad46c19a6cd3f585e2 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 5 May 2026 13:48:15 +0200 Subject: [PATCH 11/30] fix(tui): preserve OSC 8 hyperlink terminators --- packages/tui/CHANGELOG.md | 4 +++ packages/tui/src/utils.ts | 53 +++++++++++++++++++++++++---- packages/tui/test/wrap-ansi.test.ts | 15 ++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 1001d5bd..1444bbff 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed wrapped OSC 8 hyperlinks to preserve BEL terminators so OAuth login URLs remain clickable on every wrapped line. + ## [0.73.0] - 2026-05-04 ### Fixed diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index 59d21328..a6bcc8a5 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -312,6 +312,42 @@ export function extractAnsiCode(str: string, pos: number): { code: string; lengt return null; } +type Osc8Terminator = "\x07" | "\x1b\\"; + +interface ActiveHyperlink { + params: string; + url: string; + terminator: Osc8Terminator; +} + +function parseOsc8Hyperlink(ansiCode: string): ActiveHyperlink | null | undefined { + if (!ansiCode.startsWith("\x1b]8;")) { + return undefined; + } + + const terminator: Osc8Terminator = ansiCode.endsWith("\x07") ? "\x07" : "\x1b\\"; + const body = ansiCode.slice(4, terminator === "\x07" ? -1 : -2); + const separatorIndex = body.indexOf(";"); + if (separatorIndex === -1) { + return undefined; + } + + const params = body.slice(0, separatorIndex); + const url = body.slice(separatorIndex + 1); + if (!url) { + return null; + } + return { params, url, terminator }; +} + +function formatOsc8Hyperlink(hyperlink: ActiveHyperlink): string { + return `\x1b]8;${hyperlink.params};${hyperlink.url}${hyperlink.terminator}`; +} + +function formatOsc8Close(terminator: Osc8Terminator): string { + return `\x1b]8;;${terminator}`; +} + /** * Track active ANSI SGR codes to preserve styling across line breaks. */ @@ -327,13 +363,16 @@ class AnsiCodeTracker { private strikethrough = false; private fgColor: string | null = null; // Stores the full code like "31" or "38;5;240" private bgColor: string | null = null; // Stores the full code like "41" or "48;5;240" - private activeHyperlink: string | null = null; // Active OSC 8 hyperlink URL, or null + private activeHyperlink: ActiveHyperlink | null = null; process(ansiCode: string): void { - // OSC 8 hyperlink: \x1b]8;;\x1b\\ (open) or \x1b]8;;\x1b\\ (close) - if (ansiCode.startsWith("\x1b]8;")) { - const m = ansiCode.match(/^\x1b\]8;[^;]*;([^\x1b\x07]*)/); - this.activeHyperlink = m?.[1] ? m[1] : null; + // OSC 8 hyperlink: \x1b]8;;\x1b\\ (open) or \x1b]8;;\x1b\\ (close). + // Preserve the original terminator because some terminals only make BEL-terminated + // links clickable. OAuth login URLs use BEL, so reopening wrapped lines with ST + // made only the first physical line clickable in those terminals. + const hyperlink = parseOsc8Hyperlink(ansiCode); + if (hyperlink !== undefined) { + this.activeHyperlink = hyperlink; return; } @@ -495,7 +534,7 @@ class AnsiCodeTracker { let result = codes.length > 0 ? `\x1b[${codes.join(";")}m` : ""; if (this.activeHyperlink) { - result += `\x1b]8;;${this.activeHyperlink}\x1b\\`; + result += formatOsc8Hyperlink(this.activeHyperlink); } return result; } @@ -528,7 +567,7 @@ class AnsiCodeTracker { result += "\x1b[24m"; // Underline off only } if (this.activeHyperlink) { - result += "\x1b]8;;\x1b\\"; // Close hyperlink; re-opened at line start via getActiveCodes() + result += formatOsc8Close(this.activeHyperlink.terminator); // Re-opened at line start via getActiveCodes() } return result; } diff --git a/packages/tui/test/wrap-ansi.test.ts b/packages/tui/test/wrap-ansi.test.ts index e0944ded..bd24e815 100644 --- a/packages/tui/test/wrap-ansi.test.ts +++ b/packages/tui/test/wrap-ansi.test.ts @@ -191,6 +191,21 @@ describe("wrapTextWithAnsi with OSC 8 hyperlinks", () => { } }); + it("preserves BEL terminators when wrapping OAuth-style hyperlinks", () => { + const url = `https://example.com/oauth/${"a".repeat(32)}`; + const input = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`; + const lines = wrapTextWithAnsi(input, 20); + + assert.ok(lines.length > 1); + for (const line of lines) { + assert.ok(line.includes(`\x1b]8;;${url}\x07`), `Line "${line}" does not reopen the hyperlink with BEL`); + assert.ok(!line.includes(`\x1b]8;;${url}\x1b\\`), `Line "${line}" reopens the hyperlink with ST`); + } + for (const line of lines.slice(0, -1)) { + assert.ok(line.endsWith("\x1b]8;;\x07"), `Line "${line}" does not close the hyperlink with BEL`); + } + }); + it("does not emit OSC 8 sequences on lines that are outside the hyperlink", () => { const url = "https://example.com"; const input = `before \x1b]8;;${url}\x1b\\link\x1b]8;;\x1b\\ after`; From 060c10b8ffa14aa71ac632b75b90219cb3b77411 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 5 May 2026 14:47:51 +0200 Subject: [PATCH 12/30] fix(coding-agent): skip X11-only native addon for /copy on Linux The @mariozechner/clipboard native addon uses clipboard-rs, which is X11-only and does not retain selection ownership after set_text resolves. On Wayland-only compositors (Hyprland, Niri, ...) /copy reported success without populating the clipboard. Skip the native addon on Linux and let wl-copy/xclip/xsel handle text writes; they properly daemonize. closes #4177 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/utils/clipboard.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fc1b6bca..2f8621aa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)). - Fixed pending tool results not updating in the live TUI after toggling thinking block visibility while the tool is running ([#4167](https://github.com/badlogic/pi-mono/issues/4167)). +- Fixed `/copy` reporting success on Linux without writing the clipboard on Wayland-only compositors (Hyprland, Niri, ...) by skipping the X11-only native addon on Linux and routing through `wl-copy`/`xclip`/`xsel` instead ([#4177](https://github.com/badlogic/pi-mono/issues/4177)). ## [0.73.0] - 2026-05-04 diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index 4393679f..84166e95 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -35,11 +35,20 @@ function emitOsc52(text: string): boolean { export async function copyToClipboard(text: string): Promise { let copied = false; + const p = platform(); + // Prefer direct clipboard writes. Emitting OSC 52 first can make terminals // write the same native clipboard concurrently with the addon, and very large // OSC 52 payloads can desynchronize terminal rendering. + // + // On Linux, skip the native addon. The underlying `clipboard-rs` crate is + // X11-only and does not retain selection ownership after `set_text` + // resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even + // some X11 sessions the call resolves successfully without populating the + // clipboard. The platform tools below (wl-copy, xclip, xsel) properly + // daemonize and keep ownership. try { - if (clipboard) { + if (clipboard && p !== "linux") { await clipboard.setText(text); copied = true; } @@ -52,7 +61,6 @@ export async function copyToClipboard(text: string): Promise { return; } - const p = platform(); const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] }; if (!copied) { From 15aa313501ec7167241077adb28e5b1aab2c5505 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 5 May 2026 14:50:27 +0200 Subject: [PATCH 13/30] Closes #4184, codex needs a non-empty system prompt --- packages/ai/src/providers/openai-codex-responses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 80eaf496..dc6b6463 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -352,7 +352,7 @@ function buildRequestBody( model: model.id, store: false, stream: true, - instructions: context.systemPrompt, + instructions: context.systemPrompt || "You are a helpful assistant.", input: messages, text: { verbosity: options?.textVerbosity || "low" }, include: ["reasoning.encrypted_content"], From 33fcfa18dad2a1454a17d931c0e9719b254f1e21 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 5 May 2026 17:10:35 +0200 Subject: [PATCH 14/30] Closes #4182 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e2e244ec..12a09a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ packages/*/dist-firefox/ .vscode/ .zed/ .idea/ +.claude/ *.swp *.swo *~ From 31f5c23271f26047c615b2170ff9c0d80756fb26 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 6 May 2026 00:08:26 +0200 Subject: [PATCH 15/30] fix(ai): handle OpenAI Responses reasoning text deltas --- packages/ai/CHANGELOG.md | 5 +++++ .../ai/src/providers/openai-responses-shared.ts | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 365f4647..aecc4c55 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI Responses reasoning text streaming for LM Studio and other compatible providers that emit `response.reasoning_text.delta` events ([#4191](https://github.com/badlogic/pi-mono/pull/4191) by [@yaanfpv](https://github.com/yaanfpv)). +- Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)). + ## [0.73.0] - 2026-05-04 ### Breaking Changes diff --git a/packages/ai/src/providers/openai-responses-shared.ts b/packages/ai/src/providers/openai-responses-shared.ts index f25d6dcb..0d68cd7f 100644 --- a/packages/ai/src/providers/openai-responses-shared.ts +++ b/packages/ai/src/providers/openai-responses-shared.ts @@ -354,6 +354,16 @@ export async function processResponsesStream( }); } } + } else if (event.type === "response.reasoning_text.delta") { + if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") { + currentBlock.thinking += event.delta; + stream.push({ + type: "thinking_delta", + contentIndex: blockIndex(), + delta: event.delta, + partial: output, + }); + } } else if (event.type === "response.content_part.added") { if (currentItem?.type === "message") { currentItem.content = currentItem.content || []; @@ -429,7 +439,9 @@ export async function processResponsesStream( const item = event.item; if (item.type === "reasoning" && currentBlock?.type === "thinking") { - currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || ""; + const summaryText = item.summary?.map((s) => s.text).join("\n\n") || ""; + const contentText = item.content?.map((c) => c.text).join("\n\n") || ""; + currentBlock.thinking = summaryText || contentText || currentBlock.thinking; currentBlock.thinkingSignature = JSON.stringify(item); stream.push({ type: "thinking_end", From 3029836894d0f2429e2084d9043db984e92c3f0c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 6 May 2026 00:14:40 +0200 Subject: [PATCH 16/30] fix(ai): stop Codex OAuth stderr writes closes #4141 --- packages/ai/src/utils/oauth/openai-codex.ts | 45 ++++++++++++--------- packages/ai/test/openai-codex-oauth.test.ts | 32 +++++++++++++++ 2 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 packages/ai/test/openai-codex-oauth.test.ts diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 0bc705a5..62c4c352 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -30,7 +30,7 @@ const SCOPE = "openid profile email offline_access"; const JWT_CLAIM_PATH = "https://api.openai.com/auth"; type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number }; -type TokenFailure = { type: "failed" }; +type TokenFailure = { type: "failed"; message: string; status?: number }; type TokenResult = TokenSuccess | TokenFailure; type JwtPayload = { @@ -108,8 +108,11 @@ async function exchangeAuthorizationCode( if (!response.ok) { const text = await response.text().catch(() => ""); - console.error("[openai-codex] code->token failed:", response.status, text); - return { type: "failed" }; + return { + type: "failed", + status: response.status, + message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`, + }; } const json = (await response.json()) as { @@ -119,8 +122,10 @@ async function exchangeAuthorizationCode( }; if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") { - console.error("[openai-codex] token response missing fields:", json); - return { type: "failed" }; + return { + type: "failed", + message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`, + }; } return { @@ -145,8 +150,11 @@ async function refreshAccessToken(refreshToken: string): Promise { if (!response.ok) { const text = await response.text().catch(() => ""); - console.error("[openai-codex] Token refresh failed:", response.status, text); - return { type: "failed" }; + return { + type: "failed", + status: response.status, + message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`, + }; } const json = (await response.json()) as { @@ -156,8 +164,10 @@ async function refreshAccessToken(refreshToken: string): Promise { }; if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") { - console.error("[openai-codex] Token refresh response missing fields:", json); - return { type: "failed" }; + return { + type: "failed", + message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`, + }; } return { @@ -167,8 +177,10 @@ async function refreshAccessToken(refreshToken: string): Promise { expires: Date.now() + json.expires_in * 1000, }; } catch (error) { - console.error("[openai-codex] Token refresh error:", error); - return { type: "failed" }; + return { + type: "failed", + message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`, + }; } } @@ -258,12 +270,7 @@ function startLocalOAuthServer(state: string): Promise { waitForCode: () => waitForCodePromise, }); }) - .on("error", (err: NodeJS.ErrnoException) => { - console.error( - `[openai-codex] Failed to bind http://${CALLBACK_HOST}:1455 (`, - err.code, - ") Falling back to manual paste.", - ); + .on("error", (_err: NodeJS.ErrnoException) => { settleWait?.(null); resolve({ close: () => { @@ -386,7 +393,7 @@ export async function loginOpenAICodex(options: { const tokenResult = await exchangeAuthorizationCode(code, verifier); if (tokenResult.type !== "success") { - throw new Error("Token exchange failed"); + throw new Error(tokenResult.message); } const accountId = getAccountId(tokenResult.access); @@ -411,7 +418,7 @@ export async function loginOpenAICodex(options: { export async function refreshOpenAICodexToken(refreshToken: string): Promise { const result = await refreshAccessToken(refreshToken); if (result.type !== "success") { - throw new Error("Failed to refresh OpenAI Codex token"); + throw new Error(result.message); } const accountId = getAccountId(result.access); diff --git a/packages/ai/test/openai-codex-oauth.test.ts b/packages/ai/test/openai-codex-oauth.test.ts new file mode 100644 index 00000000..276abe1b --- /dev/null +++ b/packages/ai/test/openai-codex-oauth.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js"; + +describe("OpenAI Codex OAuth", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("does not write token refresh failures to stderr", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.stubGlobal( + "fetch", + vi.fn(async (): Promise => { + return new Response( + JSON.stringify({ + error: { + message: "Could not validate your token. Please try signing in again.", + type: "invalid_request_error", + }, + }), + { status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } }, + ); + }), + ); + + await expect(refreshOpenAICodexToken("invalid-refresh-token")).rejects.toThrow( + /OpenAI Codex token refresh failed \(401\).*Could not validate your token/, + ); + expect(consoleError).not.toHaveBeenCalled(); + }); +}); From c9e87d2aa628a0644d3f1a92cdae7e441b46ef29 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 May 2026 11:09:55 +0200 Subject: [PATCH 17/30] fix(tui): disable inline images in cmux Treat CMUX_WORKSPACE_ID like tmux/screen for terminal capabilities and document the issue link. fixes #4208 --- packages/tui/src/terminal-image.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index b300d8cf..a396b497 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -46,7 +46,10 @@ export function detectCapabilities(): TerminalCapabilities { // sequences differently). Force hyperlinks off whenever we detect them, even // when the outer terminal would otherwise support OSC 8. Image protocols are // also unreliable under tmux/screen, so leave `images: null` for safety. - const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen"); + // cmux currently also gets this conservative fallback due to image corruption: + // https://github.com/badlogic/pi-mono/issues/4208 + const inTmuxOrScreen = + !!process.env.TMUX || !!process.env.CMUX_WORKSPACE_ID || term.startsWith("tmux") || term.startsWith("screen"); if (inTmuxOrScreen) { const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; return { images: null, trueColor, hyperlinks: false }; From ace3fd7e301baf3bcc9d2de1ddf81533f3161954 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 May 2026 11:34:49 +0200 Subject: [PATCH 18/30] fix(ai): normalize kimi k2p6 alias to kimi-for-coding closes #4218 --- packages/ai/scripts/generate-models.ts | 12 +++++++----- packages/ai/src/models.generated.ts | 18 ------------------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index e9c17322..ab4cdd19 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -857,15 +857,17 @@ async function loadModelsDevData(): Promise[]> { const kimiModels = data["kimi-for-coding"].models as Record; const hasCanonicalModel = Object.prototype.hasOwnProperty.call(kimiModels, "kimi-for-coding"); + const kimiAliases = new Set(["k2p5", "k2p6"]); + for (const [modelId, model] of Object.entries(kimiModels)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; - // models.dev still exposes deprecated "k2p5" in some snapshots. - // Normalize to the canonical model id and drop duplicates when canonical exists. - if (modelId === "k2p5" && hasCanonicalModel) continue; + // models.dev may expose versioned aliases (e.g. k2p5/k2p6). + // Normalize aliases to the canonical model id and drop duplicates when canonical exists. + if (kimiAliases.has(modelId) && hasCanonicalModel) continue; - const normalizedId = modelId === "k2p5" ? "kimi-for-coding" : modelId; - const normalizedName = modelId === "k2p5" ? "Kimi For Coding" : m.name || normalizedId; + const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId; + const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : m.name || normalizedId; models.push({ id: normalizedId, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 73a41499..abca6a23 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -5819,24 +5819,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "kimi-coding": { - "k2p6": { - id: "k2p6", - name: "Kimi K2.6", - api: "anthropic-messages", - provider: "kimi-coding", - baseUrl: "https://api.kimi.com/coding", - headers: {"User-Agent":"KimiCLI/1.5"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 32768, - } satisfies Model<"anthropic-messages">, "kimi-for-coding": { id: "kimi-for-coding", name: "Kimi For Coding", From 88619669e2fc3efaf2697742608d0fe500ddba9d Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Wed, 6 May 2026 18:06:37 +0200 Subject: [PATCH 19/30] fix(coding-agent): strip skill wrapper XML from HTML export user messages (#4234) Skill slash commands store a structural ... wrapper in raw user messages. The TUI uses parseSkillBlock() to split this into separate SkillInvocationMessageComponent and UserMessageComponent siblings, but the HTML export renderer passed the full raw text through markdown, causing broken/dangling XML tags to appear in exported HTML. Add parseSkillBlock() to the export template and render skill-invocation and user-message as separate sibling blocks: - Sidebar tree shows skill name + user prompt separately - Content area shows a clickable skill-invocation block (collapsed by default, markdown content on expand) followed by the user message - Copy-link button preserved on the wrapper element - Toggle tools (O key) expands/collapses skill invocations alongside compaction and tool output blocks --- .../src/core/export-html/template.css | 46 +++++++++++- .../src/core/export-html/template.js | 72 +++++++++++++++++-- .../test/export-html-skill-block.test.ts | 40 +++++++++++ 3 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 packages/coding-agent/test/export-html-skill-block.test.ts diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template.css index ac292311..c807161e 100644 --- a/packages/coding-agent/src/core/export-html/template.css +++ b/packages/coding-agent/src/core/export-html/template.css @@ -204,6 +204,10 @@ color: var(--accent); } + .tree-role-skill { + color: var(--customMessageLabel); + } + .tree-role-assistant { color: var(--success); } @@ -383,7 +387,8 @@ } .user-message:hover .copy-link-btn, - .assistant-message:hover .copy-link-btn { + .assistant-message:hover .copy-link-btn, + .skill-user-entry:hover .copy-link-btn { opacity: 1; } @@ -786,6 +791,45 @@ font-weight: bold; } + /* Skill invocation - matches compaction style (clickable, collapsed by default) */ + .skill-invocation { + background: var(--customMessageBg); + border-radius: 4px; + padding: var(--line-height); + cursor: pointer; + } + + .skill-invocation-label { + color: var(--customMessageLabel); + font-weight: bold; + } + + .skill-invocation-collapsed { + color: var(--customMessageText); + } + + .skill-invocation-content { + display: none; + color: var(--customMessageText); + margin-top: var(--line-height); + } + + .skill-invocation.expanded .skill-invocation-collapsed { + display: none; + } + + .skill-invocation.expanded .skill-invocation-content { + display: block; + } + + .skill-invocation + .user-message { + margin-top: var(--line-height); + } + + .skill-user-entry { + position: relative; + } + /* Branch summary */ .branch-summary { background: var(--customMessageBg); diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 844f1eb3..cf28b935 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -313,6 +313,22 @@ return ''; } + /** + * Parse a skill block from message text. + * Returns null if the text doesn't contain a skill block. + * Matches the format: \n...\n\n\nuser message + */ + function parseSkillBlock(text) { + const match = text.match(/^\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/); + if (!match) return null; + return { + name: match[1], + location: match[2], + content: match[3], + userMessage: match[4]?.trim() || undefined, + }; + } + function getSearchableText(entry, label) { const parts = []; if (label) parts.push(label); @@ -613,7 +629,16 @@ case 'message': { const msg = entry.message; if (msg.role === 'user') { - const content = truncate(normalize(extractContent(msg.content))); + const rawContent = extractContent(msg.content); + const skillBlock = parseSkillBlock(rawContent); + if (skillBlock) { + let treeHtml = labelHtml + `skill: ${escapeHtml(skillBlock.name)}`; + if (skillBlock.userMessage) { + treeHtml += ` · user: ${escapeHtml(truncate(normalize(skillBlock.userMessage)))}`; + } + return treeHtml; + } + const content = truncate(normalize(rawContent)); return labelHtml + `user: ${escapeHtml(content)}`; } if (msg.role === 'assistant') { @@ -1140,8 +1165,46 @@ const msg = entry.message; if (msg.role === 'user') { - let html = `
${copyBtnHtml}${tsHtml}`; const content = msg.content; + const text = typeof content === 'string' ? content : + content.filter(c => c.type === 'text').map(c => c.text).join('\n'); + const skillBlock = parseSkillBlock(text); + + if (skillBlock) { + // Collect images from content array + const images = Array.isArray(content) ? content.filter(c => c.type === 'image') : []; + const hasUserContent = skillBlock.userMessage || images.length > 0; + let html = `
${copyBtnHtml}${tsHtml}`; + + // Skill invocation (collapsed by default, click to expand) + html += `
+
[skill] ${escapeHtml(skillBlock.name)}
+
${escapeHtml(skillBlock.name)} (click to expand)
+
${safeMarkedParse(skillBlock.content)}
+
`; + + // User message (separate block if present) + if (hasUserContent) { + html += '
'; + if (images.length > 0) { + html += '
'; + for (const img of images) { + html += ``; + } + html += '
'; + } + if (skillBlock.userMessage) { + html += `
${safeMarkedParse(skillBlock.userMessage)}
`; + } + html += '
'; + } + + html += '
'; + return html; + } + + // No skill block - normal user message + let html = `
${copyBtnHtml}${tsHtml}`; if (Array.isArray(content)) { const images = content.filter(c => c.type === 'image'); @@ -1154,8 +1217,6 @@ } } - const text = typeof content === 'string' ? content : - content.filter(c => c.type === 'text').map(c => c.text).join('\n'); if (text.trim()) { html += `
${safeMarkedParse(text)}
`; } @@ -1716,6 +1777,9 @@ document.querySelectorAll('.compaction').forEach(el => { el.classList.toggle('expanded', toolOutputsExpanded); }); + document.querySelectorAll('.skill-invocation').forEach(el => { + el.classList.toggle('expanded', toolOutputsExpanded); + }); }; const attachHeaderHandlers = () => { diff --git a/packages/coding-agent/test/export-html-skill-block.test.ts b/packages/coding-agent/test/export-html-skill-block.test.ts new file mode 100644 index 00000000..ae89decf --- /dev/null +++ b/packages/coding-agent/test/export-html-skill-block.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from "fs"; +import { describe, expect, it } from "vitest"; + +describe("export HTML skill block rendering", () => { + const templateJs = readFileSync(new URL("../src/core/export-html/template.js", import.meta.url), "utf-8"); + + it("strips skill wrapper XML from user message rendering", () => { + // Skill commands store a structural wrapper in the raw user message: + // \n...\n\n\nactual prompt + // The export renderer must detect that wrapper and render only the user-visible prompt, + // not the Pi-generated ... XML tags. + expect(templateJs).toMatch(/parseSkillBlock/); + expect(templateJs).toMatch(/skillBlock\.userMessage/); + }); + + it("renders skill invocation and user message as separate sibling blocks", () => { + // The skill block and user message should render as separate entry-level elements, + // matching the TUI layout where SkillInvocationMessageComponent and + // UserMessageComponent are siblings, not nested. + expect(templateJs).toMatch(/skill-invocation/); + + // When a skill block has a userMessage, the user-message div must be emitted + // as a separate block after the skill-invocation div, containing the user-authored text. + // Verify the code checks hasUserContent so the user-message div is only omitted + // when the skill block has no user prompt and no images. + expect(templateJs).toMatch(/hasUserContent/); + }); + + it("renders skill content as markdown, not raw text", () => { + // The skill block body is markdown (from the SKILL.md file). + // It should be rendered through safeMarkedParse, not escaped as raw text. + expect(templateJs).toMatch(/safeMarkedParse\(skillBlock\.content\)/); + }); + + it("shows skill name and user message in the sidebar tree", () => { + // The sidebar tree should display both the skill name and the user prompt, + // not just one or the other. + expect(templateJs).toMatch(/tree-role-skill/); + }); +}); From 6b271842e27aa00d70003877ae58a197b664681e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 May 2026 00:12:19 +0200 Subject: [PATCH 20/30] fix(ai): handle mixed chat completion deltas Fixes #4228 --- .../ai/src/providers/openai-completions.ts | 256 +++++++++--------- .../openai-completions-tool-choice.test.ts | 232 ++++++++++++++++ 2 files changed, 367 insertions(+), 121 deletions(-) diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index b4e19e5f..4f001514 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -160,45 +160,104 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA partialArgs?: string; streamIndex?: number; } + type StreamingBlock = TextContent | ThinkingContent | StreamingToolCallBlock; + type StreamingToolCallDelta = NonNullable[number]; - let currentBlock: TextContent | ThinkingContent | StreamingToolCallBlock | null = null; - const blocks = output.content; - const getContentIndex = (block: typeof currentBlock) => (block ? blocks.indexOf(block) : -1); - const currentContentIndex = () => getContentIndex(currentBlock); - const finishCurrentBlock = (block?: typeof currentBlock) => { - if (block) { - const contentIndex = getContentIndex(block); - if (contentIndex === -1) { - return; - } - if (block.type === "text") { - stream.push({ - type: "text_end", - contentIndex, - content: block.text, - partial: output, - }); - } else if (block.type === "thinking") { - stream.push({ - type: "thinking_end", - contentIndex, - content: block.thinking, - partial: output, - }); - } else if (block.type === "toolCall") { - block.arguments = parseStreamingJson(block.partialArgs); - // Finalize in-place and strip the scratch buffers so replay only - // carries parsed arguments. - delete block.partialArgs; - delete block.streamIndex; - stream.push({ - type: "toolcall_end", - contentIndex, - toolCall: block, - partial: output, - }); - } + let textBlock: TextContent | null = null; + let thinkingBlock: ThinkingContent | null = null; + const toolCallBlocksByIndex = new Map(); + const toolCallBlocksById = new Map(); + const blocks = output.content as StreamingBlock[]; + const getContentIndex = (block: StreamingBlock) => blocks.indexOf(block); + const finishBlock = (block: StreamingBlock) => { + const contentIndex = getContentIndex(block); + if (contentIndex === -1) { + return; } + if (block.type === "text") { + stream.push({ + type: "text_end", + contentIndex, + content: block.text, + partial: output, + }); + } else if (block.type === "thinking") { + stream.push({ + type: "thinking_end", + contentIndex, + content: block.thinking, + partial: output, + }); + } else if (block.type === "toolCall") { + block.arguments = parseStreamingJson(block.partialArgs); + // Finalize in-place and strip the scratch buffers so replay only + // carries parsed arguments. + delete block.partialArgs; + delete block.streamIndex; + stream.push({ + type: "toolcall_end", + contentIndex, + toolCall: block, + partial: output, + }); + } + }; + const ensureTextBlock = () => { + if (!textBlock) { + textBlock = { type: "text", text: "" }; + blocks.push(textBlock); + stream.push({ type: "text_start", contentIndex: getContentIndex(textBlock), partial: output }); + } + return textBlock; + }; + const ensureThinkingBlock = (thinkingSignature: string) => { + if (!thinkingBlock) { + thinkingBlock = { + type: "thinking", + thinking: "", + thinkingSignature, + }; + blocks.push(thinkingBlock); + stream.push({ type: "thinking_start", contentIndex: getContentIndex(thinkingBlock), partial: output }); + } + return thinkingBlock; + }; + const ensureToolCallBlock = (toolCall: StreamingToolCallDelta) => { + const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined; + let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined; + if (!block && toolCall.id) { + block = toolCallBlocksById.get(toolCall.id); + } + if (!block) { + block = { + type: "toolCall", + id: toolCall.id || "", + name: toolCall.function?.name || "", + arguments: {}, + partialArgs: "", + streamIndex, + }; + if (streamIndex !== undefined) { + toolCallBlocksByIndex.set(streamIndex, block); + } + if (toolCall.id) { + toolCallBlocksById.set(toolCall.id, block); + } + blocks.push(block); + stream.push({ + type: "toolcall_start", + contentIndex: getContentIndex(block), + partial: output, + }); + } + if (streamIndex !== undefined && block.streamIndex === undefined) { + block.streamIndex = streamIndex; + toolCallBlocksByIndex.set(streamIndex, block); + } + if (toolCall.id) { + toolCallBlocksById.set(toolCall.id, block); + } + return block; }; for await (const chunk of openaiStream) { @@ -237,22 +296,14 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA choice.delta.content !== undefined && choice.delta.content.length > 0 ) { - if (!currentBlock || currentBlock.type !== "text") { - finishCurrentBlock(currentBlock); - currentBlock = { type: "text", text: "" }; - output.content.push(currentBlock); - stream.push({ type: "text_start", contentIndex: currentContentIndex(), partial: output }); - } - - if (currentBlock.type === "text") { - currentBlock.text += choice.delta.content; - stream.push({ - type: "text_delta", - contentIndex: currentContentIndex(), - delta: choice.delta.content, - partial: output, - }); - } + const block = ensureTextBlock(); + block.text += choice.delta.content; + stream.push({ + type: "text_delta", + contentIndex: getContentIndex(block), + delta: choice.delta.content, + partial: output, + }); } // Some endpoints return reasoning in reasoning_content (llama.cpp), @@ -260,38 +311,24 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA // Use the first non-empty reasoning field to avoid duplication // (e.g., chutes.ai returns both reasoning_content and reasoning with same content) const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"]; + const deltaFields = choice.delta as Record; let foundReasoningField: string | null = null; for (const field of reasoningFields) { - if ( - (choice.delta as any)[field] !== null && - (choice.delta as any)[field] !== undefined && - (choice.delta as any)[field].length > 0 - ) { - if (!foundReasoningField) { - foundReasoningField = field; - break; - } + const value = deltaFields[field]; + if (typeof value === "string" && value.length > 0) { + foundReasoningField = field; + break; } } if (foundReasoningField) { - if (!currentBlock || currentBlock.type !== "thinking") { - finishCurrentBlock(currentBlock); - currentBlock = { - type: "thinking", - thinking: "", - thinkingSignature: foundReasoningField, - }; - output.content.push(currentBlock); - stream.push({ type: "thinking_start", contentIndex: currentContentIndex(), partial: output }); - } - - if (currentBlock.type === "thinking") { - const delta = (choice.delta as any)[foundReasoningField]; - currentBlock.thinking += delta; + const delta = deltaFields[foundReasoningField]; + if (typeof delta === "string" && delta.length > 0) { + const block = ensureThinkingBlock(foundReasoningField); + block.thinking += delta; stream.push({ type: "thinking_delta", - contentIndex: currentContentIndex(), + contentIndex: getContentIndex(block), delta, partial: output, }); @@ -300,52 +337,27 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (choice?.delta?.tool_calls) { for (const toolCall of choice.delta.tool_calls) { - const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined; - const sameToolCall = - currentBlock?.type === "toolCall" && - ((streamIndex !== undefined && currentBlock.streamIndex === streamIndex) || - (streamIndex === undefined && toolCall.id && currentBlock.id === toolCall.id)); - - if (!sameToolCall) { - finishCurrentBlock(currentBlock); - currentBlock = { - type: "toolCall", - id: toolCall.id || "", - name: toolCall.function?.name || "", - arguments: {}, - partialArgs: "", - streamIndex, - }; - output.content.push(currentBlock); - stream.push({ - type: "toolcall_start", - contentIndex: getContentIndex(currentBlock), - partial: output, - }); + const block = ensureToolCallBlock(toolCall); + if (!block.id && toolCall.id) { + block.id = toolCall.id; + toolCallBlocksById.set(toolCall.id, block); + } + if (!block.name && toolCall.function?.name) { + block.name = toolCall.function.name; } - const currentToolCallBlock = currentBlock?.type === "toolCall" ? currentBlock : null; - if (currentToolCallBlock) { - if (!currentToolCallBlock.id && toolCall.id) currentToolCallBlock.id = toolCall.id; - if (!currentToolCallBlock.name && toolCall.function?.name) { - currentToolCallBlock.name = toolCall.function.name; - } - if (currentToolCallBlock.streamIndex === undefined && streamIndex !== undefined) { - currentToolCallBlock.streamIndex = streamIndex; - } - let delta = ""; - if (toolCall.function?.arguments) { - delta = toolCall.function.arguments; - currentToolCallBlock.partialArgs += toolCall.function.arguments; - currentToolCallBlock.arguments = parseStreamingJson(currentToolCallBlock.partialArgs); - } - stream.push({ - type: "toolcall_delta", - contentIndex: getContentIndex(currentToolCallBlock), - delta, - partial: output, - }); + let delta = ""; + if (toolCall.function?.arguments) { + delta = toolCall.function.arguments; + block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments; + block.arguments = parseStreamingJson(block.partialArgs); } + stream.push({ + type: "toolcall_delta", + contentIndex: getContentIndex(block), + delta, + partial: output, + }); } } @@ -365,7 +377,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA } } - finishCurrentBlock(currentBlock); + for (const block of blocks) { + finishBlock(block); + } if (options?.signal?.aborted) { throw new Error("Request was aborted"); } diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 0fa00341..c9c8ec9f 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -552,6 +552,238 @@ describe("openai-completions tool_choice", () => { expect(toolCall).not.toHaveProperty("partialArgs"); }); + it("accumulates mixed content, reasoning, and parallel tool call deltas independently", async () => { + mockState.chunks = [ + { + id: "chatcmpl-mixed-deltas", + choices: [ + { + delta: { + content: "answer 1", + reasoning_content: "think 1", + tool_calls: [ + { + index: 0, + id: "tc_read_initial", + type: "function", + function: { name: "read", arguments: '{"path":"README' }, + }, + { + index: 1, + id: "tc_grep_initial", + type: "function", + function: { name: "grep", arguments: '{"pattern":"TODO' }, + }, + { + id: "tc_list_no_index", + type: "function", + function: { name: "list", arguments: '{"path":"packages' }, + }, + { + id: "tc_write_no_index", + type: "function", + function: { name: "write", arguments: '{"path":"out' }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: "chatcmpl-mixed-deltas", + choices: [ + { + delta: { + content: " answer 2", + tool_calls: [ + { + index: 1, + id: "tc_grep_changed", + type: "function", + function: { arguments: '","path":"src' }, + }, + { + id: "tc_write_no_index", + type: "function", + function: { arguments: '.txt","content":"ok"}' }, + }, + { + id: "tc_list_no_index", + type: "function", + function: { arguments: '/ai"}' }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: "chatcmpl-mixed-deltas", + choices: [ + { + delta: { + content: "\n", + reasoning_content: " think 2", + tool_calls: [ + { + index: 0, + id: "tc_read_changed", + type: "function", + function: { arguments: '.md"}' }, + }, + { + index: 1, + type: "function", + function: { arguments: '"}' }, + }, + ], + }, + finish_reason: "tool_calls", + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 8, + prompt_tokens_details: { cached_tokens: 0 }, + completion_tokens_details: { reasoning_tokens: 2 }, + }, + }, + ]; + + const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!; + const model = { ...baseModel, api: "openai-completions" } as const; + const tools: Tool[] = [ + { + name: "read", + description: "Read a file", + parameters: Type.Object({ path: Type.String() }), + }, + { + name: "grep", + description: "Search a file", + parameters: Type.Object({ pattern: Type.String(), path: Type.String() }), + }, + { + name: "list", + description: "List a directory", + parameters: Type.Object({ path: Type.String() }), + }, + { + name: "write", + description: "Write a file", + parameters: Type.Object({ path: Type.String(), content: Type.String() }), + }, + ]; + const s = streamSimple( + model, + { + messages: [ + { + role: "user", + content: "Think, answer, and use tools.", + timestamp: Date.now(), + }, + ], + tools, + }, + { apiKey: "test" }, + ); + + const eventTypes: string[] = []; + const toolEventsByContentIndex = new Map(); + for await (const event of s) { + eventTypes.push(event.type); + if (event.type === "toolcall_start" || event.type === "toolcall_delta" || event.type === "toolcall_end") { + const events = toolEventsByContentIndex.get(event.contentIndex) ?? []; + events.push(event.type); + toolEventsByContentIndex.set(event.contentIndex, events); + } + } + + const response = await s.result(); + expect(response.stopReason).toBe("toolUse"); + expect(eventTypes.filter((type) => type === "text_start")).toHaveLength(1); + expect(eventTypes.filter((type) => type === "text_delta")).toHaveLength(3); + expect(eventTypes.filter((type) => type === "text_end")).toHaveLength(1); + expect(eventTypes.filter((type) => type === "thinking_start")).toHaveLength(1); + expect(eventTypes.filter((type) => type === "thinking_delta")).toHaveLength(2); + expect(eventTypes.filter((type) => type === "thinking_end")).toHaveLength(1); + expect(eventTypes.filter((type) => type === "toolcall_start")).toHaveLength(4); + expect(eventTypes.filter((type) => type === "toolcall_delta")).toHaveLength(9); + expect(eventTypes.filter((type) => type === "toolcall_end")).toHaveLength(4); + expect(toolEventsByContentIndex.get(2)).toEqual([ + "toolcall_start", + "toolcall_delta", + "toolcall_delta", + "toolcall_end", + ]); + expect(toolEventsByContentIndex.get(3)).toEqual([ + "toolcall_start", + "toolcall_delta", + "toolcall_delta", + "toolcall_delta", + "toolcall_end", + ]); + expect(toolEventsByContentIndex.get(4)).toEqual([ + "toolcall_start", + "toolcall_delta", + "toolcall_delta", + "toolcall_end", + ]); + expect(toolEventsByContentIndex.get(5)).toEqual([ + "toolcall_start", + "toolcall_delta", + "toolcall_delta", + "toolcall_end", + ]); + + expect(response.content).toHaveLength(6); + expect(response.content[0]).toEqual({ type: "text", text: "answer 1 answer 2\n" }); + expect(response.content[1]).toEqual({ + type: "thinking", + thinking: "think 1 think 2", + thinkingSignature: "reasoning_content", + }); + const readCall = response.content[2]; + const grepCall = response.content[3]; + const listCall = response.content[4]; + const writeCall = response.content[5]; + expect(readCall.type).toBe("toolCall"); + expect(grepCall.type).toBe("toolCall"); + expect(listCall.type).toBe("toolCall"); + expect(writeCall.type).toBe("toolCall"); + if ( + readCall.type !== "toolCall" || + grepCall.type !== "toolCall" || + listCall.type !== "toolCall" || + writeCall.type !== "toolCall" + ) { + throw new Error("Expected toolCall content"); + } + expect(readCall.id).toBe("tc_read_initial"); + expect(readCall.name).toBe("read"); + expect(readCall.arguments).toEqual({ path: "README.md" }); + expect(readCall).not.toHaveProperty("streamIndex"); + expect(readCall).not.toHaveProperty("partialArgs"); + expect(grepCall.id).toBe("tc_grep_initial"); + expect(grepCall.name).toBe("grep"); + expect(grepCall.arguments).toEqual({ pattern: "TODO", path: "src" }); + expect(grepCall).not.toHaveProperty("streamIndex"); + expect(grepCall).not.toHaveProperty("partialArgs"); + expect(listCall.id).toBe("tc_list_no_index"); + expect(listCall.name).toBe("list"); + expect(listCall.arguments).toEqual({ path: "packages/ai" }); + expect(listCall).not.toHaveProperty("streamIndex"); + expect(listCall).not.toHaveProperty("partialArgs"); + expect(writeCall.id).toBe("tc_write_no_index"); + expect(writeCall.name).toBe("write"); + expect(writeCall.arguments).toEqual({ path: "out.txt", content: "ok" }); + expect(writeCall).not.toHaveProperty("streamIndex"); + expect(writeCall).not.toHaveProperty("partialArgs"); + }); + it("does not double-count reasoning tokens in completion usage", async () => { mockState.chunks = [ { From 50993d743d31d9961edd1cf4af0b2ac17dc858ed Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 7 May 2026 01:04:51 +0200 Subject: [PATCH 21/30] chore(coding-agent): switch back from fork to upstream jiti 2.7 (#4244) --- package-lock.json | 36 ++++--------------- package.json | 2 +- packages/coding-agent/package.json | 2 +- .../src/core/extensions/loader.ts | 3 +- 4 files changed, 9 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05e2b28c..e79b3a3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "packages/coding-agent/examples/extensions/sandbox" ], "dependencies": { - "@mariozechner/jiti": "^2.6.5", "@mariozechner/pi-coding-agent": "^0.30.2", "get-east-asian-width": "^1.4.0" }, @@ -27,6 +26,7 @@ "@typescript/native-preview": "7.0.0-dev.20260120.1", "concurrently": "^9.2.1", "husky": "^9.1.7", + "jiti": "^2.7.0", "shx": "^0.4.0", "tsx": "^4.20.3", "typescript": "^5.9.2" @@ -1783,19 +1783,6 @@ "node": ">= 10" } }, - "node_modules/@mariozechner/jiti": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz", - "integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==", - "license": "MIT", - "dependencies": { - "std-env": "^3.10.0", - "yoctocolors": "^2.1.2" - }, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/@mariozechner/mini-lit": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@mariozechner/mini-lit/-/mini-lit-0.2.1.tgz", @@ -5772,9 +5759,9 @@ "license": "ISC" }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -7370,6 +7357,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, "license": "MIT" }, "node_modules/string_decoder": { @@ -8266,18 +8254,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", @@ -8381,7 +8357,6 @@ "version": "0.73.0", "license": "MIT", "dependencies": { - "@mariozechner/jiti": "^2.6.2", "@mariozechner/pi-agent-core": "^0.73.0", "@mariozechner/pi-ai": "^0.73.0", "@mariozechner/pi-tui": "^0.73.0", @@ -8394,6 +8369,7 @@ "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", + "jiti": "^2.7.0", "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", diff --git a/package.json b/package.json index 332259c3..6079c785 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@typescript/native-preview": "7.0.0-dev.20260120.1", "concurrently": "^9.2.1", "husky": "^9.1.7", + "jiti": "^2.7.0", "tsx": "^4.20.3", "typescript": "^5.9.2", "shx": "^0.4.0" @@ -48,7 +49,6 @@ }, "version": "0.0.3", "dependencies": { - "@mariozechner/jiti": "^2.6.5", "@mariozechner/pi-coding-agent": "^0.30.2", "get-east-asian-width": "^1.4.0" }, diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 635de12e..b4f3ba7f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -38,7 +38,6 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/jiti": "^2.6.2", "@mariozechner/pi-agent-core": "^0.73.0", "@mariozechner/pi-ai": "^0.73.0", "@mariozechner/pi-tui": "^0.73.0", @@ -51,6 +50,7 @@ "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", + "jiti": "^2.7.0", "marked": "^15.0.12", "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index b72e0655..c01252dc 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -1,7 +1,6 @@ /** * Extension loader - loads TypeScript extension modules using jiti. * - * Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries. */ import * as fs from "node:fs"; @@ -9,12 +8,12 @@ import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import { createJiti } from "@mariozechner/jiti"; import * as _bundledPiAgentCore from "@mariozechner/pi-agent-core"; import * as _bundledPiAi from "@mariozechner/pi-ai"; import * as _bundledPiAiOauth from "@mariozechner/pi-ai/oauth"; import type { KeyId } from "@mariozechner/pi-tui"; import * as _bundledPiTui from "@mariozechner/pi-tui"; +import { createJiti } from "jiti/static"; // Static imports of packages that extensions may use. // These MUST be static so Bun bundles them into the compiled binary. // The virtualModules option then makes them available to extensions. From b8712457d2c46987b5f8ad33de5465d2cb3a47e3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 May 2026 10:34:37 +0200 Subject: [PATCH 22/30] fix(tui): keep kitty image redraws inside TUI Fixes #4208. --- packages/tui/src/components/image.ts | 26 ++++--- packages/tui/src/terminal-image.ts | 22 +++--- packages/tui/src/tui.ts | 75 +++++++++++++++++++- packages/tui/test/terminal-image.test.ts | 87 +++++++++++++++++++++++- packages/tui/test/tui-render.test.ts | 82 ++++++++++++++++++++++ 5 files changed, 273 insertions(+), 19 deletions(-) diff --git a/packages/tui/src/components/image.ts b/packages/tui/src/components/image.ts index ca76cddd..876996f3 100644 --- a/packages/tui/src/components/image.ts +++ b/packages/tui/src/components/image.ts @@ -1,4 +1,5 @@ import { + allocateImageId, getCapabilities, getImageDimensions, type ImageDimensions, @@ -26,6 +27,7 @@ export class Image implements Component { private theme: ImageTheme; private options: ImageOptions; private imageId?: number; + private readonly explicitImageId?: number; private cachedLines?: string[]; private cachedWidth?: number; @@ -42,12 +44,13 @@ export class Image implements Component { this.theme = theme; this.options = options; this.dimensions = dimensions || getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 }; + this.explicitImageId = options.imageId; this.imageId = options.imageId; } - /** Get the Kitty image ID used by this image (if any). */ + /** Get the explicit Kitty image ID configured for this image (if any). */ getImageId(): number | undefined { - return this.imageId; + return this.explicitImageId; } invalidate(): void { @@ -66,9 +69,13 @@ export class Image implements Component { let lines: string[]; if (caps.images) { + if (caps.images === "kitty" && this.imageId === undefined) { + this.imageId = allocateImageId(); + } const result = renderImage(this.base64Data, this.dimensions, { maxWidthCells: maxWidth, imageId: this.imageId, + moveCursor: false, }); if (result) { @@ -77,16 +84,19 @@ export class Image implements Component { this.imageId = result.imageId; } - // Return `rows` lines so TUI accounts for image height - // First (rows-1) lines are empty (TUI clears them) - // Last line: move cursor back up, then output image sequence + // Return `rows` lines so TUI accounts for image height. + // First (rows-1) lines are empty and cleared before the image is drawn. + // Last line: move cursor back up, draw the image, then move back down + // for Kitty (this component disables Kitty's terminal-side cursor movement) + // so TUI cursor accounting stays inside the scroll area. lines = []; for (let i = 0; i < result.rows - 1; i++) { lines.push(""); } - // Move cursor up to first row, then output image - const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : ""; - lines.push(moveUp + result.sequence); + const rowOffset = result.rows - 1; + const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : ""; + const moveDown = caps.images === "kitty" && rowOffset > 0 ? `\x1b[${rowOffset}B` : ""; + lines.push(moveUp + result.sequence + moveDown); } else { const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename); lines = [this.theme.fallbackColor(fallback)]; diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index a396b497..6a84716c 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -22,6 +22,8 @@ export interface ImageRenderOptions { preserveAspectRatio?: boolean; /** Kitty image ID. If provided, reuses/replaces existing image with this ID. */ imageId?: number; + /** Whether Kitty should apply its default cursor movement after placement. */ + moveCursor?: boolean; } let cachedCapabilities: TerminalCapabilities | null = null; @@ -46,10 +48,7 @@ export function detectCapabilities(): TerminalCapabilities { // sequences differently). Force hyperlinks off whenever we detect them, even // when the outer terminal would otherwise support OSC 8. Image protocols are // also unreliable under tmux/screen, so leave `images: null` for safety. - // cmux currently also gets this conservative fallback due to image corruption: - // https://github.com/badlogic/pi-mono/issues/4208 - const inTmuxOrScreen = - !!process.env.TMUX || !!process.env.CMUX_WORKSPACE_ID || term.startsWith("tmux") || term.startsWith("screen"); + const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen"); if (inTmuxOrScreen) { const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; return { images: null, trueColor, hyperlinks: false }; @@ -131,12 +130,15 @@ export function encodeKitty( columns?: number; rows?: number; imageId?: number; + /** Whether Kitty should apply its default cursor movement after placement. Default: true. */ + moveCursor?: boolean; } = {}, ): string { const CHUNK_SIZE = 4096; const params: string[] = ["a=T", "f=100", "q=2"]; + if (options.moveCursor === false) params.push("C=1"); if (options.columns) params.push(`c=${options.columns}`); if (options.rows) params.push(`r=${options.rows}`); if (options.imageId) params.push(`i=${options.imageId}`); @@ -173,7 +175,7 @@ export function encodeKitty( * Uses uppercase 'I' to also free the image data. */ export function deleteKittyImage(imageId: number): string { - return `\x1b_Ga=d,d=I,i=${imageId}\x1b\\`; + return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`; } /** @@ -181,7 +183,7 @@ export function deleteKittyImage(imageId: number): string { * Uses uppercase 'A' to also free the image data. */ export function deleteAllKittyImages(): string { - return `\x1b_Ga=d,d=A\x1b\\`; + return "\x1b_Ga=d,d=A,q=2\x1b\\"; } export function encodeITerm2( @@ -377,8 +379,12 @@ export function renderImage( const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions()); if (caps.images === "kitty") { - // Only use imageId if explicitly provided - static images don't need IDs - const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId: options.imageId }); + const sequence = encodeKitty(base64Data, { + columns: maxWidth, + rows, + imageId: options.imageId, + moveCursor: options.moveCursor, + }); return { sequence, rows, imageId: options.imageId }; } diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index fda30282..e4b22d5b 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -8,9 +8,26 @@ import * as path from "node:path"; import { performance } from "node:perf_hooks"; import { isKeyRelease, matchesKey } from "./keys.js"; import type { Terminal } from "./terminal.js"; -import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js"; +import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js"; import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js"; +const KITTY_SEQUENCE_PATTERN = /\x1b_G([^;\x1b]*);/g; + +function extractKittyImageIds(line: string): number[] { + const ids: number[] = []; + for (const match of line.matchAll(KITTY_SEQUENCE_PATTERN)) { + for (const param of match[1].split(",")) { + const [key, value] = param.split("=", 2); + if (key !== "i" || value === undefined) continue; + const id = Number(value); + if (Number.isInteger(id) && id > 0 && id <= 0xffffffff && !ids.includes(id)) { + ids.push(id); + } + } + } + return ids; +} + /** * Component interface - all components must implement this */ @@ -217,6 +234,7 @@ export class Container implements Component { export class TUI extends Container { public terminal: Terminal; private previousLines: string[] = []; + private previousKittyImageIds = new Set(); private previousWidth = 0; private previousHeight = 0; private focusedComponent: Component | null = null; @@ -806,6 +824,48 @@ export class TUI extends Container { return lines; } + private collectKittyImageIds(lines: string[]): Set { + const ids = new Set(); + for (const line of lines) { + for (const id of extractKittyImageIds(line)) { + ids.add(id); + } + } + return ids; + } + + private deleteKittyImages(ids: Iterable): string { + let buffer = ""; + for (const id of ids) { + buffer += deleteKittyImage(id); + } + return buffer; + } + + private expandLastChangedForKittyImages(firstChanged: number, lastChanged: number): number { + let expandedLastChanged = lastChanged; + for (let i = firstChanged; i < this.previousLines.length; i++) { + if (extractKittyImageIds(this.previousLines[i]).length > 0) { + expandedLastChanged = Math.max(expandedLastChanged, i); + } + } + return expandedLastChanged; + } + + private deleteChangedKittyImages(firstChanged: number, lastChanged: number): string { + if (firstChanged < 0 || lastChanged < firstChanged) return ""; + + const ids = new Set(); + const maxLine = Math.min(lastChanged, this.previousLines.length - 1); + for (let i = firstChanged; i <= maxLine; i++) { + for (const id of extractKittyImageIds(this.previousLines[i] ?? "")) { + ids.add(id); + } + } + + return this.deleteKittyImages(ids); + } + /** Splice overlay content into a base line at a specific column. Single-pass optimized. */ private compositeLineAt( baseLine: string, @@ -918,7 +978,10 @@ export class TUI extends Container { const fullRender = (clear: boolean): void => { this.fullRedrawCount += 1; let buffer = "\x1b[?2026h"; // Begin synchronized output - if (clear) buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback + if (clear) { + buffer += this.deleteKittyImages(this.previousKittyImageIds); + buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback + } for (let i = 0; i < newLines.length; i++) { if (i > 0) buffer += "\r\n"; buffer += newLines[i]; @@ -937,6 +1000,7 @@ export class TUI extends Container { this.previousViewportTop = Math.max(0, bufferLength - height); this.positionHardwareCursor(cursorPos, newLines.length); this.previousLines = newLines; + this.previousKittyImageIds = this.collectKittyImageIds(newLines); this.previousWidth = width; this.previousHeight = height; }; @@ -1003,6 +1067,9 @@ export class TUI extends Container { } lastChanged = newLines.length - 1; } + if (firstChanged !== -1) { + lastChanged = this.expandLastChangedForKittyImages(firstChanged, lastChanged); + } const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0; // No changes - but still need to update hardware cursor position if it moved @@ -1017,6 +1084,7 @@ export class TUI extends Container { if (firstChanged >= newLines.length) { if (this.previousLines.length > newLines.length) { let buffer = "\x1b[?2026h"; + buffer += this.deleteChangedKittyImages(firstChanged, lastChanged); // Move to end of new content (clamp to 0 for empty content) const targetRow = Math.max(0, newLines.length - 1); if (targetRow < prevViewportTop) { @@ -1052,6 +1120,7 @@ export class TUI extends Container { } this.positionHardwareCursor(cursorPos, newLines.length); this.previousLines = newLines; + this.previousKittyImageIds = this.collectKittyImageIds(newLines); this.previousWidth = width; this.previousHeight = height; this.previousViewportTop = prevViewportTop; @@ -1069,6 +1138,7 @@ export class TUI extends Container { // Render from first changed line to end // Build buffer with all updates wrapped in synchronized output let buffer = "\x1b[?2026h"; // Begin synchronized output + buffer += this.deleteChangedKittyImages(firstChanged, lastChanged); const prevViewportBottom = prevViewportTop + height - 1; const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged; if (moveTargetRow > prevViewportBottom) { @@ -1199,6 +1269,7 @@ export class TUI extends Container { this.positionHardwareCursor(cursorPos, newLines.length); this.previousLines = newLines; + this.previousKittyImageIds = this.collectKittyImageIds(newLines); this.previousWidth = width; this.previousHeight = height; } diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index dd67e6bf..bdb2220c 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -4,7 +4,19 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { detectCapabilities, hyperlink, isImageLine } from "../src/terminal-image.js"; +import { Image } from "../src/components/image.js"; +import { + deleteAllKittyImages, + deleteKittyImage, + detectCapabilities, + encodeKitty, + hyperlink, + isImageLine, + renderImage, + resetCapabilitiesCache, + setCapabilities, + setCellDimensions, +} from "../src/terminal-image.js"; const ENV_KEYS = [ "TERM", @@ -15,6 +27,7 @@ const ENV_KEYS = [ "GHOSTTY_RESOURCES_DIR", "WEZTERM_PANE", "ITERM_SESSION_ID", + "CMUX_WORKSPACE_ID", ] as const; function withEnv(overrides: Record, fn: () => void): void { @@ -223,6 +236,14 @@ describe("detectCapabilities", () => { }); }); + it("does not disable Ghostty images solely because cmux is present", () => { + withEnv({ TERM_PROGRAM: "ghostty", CMUX_WORKSPACE_ID: "workspace" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.images, "kitty"); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + it("enables hyperlinks for Kitty", () => { withEnv({ KITTY_WINDOW_ID: "1" }, () => { const caps = detectCapabilities(); @@ -252,6 +273,70 @@ describe("detectCapabilities", () => { }); }); +describe("Kitty image cursor movement", () => { + it("can request no terminal-side cursor movement", () => { + const sequence = encodeKitty("AAAA", { columns: 2, rows: 2, moveCursor: false }); + assert.ok(sequence.startsWith("\x1b_Ga=T,f=100,q=2,C=1,c=2,r=2;")); + }); + + it("suppresses Kitty replies for delete commands", () => { + assert.strictEqual(deleteKittyImage(42), "\x1b_Ga=d,d=I,i=42,q=2\x1b\\"); + assert.strictEqual(deleteAllKittyImages(), "\x1b_Ga=d,d=A,q=2\x1b\\"); + }); + + it("preserves renderImage's default terminal-side cursor movement", () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const result = renderImage("AAAA", { widthPx: 20, heightPx: 20 }, { maxWidthCells: 2 }); + assert.ok(result); + assert.ok(!result.sequence.includes(",C=1,")); + assert.strictEqual(result.rows, 2); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + + it("can opt renderImage into no terminal-side cursor movement", () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const result = renderImage("AAAA", { widthPx: 20, heightPx: 20 }, { maxWidthCells: 2, moveCursor: false }); + assert.ok(result); + assert.ok(result.sequence.includes(",C=1,")); + assert.strictEqual(result.rows, 2); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + + it("restores the cursor to the reserved image row after Kitty rendering", () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 2 }, + { widthPx: 20, heightPx: 20 }, + ); + const lines = image.render(4); + assert.strictEqual(image.getImageId(), undefined); + assert.deepStrictEqual(lines.slice(0, -1), [""]); + assert.ok(lines[1].startsWith("\x1b[1A\x1b_G")); + assert.ok(lines[1].includes(",C=1,")); + assert.ok(lines[1].includes(",i=")); + assert.ok(lines[1].endsWith("\x1b[1B")); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); +}); + describe("hyperlink", () => { it("wraps text in OSC 8 open and close sequences", () => { const result = hyperlink("click me", "https://example.com"); diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index 4357a367..91cb9695 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; +import { deleteKittyImage, encodeKitty } from "../src/terminal-image.js"; import { type Component, TUI } from "../src/tui.js"; import { VirtualTerminal } from "./virtual-terminal.js"; @@ -63,6 +64,87 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num return cell.isItalic(); } +describe("TUI Kitty image cleanup", () => { + it("deletes changed image ids before drawing moved placements", async () => { + const terminal = new LoggingVirtualTerminal(40, 10); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + const oldImage = encodeKitty("AAAA", { columns: 2, rows: 2, imageId: 42, moveCursor: false }); + component.lines = ["top", oldImage]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + + const newImage = encodeKitty("BBBB", { columns: 2, rows: 1, imageId: 42, moveCursor: false }); + component.lines = [newImage, ""]; + tui.requestRender(); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + const deleteIndex = writes.indexOf(deleteKittyImage(42)); + const drawIndex = writes.indexOf(newImage); + assert.ok(deleteIndex >= 0, "changed old image should be deleted"); + assert.ok(drawIndex >= 0, "new image should be drawn"); + assert.ok(deleteIndex < drawIndex, "old image must be deleted before the new placement is drawn"); + + tui.stop(); + }); + + it("redraws image lines when an earlier reserved image row changes", async () => { + const terminal = new LoggingVirtualTerminal(40, 10); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + const image = encodeKitty("AAAA", { columns: 2, rows: 2, imageId: 88, moveCursor: false }); + component.lines = ["", image]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + + component.lines = ["covered", image]; + tui.requestRender(); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + const deleteIndex = writes.indexOf(deleteKittyImage(88)); + const drawIndex = writes.indexOf(image); + assert.ok(deleteIndex >= 0, "image should be deleted when a reserved row changes"); + assert.ok(drawIndex >= 0, "unchanged image line should be redrawn after deleting the placement"); + assert.ok(deleteIndex < drawIndex, "old placement must be deleted before the image line is redrawn"); + assert.ok(!writes.includes("\x1b[2J"), "reserved row changes should not force a full redraw"); + + tui.stop(); + }); + + it("deletes previously rendered image ids during full redraws", async () => { + const terminal = new LoggingVirtualTerminal(40, 10); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = [encodeKitty("AAAA", { columns: 2, rows: 2, imageId: 77, moveCursor: false })]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + + component.lines = ["plain text"]; + tui.requestRender(true); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + const deleteIndex = writes.indexOf(deleteKittyImage(77)); + const clearIndex = writes.indexOf("\x1b[2J"); + assert.ok(deleteIndex >= 0, "previous image should be deleted during full redraw"); + assert.ok(clearIndex >= 0, "full redraw should clear the screen"); + assert.ok(deleteIndex < clearIndex, "old image should be deleted before the screen is cleared"); + + tui.stop(); + }); +}); + describe("TUI resize handling", () => { it("triggers full re-render when terminal height changes", async () => { await withEnv({ TERMUX_VERSION: undefined }, async () => { From 83a917073c986673839efc66ca6b473e32e91152 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 May 2026 10:40:15 +0200 Subject: [PATCH 23/30] fix(tui): remove explicit image ids --- packages/tui/src/components/image.ts | 6 ++---- packages/tui/test/terminal-image.test.ts | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/tui/src/components/image.ts b/packages/tui/src/components/image.ts index 876996f3..74992b34 100644 --- a/packages/tui/src/components/image.ts +++ b/packages/tui/src/components/image.ts @@ -27,7 +27,6 @@ export class Image implements Component { private theme: ImageTheme; private options: ImageOptions; private imageId?: number; - private readonly explicitImageId?: number; private cachedLines?: string[]; private cachedWidth?: number; @@ -44,13 +43,12 @@ export class Image implements Component { this.theme = theme; this.options = options; this.dimensions = dimensions || getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 }; - this.explicitImageId = options.imageId; this.imageId = options.imageId; } - /** Get the explicit Kitty image ID configured for this image (if any). */ + /** Get the Kitty image ID used by this image (if any). */ getImageId(): number | undefined { - return this.explicitImageId; + return this.imageId; } invalidate(): void { diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index bdb2220c..58e018d2 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -324,11 +324,12 @@ describe("Kitty image cursor movement", () => { { widthPx: 20, heightPx: 20 }, ); const lines = image.render(4); - assert.strictEqual(image.getImageId(), undefined); + const imageId = image.getImageId(); + assert.strictEqual(typeof imageId, "number"); assert.deepStrictEqual(lines.slice(0, -1), [""]); assert.ok(lines[1].startsWith("\x1b[1A\x1b_G")); assert.ok(lines[1].includes(",C=1,")); - assert.ok(lines[1].includes(",i=")); + assert.ok(lines[1].includes(`,i=${imageId}`)); assert.ok(lines[1].endsWith("\x1b[1B")); } finally { resetCapabilitiesCache(); From 6d4d2e928807d90539c608fd72d6bf5604ee01fa Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 11:13:34 +0200 Subject: [PATCH 24/30] add tool stats script --- scripts/tool-stats.ts | 232 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100755 scripts/tool-stats.ts diff --git a/scripts/tool-stats.ts b/scripts/tool-stats.ts new file mode 100755 index 00000000..8d39b868 --- /dev/null +++ b/scripts/tool-stats.ts @@ -0,0 +1,232 @@ +#!/usr/bin/env npx tsx + +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { spawn } from "node:child_process"; + +interface TextContent { type: "text"; text: string } +interface ImageContent { type: "image"; data: string; mimeType?: string } +interface ToolCallContent { type: "toolCall"; id: string; name: string; arguments?: Record } +type Content = TextContent | ImageContent | ToolCallContent | { type: string; [key: string]: unknown }; +interface Message { role?: string; content?: string | Content[]; toolCallId?: string; toolName?: string; details?: unknown } +interface Entry { type?: string; message?: Message } +interface ToolStats { calls: number; results: number; estimatedTokens: number; samples: number[]; errors: number } +interface BashCommandStats { calls: number; estimatedTokens: number; samples: number[] } +interface ToolCallInfo { toolName: string; bashCommand?: string } + +const BUCKETS = [0, 50, 100, 250, 500, 1000, 2000, 4000, 8000, 16000, 32000, Number.POSITIVE_INFINITY]; + +function parseArgs(): { sessionsDir: string; output: string } { + let sessionsDir = join(homedir(), ".pi", "agent", "sessions"); + let output = join(tmpdir(), "pi-tool-stats.html"); + const args = process.argv.slice(2); + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--sessions-dir" && args[i + 1]) sessionsDir = resolve(args[++i]); + else if ((arg === "--output" || arg === "-o") && args[i + 1]) output = resolve(args[++i]); + else if (arg === "--help" || arg === "-h") { + console.log(`Usage: scripts/tool-stats.ts [--sessions-dir ] [--output ]`); + process.exit(0); + } + } + return { sessionsDir, output }; +} + +function jsonlFiles(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) out.push(...jsonlFiles(path)); + else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path); + } + return out; +} + +function getStats(map: Map, key: string, create: () => T): T { + let stats = map.get(key); + if (!stats) { + stats = create(); + map.set(key, stats); + } + return stats; +} + +function createToolStats(): ToolStats { + return { calls: 0, results: 0, estimatedTokens: 0, samples: [], errors: 0 }; +} + +function createBashStats(): BashCommandStats { + return { calls: 0, estimatedTokens: 0, samples: [] }; +} + +function estimateTokenCount(text: string): number { + return Math.ceil(text.length / 4); +} + +function contentText(content: Message["content"]): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content.map((block) => { + if (block.type === "text" && "text" in block && typeof block.text === "string") return block.text; + if (block.type === "image" && "data" in block && typeof block.data === "string") return block.data; + return JSON.stringify(block); + }).join("\n"); +} + +function getBashCommand(args: Record | undefined): string | undefined { + const command = args?.command; + return typeof command === "string" ? command : undefined; +} + +function commandKey(command: string): string { + const first = command.split(/\n|&&|\|\||;|\|/)[0]?.trim() ?? command.trim(); + const match = first.match(/^(?:\w+=\S+\s+)*(?:sudo\s+)?([^\s]+)(?:\s+([^\s]+))?/); + if (!match) return "unknown"; + const bin = match[1] ?? "unknown"; + const sub = match[2] && !match[2].startsWith("-") ? ` ${match[2]}` : ""; + return `${bin}${sub}`; +} + +function bucketCounts(samples: number[]): number[] { + const counts = new Array(BUCKETS.length - 1).fill(0) as number[]; + for (const sample of samples) { + const index = BUCKETS.findIndex((max, i) => i > 0 && sample <= max) - 1; + counts[Math.max(0, index)]++; + } + return counts; +} + +function bucketLabels(): string[] { + return BUCKETS.slice(0, -1).map((min, i) => { + const max = BUCKETS[i + 1]; + return Number.isFinite(max) ? `${min}-${max}` : `${min}+`; + }); +} + +const { sessionsDir, output } = parseArgs(); +if (!existsSync(sessionsDir)) throw new Error(`Sessions directory not found: ${sessionsDir}`); + +const tools = new Map(); +const bashCommands = new Map(); +const callsById = new Map(); +let parseErrors = 0; +const files = jsonlFiles(sessionsDir); + +for (const file of files) { + for (const line of readFileSync(file, "utf8").split("\n")) { + if (!line.trim()) continue; + let entry: Entry; + try { entry = JSON.parse(line) as Entry; } catch { parseErrors++; continue; } + if (entry.type !== "message") continue; + const message = entry.message; + if (!message) continue; + if (message.role === "assistant" && Array.isArray(message.content)) { + for (const block of message.content) { + if (block.type !== "toolCall" || !("name" in block) || typeof block.name !== "string") continue; + const stats = getStats(tools, block.name, createToolStats); + stats.calls++; + const bashCommand = block.name === "bash" ? getBashCommand(block.arguments) : undefined; + callsById.set(block.id, { toolName: block.name, bashCommand }); + if (bashCommand) getStats(bashCommands, commandKey(bashCommand), createBashStats).calls++; + } + } else if (message.role === "toolResult" && message.toolName) { + const text = contentText(message.content); + const tokens = estimateTokenCount(text); + const stats = getStats(tools, message.toolName, createToolStats); + stats.results++; + stats.estimatedTokens += tokens; + stats.samples.push(tokens); + if ("isError" in message && message.isError === true) stats.errors++; + const call = message.toolCallId ? callsById.get(message.toolCallId) : undefined; + if (call?.bashCommand) { + const bash = getStats(bashCommands, commandKey(call.bashCommand), createBashStats); + bash.estimatedTokens += tokens; + bash.samples.push(tokens); + } + } + } +} + +const toolRows = [...tools.entries()].map(([name, s]) => ({ name, ...s, avg: s.results ? s.estimatedTokens / s.results : 0, histogram: bucketCounts(s.samples) })).sort((a, b) => b.estimatedTokens - a.estimatedTokens); +const bashRows = [...bashCommands.entries()].map(([name, s]) => ({ name, ...s, avg: s.samples.length ? s.estimatedTokens / s.samples.length : 0, histogram: bucketCounts(s.samples) })).sort((a, b) => b.estimatedTokens - a.estimatedTokens).slice(0, 50); +const data = { generatedAt: new Date().toISOString(), sessionsDir, files: files.length, parseErrors, bucketLabels: bucketLabels(), tools: toolRows, bashCommands: bashRows }; + +const html = ` + + + + Pi Tool Stats + + + + +
+

Pi Tool Stats

+

${data.files} session files from ${sessionsDir}. Generated ${data.generatedAt}.

+
+

Estimated result tokens by tool

+

Tool calls

+
+
+
+
+

Tool result token histogram

+ +
+

+ +
+
+
+

Bash result token histogram

+ +
+

+ +
+
+

Tools

+
+

Bash common commands (best effort)

+
+
+
+ + +`; + +mkdirSync(resolve(output, ".."), { recursive: true }); +writeFileSync(output, html); +console.log(`Wrote ${output}`); +spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? ["/c", "start", output] : [output], { detached: true, stdio: "ignore" }).unref(); From 9eb126e7b2fe3a539b653e9c846638ce15f32ee1 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 11:42:35 +0200 Subject: [PATCH 25/30] docs(ai): document interleaved stream events --- packages/ai/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ai/README.md b/packages/ai/README.md index 51b3ef53..df8136a4 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -383,6 +383,8 @@ All streaming events emitted during assistant message generation: | `done` | Stream complete | `reason`: Stop reason ("stop", "length", "toolUse"), `message`: Final assistant message | | `error` | Error occurred | `reason`: Error type ("error" or "aborted"), `error`: AssistantMessage with partial content | +Streaming events for different content blocks are not guaranteed to be contiguous. Providers may emit deltas for text, thinking, and tool calls in the same upstream chunk, and pi may surface corresponding events interleaved, for example `text_start`, `text_delta`, `toolcall_start`, `text_delta`, `toolcall_delta`. Consumers must use `contentIndex` to associate each delta/end event with its block and must not assume that a block's `*_start`/`*_delta`/`*_end` sequence is uninterrupted by events for other blocks. + ## Image Input Models with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored. From 801db80b65210b25462bce1700675e073fe3dbe5 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 12:21:29 +0200 Subject: [PATCH 26/30] fix(tui): bound kitty image id parsing --- packages/tui/src/tui.ts | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index e4b22d5b..824ac82f 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -11,21 +11,26 @@ import type { Terminal } from "./terminal.js"; import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js"; import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js"; -const KITTY_SEQUENCE_PATTERN = /\x1b_G([^;\x1b]*);/g; +const KITTY_SEQUENCE_PREFIX = "\x1b_G"; function extractKittyImageIds(line: string): number[] { - const ids: number[] = []; - for (const match of line.matchAll(KITTY_SEQUENCE_PATTERN)) { - for (const param of match[1].split(",")) { - const [key, value] = param.split("=", 2); - if (key !== "i" || value === undefined) continue; - const id = Number(value); - if (Number.isInteger(id) && id > 0 && id <= 0xffffffff && !ids.includes(id)) { - ids.push(id); - } + const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX); + if (sequenceStart === -1) return []; + + const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length; + const paramsEnd = line.indexOf(";", paramsStart); + if (paramsEnd === -1) return []; + + const params = line.slice(paramsStart, paramsEnd); + for (const param of params.split(",")) { + const [key, value] = param.split("=", 2); + if (key !== "i" || value === undefined) continue; + const id = Number(value); + if (Number.isInteger(id) && id > 0 && id <= 0xffffffff) { + return [id]; } } - return ids; + return []; } /** From 5e1e4c3c88329fcd34366dc9cda6d3debeacb33f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 May 2026 16:11:06 +0200 Subject: [PATCH 27/30] feat(coding-agent): support renamed self-update package --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/config.ts | 80 +++++++-- .../coding-agent/src/package-manager-cli.ts | 83 +++++---- .../coding-agent/src/utils/version-check.ts | 25 ++- packages/coding-agent/test/config.test.ts | 158 ++++++++++++++++++ .../test/package-command-paths.test.ts | 158 +++++++++++++++++- .../coding-agent/test/version-check.test.ts | 8 + 7 files changed, 460 insertions(+), 56 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f8621aa..79ca11e6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Changed `pi update --self` to honor the active package name returned by the Pi version check endpoint, defaulting to the current package when omitted and uninstalling the old global package before installing a renamed package. + ### Fixed - Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)). diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 0d961655..869f4024 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -28,12 +28,36 @@ export const isBunRuntime = !!process.versions.bun; export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown"; -export interface SelfUpdateCommand { +interface SelfUpdateCommandStep { command: string; args: string[]; display: string; } +export interface SelfUpdateCommand extends SelfUpdateCommandStep { + steps?: SelfUpdateCommandStep[]; +} + +function makeSelfUpdateCommand( + installStep: SelfUpdateCommandStep, + uninstallStep?: SelfUpdateCommandStep, +): SelfUpdateCommand { + if (!uninstallStep) return installStep; + return { + ...installStep, + display: `${uninstallStep.display} && ${installStep.display}`, + steps: [uninstallStep, installStep], + }; +} + +function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep { + return { + command, + args, + display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "), + }; +} + export function detectInstallMethod(): InstallMethod { if (isBunBinary) { return "bun-binary"; @@ -83,24 +107,44 @@ function getInferredNpmInstall(packageName: string): { root: string; prefix: str function getSelfUpdateCommandForMethod( method: InstallMethod, - packageName: string, + installedPackageName: string, + updatePackageName = installedPackageName, npmCommand?: string[], ): SelfUpdateCommand | undefined { switch (method) { case "bun-binary": return undefined; case "pnpm": - return { command: "pnpm", args: ["install", "-g", packageName], display: `pnpm install -g ${packageName}` }; + return makeSelfUpdateCommand( + makeSelfUpdateCommandStep("pnpm", ["install", "-g", updatePackageName]), + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), + ); case "yarn": - return { command: "yarn", args: ["global", "add", packageName], display: `yarn global add ${packageName}` }; + return makeSelfUpdateCommand( + makeSelfUpdateCommandStep("yarn", ["global", "add", updatePackageName]), + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), + ); case "bun": - return { command: "bun", args: ["install", "-g", packageName], display: `bun install -g ${packageName}` }; + return makeSelfUpdateCommand( + makeSelfUpdateCommandStep("bun", ["install", "-g", updatePackageName]), + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), + ); case "npm": { const [command = "npm", ...npmArgs] = npmCommand ?? []; - const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(packageName); - const args = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : []), "install", "-g", packageName]; - const display = [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "); - return { command, args, display }; + const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(installedPackageName); + const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])]; + const installStep = makeSelfUpdateCommandStep(command, [...prefixArgs, "install", "-g", updatePackageName]); + const uninstallStep = + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]); + return makeSelfUpdateCommand(installStep, uninstallStep); } case "unknown": return undefined; @@ -210,28 +254,36 @@ function isManagedByGlobalPackageManager(method: InstallMethod, packageName: str ); } -export function getSelfUpdateCommand(packageName: string, npmCommand?: string[]): SelfUpdateCommand | undefined { +export function getSelfUpdateCommand( + packageName: string, + npmCommand?: string[], + updatePackageName = packageName, +): SelfUpdateCommand | undefined { const method = detectInstallMethod(); - const command = getSelfUpdateCommandForMethod(method, packageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { return undefined; } return command; } -export function getSelfUpdateUnavailableInstruction(packageName: string, npmCommand?: string[]): string { +export function getSelfUpdateUnavailableInstruction( + packageName: string, + npmCommand?: string[], + updatePackageName = packageName, +): string { const method = detectInstallMethod(); if (method === "bun-binary") { return `Download from: https://github.com/badlogic/pi-mono/releases/latest`; } - const command = getSelfUpdateCommandForMethod(method, packageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); if (command) { if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) { return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; } return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; } - return `Update ${packageName} using the package manager, wrapper, or source checkout that provides this installation.`; + return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`; } export function getUpdateInstruction(packageName: string): string { diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index d9153ac7..a41fb7a6 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -13,7 +13,7 @@ import { import { DefaultPackageManager } from "./core/package-manager.js"; import { SettingsManager } from "./core/settings-manager.js"; import { shouldUseWindowsShell } from "./utils/child-process.js"; -import { getLatestPiVersion, isNewerPackageVersion } from "./utils/version-check.js"; +import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; export type PackageCommand = "install" | "remove" | "update" | "list"; @@ -273,9 +273,9 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean { return target.type === "all" || target.type === "extensions"; } -function printSelfUpdateUnavailable(npmCommand?: string[]): void { +function printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void { console.error(`error: ${APP_NAME} cannot self-update this installation.`); - console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand)); + console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName)); const entrypoint = process.argv[1]; if (entrypoint) { @@ -288,47 +288,53 @@ function printSelfUpdateFallback(command: SelfUpdateCommand): void { console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`)); } -async function shouldRunSelfUpdate(force: boolean): Promise { +interface SelfUpdatePlan { + packageName: string; + shouldRun: boolean; +} + +async function getSelfUpdatePlan(force: boolean): Promise { if (force) { - return true; + return { packageName: PACKAGE_NAME, shouldRun: true }; } - let latestVersion: string | undefined; try { - latestVersion = await getLatestPiVersion(VERSION); + const latestRelease = await getLatestPiRelease(VERSION); + const packageName = latestRelease?.packageName ?? PACKAGE_NAME; + if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { + return { packageName, shouldRun: true }; + } } catch { - return true; - } - - if (!latestVersion || isNewerPackageVersion(latestVersion, VERSION)) { - return true; + return { packageName: PACKAGE_NAME, shouldRun: true }; } console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`)); - return false; + return { packageName: PACKAGE_NAME, shouldRun: false }; } async function runSelfUpdate(command: SelfUpdateCommand): Promise { console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`)); - await new Promise((resolve, reject) => { - // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them. - const child = spawn(command.command, command.args, { - stdio: "inherit", - shell: shouldUseWindowsShell(command.command), + for (const step of command.steps ?? [command]) { + await new Promise((resolve, reject) => { + // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them. + const child = spawn(step.command, step.args, { + stdio: "inherit", + shell: shouldUseWindowsShell(step.command), + }); + child.on("error", (error) => { + reject(error); + }); + child.on("close", (code, signal) => { + if (code === 0) { + resolve(); + } else if (signal) { + reject(new Error(`${step.display} terminated by signal ${signal}`)); + } else { + reject(new Error(`${step.display} exited with code ${code ?? "unknown"}`)); + } + }); }); - child.on("error", (error) => { - reject(error); - }); - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - } else if (signal) { - reject(new Error(`${command.display} terminated by signal ${signal}`)); - } else { - reject(new Error(`${command.display} exited with code ${code ?? "unknown"}`)); - } - }); - }); + } } export async function handleConfigCommand(args: string[]): Promise { @@ -480,13 +486,18 @@ export async function handlePackageCommand(args: string[]): Promise { } } if (updateTargetIncludesSelf(target)) { - const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, selfUpdateNpmCommand); - if (!selfUpdateCommand) { - printSelfUpdateUnavailable(selfUpdateNpmCommand); - process.exitCode = 1; + const selfUpdatePlan = await getSelfUpdatePlan(options.force); + if (!selfUpdatePlan.shouldRun) { return true; } - if (!(await shouldRunSelfUpdate(options.force))) { + const selfUpdateCommand = getSelfUpdateCommand( + PACKAGE_NAME, + selfUpdateNpmCommand, + selfUpdatePlan.packageName, + ); + if (!selfUpdateCommand) { + printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName); + process.exitCode = 1; return true; } try { diff --git a/packages/coding-agent/src/utils/version-check.ts b/packages/coding-agent/src/utils/version-check.ts index e246dcd6..ec969469 100644 --- a/packages/coding-agent/src/utils/version-check.ts +++ b/packages/coding-agent/src/utils/version-check.ts @@ -3,6 +3,11 @@ import { getPiUserAgent } from "./pi-user-agent.js"; const LATEST_VERSION_URL = "https://pi.dev/api/latest-version"; const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000; +export interface LatestPiRelease { + version: string; + packageName?: string; +} + interface ParsedVersion { major: number; minor: number; @@ -47,10 +52,10 @@ export function isNewerPackageVersion(candidateVersion: string, currentVersion: return candidateVersion.trim() !== currentVersion.trim(); } -export async function getLatestPiVersion( +export async function getLatestPiRelease( currentVersion: string, options: { timeoutMs?: number } = {}, -): Promise { +): Promise { if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined; const response = await fetch(LATEST_VERSION_URL, { @@ -62,8 +67,20 @@ export async function getLatestPiVersion( }); if (!response.ok) return undefined; - const data = (await response.json()) as { version?: unknown }; - return typeof data.version === "string" && data.version.trim() ? data.version.trim() : undefined; + const data = (await response.json()) as { packageName?: unknown; version?: unknown }; + if (typeof data.version !== "string" || !data.version.trim()) { + return undefined; + } + const packageName = + typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined; + return { version: data.version.trim(), packageName }; +} + +export async function getLatestPiVersion( + currentVersion: string, + options: { timeoutMs?: number } = {}, +): Promise { + return (await getLatestPiRelease(currentVersion, options))?.version; } export async function checkForNewPiVersion(currentVersion: string): Promise { diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index e0a556c5..8e29d9e4 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -54,6 +54,49 @@ function createNpmPrefixInstall(template = "pi-prefix-"): { prefix: string; pack return { prefix, packageDir }; } +function createPnpmGlobalInstall(): { root: string; packageDir: string } { + const temp = mkdtempSync(join(tmpdir(), "pi-pnpm-")); + const binDir = join(temp, "bin"); + const root = join(temp, "pnpm", "global", "5", "node_modules"); + const packageDir = join(root, "@mariozechner", "pi-coding-agent"); + mkdirSync(packageDir, { recursive: true }); + mkdirSync(binDir, { recursive: true }); + writeFileSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), createFakePnpmScript(root)); + chmodSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), 0o755); + tempDir = temp; + process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`; + process.env.PI_PACKAGE_DIR = packageDir; + setExecPath( + join( + root, + ".pnpm", + "@mariozechner+pi-coding-agent@0.0.0", + "node_modules", + "@mariozechner", + "pi-coding-agent", + "dist", + "cli.js", + ), + ); + return { root, packageDir }; +} + +function createYarnGlobalInstall(): { globalDir: string; packageDir: string } { + const temp = mkdtempSync(join(tmpdir(), "pi-yarn-")); + const binDir = join(temp, "bin"); + const globalDir = join(temp, "yarn", "global"); + const packageDir = join(globalDir, "node_modules", "@mariozechner", "pi-coding-agent"); + mkdirSync(packageDir, { recursive: true }); + mkdirSync(binDir, { recursive: true }); + writeFileSync(join(binDir, process.platform === "win32" ? "yarn.cmd" : "yarn"), createFakeYarnScript(globalDir)); + chmodSync(join(binDir, process.platform === "win32" ? "yarn.cmd" : "yarn"), 0o755); + tempDir = temp; + process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`; + process.env.PI_PACKAGE_DIR = packageDir; + setExecPath(join(globalDir, ".yarn", "@mariozechner", "pi-coding-agent", "dist", "cli.js")); + return { globalDir, packageDir }; +} + function createBunGlobalInstall(): { packageDir: string } { const temp = mkdtempSync(join(tmpdir(), "pi-bun-")); const prefix = join(temp, ".bun"); @@ -72,6 +115,22 @@ function createBunGlobalInstall(): { packageDir: string } { return { packageDir }; } +function createFakePnpmScript(root: string): string { + if (process.platform === "win32") { + return `@echo off\r\nif "%1"=="root" if "%2"=="-g" echo ${root}\r\n`; + } + const escapedRoot = root.replaceAll("'", "'\\''"); + return `#!/bin/sh\nif [ "$1" = "root" ] && [ "$2" = "-g" ]; then\n\tprintf '%s\\n' '${escapedRoot}'\n\texit 0\nfi\nexit 1\n`; +} + +function createFakeYarnScript(globalDir: string): string { + if (process.platform === "win32") { + return `@echo off\r\nif "%1"=="global" if "%2"=="dir" echo ${globalDir}\r\n`; + } + const escapedGlobalDir = globalDir.replaceAll("'", "'\\''"); + return `#!/bin/sh\nif [ "$1" = "global" ] && [ "$2" = "dir" ]; then\n\tprintf '%s\\n' '${escapedGlobalDir}'\n\texit 0\nfi\nexit 1\n`; +} + function createFakeBunScript(bunBin: string): string { if (process.platform === "win32") { return `@echo off\r\nif "%1"=="pm" if "%2"=="bin" if "%3"=="-g" echo ${bunBin}\r\n`; @@ -115,6 +174,30 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates renamed packages from the current install prefix", () => { + const { prefix } = createNpmPrefixInstall(); + + const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi"); + + expect(command).toEqual({ + command: "npm", + args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"], + display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g @new-scope/pi`, + steps: [ + { + command: "npm", + args: ["--prefix", prefix, "uninstall", "-g", "@mariozechner/pi-coding-agent"], + display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent`, + }, + { + command: "npm", + args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"], + display: `npm --prefix ${prefix} install -g @new-scope/pi`, + }, + ], + }); + }); + test("self-update respects configured npmCommand", () => { const { prefix } = createNpmPrefixInstall(); @@ -167,6 +250,81 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates renamed pnpm global installs by removing the old package first", () => { + createPnpmGlobalInstall(); + + const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi"); + + expect(detectInstallMethod()).toBe("pnpm"); + expect(command).toEqual({ + command: "pnpm", + args: ["install", "-g", "@new-scope/pi"], + display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g @new-scope/pi", + steps: [ + { + command: "pnpm", + args: ["remove", "-g", "@mariozechner/pi-coding-agent"], + display: "pnpm remove -g @mariozechner/pi-coding-agent", + }, + { + command: "pnpm", + args: ["install", "-g", "@new-scope/pi"], + display: "pnpm install -g @new-scope/pi", + }, + ], + }); + }); + + test("self-updates renamed yarn global installs by removing the old package first", () => { + createYarnGlobalInstall(); + + const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi"); + + expect(detectInstallMethod()).toBe("yarn"); + expect(command).toEqual({ + command: "yarn", + args: ["global", "add", "@new-scope/pi"], + display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add @new-scope/pi", + steps: [ + { + command: "yarn", + args: ["global", "remove", "@mariozechner/pi-coding-agent"], + display: "yarn global remove @mariozechner/pi-coding-agent", + }, + { + command: "yarn", + args: ["global", "add", "@new-scope/pi"], + display: "yarn global add @new-scope/pi", + }, + ], + }); + }); + + test("self-updates renamed bun global installs by removing the old package first", () => { + createBunGlobalInstall(); + + const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi"); + + expect(detectInstallMethod()).toBe("bun"); + expect(command).toEqual({ + command: "bun", + args: ["install", "-g", "@new-scope/pi"], + display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g @new-scope/pi", + steps: [ + { + command: "bun", + args: ["uninstall", "-g", "@mariozechner/pi-coding-agent"], + display: "bun uninstall -g @mariozechner/pi-coding-agent", + }, + { + command: "bun", + args: ["install", "-g", "@new-scope/pi"], + display: "bun install -g @new-scope/pi", + }, + ], + }); + }); + test("does not self-update when npm install path is not writable", () => { const { packageDir } = createNpmPrefixInstall(); chmodSync(packageDir, 0o500); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 1c1ceab4..74cb94f9 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "no import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ENV_AGENT_DIR } from "../src/config.js"; +import { ENV_AGENT_DIR, PACKAGE_NAME } from "../src/config.js"; import { main } from "../src/main.js"; describe("package commands", () => { @@ -36,6 +36,7 @@ describe("package commands", () => { }); afterEach(() => { + vi.unstubAllGlobals(); process.chdir(originalCwd); process.exitCode = originalExitCode; if (originalAgentDir === undefined) { @@ -128,7 +129,7 @@ describe("package commands", () => { } }); - it("uses global npmCommand for self updates", async () => { + it("uses global npmCommand and current package name for forced self updates without checking the api", async () => { const globalPrefix = join(tempDir, "global-prefix"); const projectPrefix = join(tempDir, "project-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); @@ -156,6 +157,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -165,8 +168,10 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; expect(recordedArgs).toContain(globalPrefix); + expect(recordedArgs).toContain(PACKAGE_NAME); expect(recordedArgs).not.toContain(projectPrefix); } finally { logSpy.mockRestore(); @@ -174,6 +179,155 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); } }); + it("uses the current package name when the update check omits packageName", async () => { + const globalPrefix = join(tempDir, "global-prefix"); + const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); + const fakeNpmPath = join(tempDir, "fake-npm.cjs"); + const recordPath = join(tempDir, "self-update.json"); + mkdirSync(selfPackageDir, { recursive: true }); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1]; +if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules")); +else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); +`, + ); + writeFileSync( + join(agentDir, "settings.json"), + JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), + ); + process.env.PI_PACKAGE_DIR = selfPackageDir; + Object.defineProperty(process, "execPath", { + value: join(selfPackageDir, "dist", "cli.js"), + configurable: true, + }); + const fetchMock = vi.fn(async () => Response.json({ version: "0.73.1" })); + vi.stubGlobal("fetch", fetchMock); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + try { + await expect(main(["update", "--self"])).resolves.toBeUndefined(); + + expect(process.exitCode).toBeUndefined(); + expect(errorSpy).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledOnce(); + const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; + expect(recordedArgs).toContain(PACKAGE_NAME); + } finally { + logSpy.mockRestore(); + errorSpy.mockRestore(); + } + }); + + it("installs the active package name from the update check during self-update", async () => { + const globalPrefix = join(tempDir, "global-prefix"); + const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); + const fakeNpmPath = join(tempDir, "fake-npm.cjs"); + const recordPath = join(tempDir, "self-update.json"); + mkdirSync(selfPackageDir, { recursive: true }); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1]; +if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules")); +else { + const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[]; + records.push(args); + fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records)); +} +`, + ); + writeFileSync( + join(agentDir, "settings.json"), + JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), + ); + process.env.PI_PACKAGE_DIR = selfPackageDir; + Object.defineProperty(process, "execPath", { + value: join(selfPackageDir, "dist", "cli.js"), + configurable: true, + }); + const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi"; + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })), + ); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + try { + await expect(main(["update", "--self"])).resolves.toBeUndefined(); + + expect(process.exitCode).toBeUndefined(); + expect(errorSpy).not.toHaveBeenCalled(); + const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; + expect(recordedCalls).toEqual([ + expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), + expect.arrayContaining(["install", "-g", activePackageName]), + ]); + } finally { + logSpy.mockRestore(); + errorSpy.mockRestore(); + } + }); + + it("fails self-update when renamed npm package installation fails", async () => { + const globalPrefix = join(tempDir, "global-prefix"); + const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); + const fakeNpmPath = join(tempDir, "fake-npm-fail.cjs"); + const recordPath = join(tempDir, "self-update-fail.json"); + mkdirSync(selfPackageDir, { recursive: true }); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1]; +if(args.includes("root")) { + console.log(path.join(prefix,"lib","node_modules")); + process.exit(0); +} +const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[]; +records.push(args); +fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records)); +if(args.includes("install")) process.exit(23); +`, + ); + writeFileSync( + join(agentDir, "settings.json"), + JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), + ); + process.env.PI_PACKAGE_DIR = selfPackageDir; + Object.defineProperty(process, "execPath", { + value: join(selfPackageDir, "dist", "cli.js"), + configurable: true, + }); + const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi"; + vi.stubGlobal( + "fetch", + vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })), + ); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + try { + await expect(main(["update", "--self"])).resolves.toBeUndefined(); + + expect(process.exitCode).toBe(1); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); + const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n"); + expect(stdout).not.toContain(`Updated pi`); + expect(stderr).toContain("exited with code 23"); + const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; + expect(recordedCalls).toEqual([ + expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), + expect.arrayContaining(["install", "-g", activePackageName]), + ]); + } finally { + logSpy.mockRestore(); + errorSpy.mockRestore(); + } + }); + it("suggests the configured source when update input omits the npm prefix", async () => { const settingsPath = join(agentDir, "settings.json"); writeFileSync(settingsPath, JSON.stringify({ packages: ["npm:pi-formatter"] }, null, 2)); diff --git a/packages/coding-agent/test/version-check.test.ts b/packages/coding-agent/test/version-check.test.ts index 3e5bc8f2..c871718f 100644 --- a/packages/coding-agent/test/version-check.test.ts +++ b/packages/coding-agent/test/version-check.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { checkForNewPiVersion, comparePackageVersions, + getLatestPiRelease, getLatestPiVersion, isNewerPackageVersion, } from "../src/utils/version-check.js"; @@ -56,6 +57,13 @@ describe("version checks", () => { ); }); + it("returns the active package name from the version check api", async () => { + const fetchMock = vi.fn(async () => Response.json({ packageName: "@new-scope/pi", version: "1.2.4" })); + vi.stubGlobal("fetch", fetchMock); + + await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ packageName: "@new-scope/pi", version: "1.2.4" }); + }); + it("skips api calls when version checks are disabled", async () => { process.env.PI_SKIP_VERSION_CHECK = "1"; const fetchMock = vi.fn(); From 7fa924b77bb10574a668122f1b58927d554009d6 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 16:43:26 +0200 Subject: [PATCH 28/30] docs: audit unreleased changelog entries --- packages/ai/CHANGELOG.md | 7 +++++++ packages/coding-agent/CHANGELOG.md | 20 ++++++++++++++++++++ packages/tui/CHANGELOG.md | 3 +++ 3 files changed, 30 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index aecc4c55..363006ca 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,10 +2,17 @@ ## [Unreleased] +### Added + +- Added OAuth login flow metadata so clients can present interactive provider choices during login ([#4190](https://github.com/earendil-works/pi-mono/pull/4190) by [@mitsuhiko](https://github.com/mitsuhiko)). + ### Fixed - Fixed OpenAI Responses reasoning text streaming for LM Studio and other compatible providers that emit `response.reasoning_text.delta` events ([#4191](https://github.com/badlogic/pi-mono/pull/4191) by [@yaanfpv](https://github.com/yaanfpv)). - Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)). +- Fixed OpenAI-compatible chat completion streams that interleave content and tool-call deltas in the same choice. +- Fixed the Kimi K2 P6 model alias to normalize to `kimi-for-coding` ([#4218](https://github.com/earendil-works/pi-mono/issues/4218)). +- Fixed OpenAI Codex Responses requests to send a non-empty system prompt ([#4184](https://github.com/earendil-works/pi-mono/issues/4184)). ## [0.73.0] - 2026-05-04 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 79ca11e6..86051883 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,15 +2,35 @@ ## [Unreleased] +### New Features + +- **Self-update support for the npm scope migration**: `pi update --self` now supports the upcoming package rename from `@mariozechner/pi-coding-agent` to `@earendil-works/pi-coding-agent`. After the new package is published, existing global installs can update through the normal self-update flow; pi will uninstall the old global package and install the package name returned by the version check endpoint. +- **Interactive OAuth login selection**: OAuth providers can now present multiple login choices in `/login`, enabling provider-specific interactive authentication flows. See [Providers](docs/providers.md). +- **JSONC-style `models.json` parsing**: `models.json` now allows comments and trailing commas, making custom provider and model configuration easier to maintain. See [Providers](docs/providers.md) and [Custom Providers](docs/custom-provider.md). + +### Added + +- Added interactive login selection support so OAuth providers can present multiple login choices ([#4190](https://github.com/earendil-works/pi-mono/pull/4190) by [@mitsuhiko](https://github.com/mitsuhiko)). + ### Changed - Changed `pi update --self` to honor the active package name returned by the Pi version check endpoint, defaulting to the current package when omitted and uninstalling the old global package before installing a renamed package. +- Changed extension loading to use upstream `jiti` 2.7 instead of the `@mariozechner/jiti` fork ([#4244](https://github.com/earendil-works/pi-mono/pull/4244) by [@pi0](https://github.com/pi0)). +- Changed `models.json` parsing to allow comments and trailing commas ([#4162](https://github.com/earendil-works/pi-mono/pull/4162) by [@julien-c](https://github.com/julien-c)). ### Fixed - Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)). - Fixed pending tool results not updating in the live TUI after toggling thinking block visibility while the tool is running ([#4167](https://github.com/badlogic/pi-mono/issues/4167)). - Fixed `/copy` reporting success on Linux without writing the clipboard on Wayland-only compositors (Hyprland, Niri, ...) by skipping the X11-only native addon on Linux and routing through `wl-copy`/`xclip`/`xsel` instead ([#4177](https://github.com/badlogic/pi-mono/issues/4177)). +- Fixed HTML session exports to strip skill wrapper XML from rendered user messages ([#4234](https://github.com/earendil-works/pi-mono/pull/4234) by [@aliou](https://github.com/aliou)). +- Fixed OpenAI-compatible chat completion streams that interleave content and tool-call deltas in the same choice. +- Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)). +- Fixed OpenAI Codex Responses requests to send a non-empty system prompt ([#4184](https://github.com/earendil-works/pi-mono/issues/4184)). +- Fixed Kimi For Coding model resolution for the Kimi K2 P6 alias ([#4218](https://github.com/earendil-works/pi-mono/issues/4218)). +- Fixed Kitty inline image redraws to stay within TUI-owned terminal regions and avoid writing below the active viewport. +- Fixed Kitty inline image rendering by letting the terminal allocate image ids and bounding parsed image ids to valid values. +- Fixed inline image capability detection to disable inline images in cmux terminals. ## [0.73.0] - 2026-05-04 diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 1444bbff..a8d6ae44 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -5,6 +5,9 @@ ### Fixed - Fixed wrapped OSC 8 hyperlinks to preserve BEL terminators so OAuth login URLs remain clickable on every wrapped line. +- Fixed Kitty inline image redraws to stay within TUI-owned terminal regions and avoid writing below the active viewport. +- Fixed Kitty inline image rendering by letting the terminal allocate image ids and bounding parsed image ids to valid values. +- Fixed inline image capability detection to disable inline images in cmux terminals. ## [0.73.0] - 2026-05-04 From 781152fc24841dc54b22284514604048ebe5e2c9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 16:44:28 +0200 Subject: [PATCH 29/30] Release v0.73.1 --- package-lock.json | 250 ++++++------------ packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- packages/web-ui/CHANGELOG.md | 2 +- packages/web-ui/example/package.json | 2 +- packages/web-ui/package.json | 6 +- 19 files changed, 103 insertions(+), 201 deletions(-) diff --git a/package-lock.json b/package-lock.json index e79b3a3c..df0f314c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,9 +224,9 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1041.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1041.0.tgz", - "integrity": "sha512-1QehYO3jhdvNQ5mOKtwIiNV04y4aywaNZw9HzCp7SSYCX4yy+AGXc2hhYjCiMDUvQPIELuvbR8MXw81NGAj8ZQ==", + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1044.0.tgz", + "integrity": "sha512-uwJB0pVZIey+kYag8xeUirMvuaDhhEHuYgSsapOKT5XCAije5pLNlg160eECJdhX9JEf0IthIM3IiHKSEkFtMw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -241,7 +241,7 @@ "@aws-sdk/middleware-user-agent": "^3.972.38", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/token-providers": "3.1044.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", @@ -446,6 +446,24 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.972.38", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", @@ -690,9 +708,9 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1041.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", - "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1044.0.tgz", + "integrity": "sha512-uvpWTfpzOM9qVrR0kdAjzBbvv2ERW7S1CKeBeRkHyHy21Ext5fNReIVrbAzhjuVC7UxRyZQ8PHYW/3T83hWlbg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.8", @@ -914,9 +932,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -934,9 +949,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -954,9 +966,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -974,9 +983,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -1447,9 +1453,9 @@ } }, "node_modules/@google/genai": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.51.0.tgz", - "integrity": "sha512-vTZZF3CSimN7cn2zsLpW2p5WF0eZa5Gz69ITMPCNHpPrDlAstOfGifSfi0p/s9Z9400f7xJRkgvkQNrcM7pJ6w==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1663,9 +1669,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1682,9 +1685,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1701,9 +1701,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1720,9 +1717,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1739,9 +1733,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1973,9 +1964,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1996,9 +1984,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2019,9 +2004,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2042,9 +2024,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2065,9 +2044,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2299,9 +2275,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2323,9 +2296,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2347,9 +2317,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2371,9 +2338,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2395,9 +2359,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2419,9 +2380,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2676,9 +2634,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2692,9 +2647,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2708,9 +2660,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2724,9 +2673,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2740,9 +2686,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2756,9 +2699,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2772,9 +2712,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2788,9 +2725,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2804,9 +2738,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2820,9 +2751,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2836,9 +2764,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2852,9 +2777,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2868,9 +2790,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3743,9 +3662,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3762,9 +3678,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3781,9 +3694,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3800,9 +3710,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3942,9 +3849,10 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, "node_modules/@types/hosted-git-info": { @@ -5199,9 +5107,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.7.tgz", - "integrity": "sha512-Yh7/7rQuMXICNr0oMYDR2yHP6oUvmQsTToFeOWj/kIDhAwQ+c4Ol/lbcwOmEM5OHYQmh6S6EQSQ1sljCKP36bQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "funding": [ { "type": "github", @@ -5678,13 +5586,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -5894,9 +5802,9 @@ } }, "node_modules/koffi": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.1.tgz", - "integrity": "sha512-0Ie6CfD026dNfWSosDw9dPxPzO9Rlyo0N8m5r05S8YjytIpuilzMFDMY4IDy/8xQsTwpuVinhncD+S8n3bcYZQ==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6049,9 +5957,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6072,9 +5977,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6095,9 +5997,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6118,9 +6017,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6432,9 +6328,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "3.90.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.90.0.tgz", - "integrity": "sha512-pZNQT7UnYlMwMBy5N1lV5X/YLTbZM5ncytN3xL7CHEzhDN8uVe0u55yaPUJICIJjaCW8NrM5BFdqr7HLweStNA==", + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7085,6 +6981,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7454,9 +7356,9 @@ } }, "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -7785,9 +7687,9 @@ } }, "node_modules/typebox": { - "version": "1.1.37", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.37.tgz", - "integrity": "sha512-jb7jp6KvOvvy5sd+11AfJ0/e0F0AS9RcOXd55oGi2ZnRHIGmFvrTaNF+ZidRmGBmmNTkM5KKl0Z37KzxJ+owEQ==", + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", "license": "MIT" }, "node_modules/typescript": { @@ -7860,9 +7762,9 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -8274,10 +8176,10 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent-core", - "version": "0.73.0", + "version": "0.73.1", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.73.0", + "@mariozechner/pi-ai": "^0.73.1", "typebox": "^1.1.24" }, "devDependencies": { @@ -8308,7 +8210,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.73.0", + "version": "0.73.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.91.1", @@ -8354,12 +8256,12 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.73.0", + "version": "0.73.1", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent-core": "^0.73.0", - "@mariozechner/pi-ai": "^0.73.0", - "@mariozechner/pi-tui": "^0.73.0", + "@mariozechner/pi-agent-core": "^0.73.1", + "@mariozechner/pi-ai": "^0.73.1", + "@mariozechner/pi-tui": "^0.73.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", @@ -8401,25 +8303,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.73.0", + "version": "0.73.1", "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.73.0" + "version": "0.73.1" }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.73.0", + "version": "0.73.1", "dependencies": { "ms": "^2.1.3" }, @@ -8455,7 +8357,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.73.0", + "version": "0.73.1", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -8477,12 +8379,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.73.0", + "version": "0.73.1", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.73.0", - "@mariozechner/pi-tui": "^0.73.0", + "@mariozechner/pi-ai": "^0.73.1", + "@mariozechner/pi-tui": "^0.73.1", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", @@ -8504,7 +8406,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "0.73.0", + "version": "0.73.1", "dependencies": { "@mariozechner/mini-lit": "^0.2.0", "@mariozechner/pi-ai": "file:../../ai", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 972c0c51..177d8673 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.1] - 2026-05-07 ## [0.73.0] - 2026-05-04 diff --git a/packages/agent/package.json b/packages/agent/package.json index 6fc8ec8f..b037c9d1 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent-core", - "version": "0.73.0", + "version": "0.73.1", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.73.0", + "@mariozechner/pi-ai": "^0.73.1", "typebox": "^1.1.24" }, "keywords": [ diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 363006ca..fd67dd35 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.1] - 2026-05-07 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index f05b48cf..027059de 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.73.0", + "version": "0.73.1", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 86051883..2ab0d692 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.1] - 2026-05-07 ### New Features 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 ac299123..74e41e17 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.73.0", + "version": "0.73.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.73.0", + "version": "0.73.1", "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 7f1f0088..dc26f599 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.73.0", + "version": "0.73.1", "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 b74591d2..79aea40c 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.73.0", + "version": "0.73.1", "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 fbe7ec7c..88401a96 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.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.3.0", + "version": "1.3.1", "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 e9190db9..6ff0124e 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.3.0", + "version": "1.3.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", 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 98a99292..2cb24919 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.73.0", + "version": "0.73.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.73.0", + "version": "0.73.1", "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 5825a89a..acd5a6c3 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.73.0", + "version": "0.73.1", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index b4f3ba7f..89f89729 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-coding-agent", - "version": "0.73.0", + "version": "0.73.1", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -38,9 +38,9 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-agent-core": "^0.73.0", - "@mariozechner/pi-ai": "^0.73.0", - "@mariozechner/pi-tui": "^0.73.0", + "@mariozechner/pi-agent-core": "^0.73.1", + "@mariozechner/pi-ai": "^0.73.1", + "@mariozechner/pi-tui": "^0.73.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index a8d6ae44..ffdd51af 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.1] - 2026-05-07 ### Fixed diff --git a/packages/tui/package.json b/packages/tui/package.json index bd7e17c8..879b9880 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-tui", - "version": "0.73.0", + "version": "0.73.1", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index b03de5c9..60e8ea86 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.73.1] - 2026-05-07 ## [0.73.0] - 2026-05-04 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index afe8c076..dccddd2f 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.73.0", + "version": "0.73.1", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 38e89871..6525d6de 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-web-ui", - "version": "0.73.0", + "version": "0.73.1", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.73.0", - "@mariozechner/pi-tui": "^0.73.0", + "@mariozechner/pi-ai": "^0.73.1", + "@mariozechner/pi-tui": "^0.73.1", "typebox": "^1.1.24", "docx-preview": "^0.3.7", "jszip": "^3.10.1", From 147f8158022db898809f9309b44102b65ec51800 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 16:45:27 +0200 Subject: [PATCH 30/30] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 + packages/ai/CHANGELOG.md | 2 + packages/ai/src/models.generated.ts | 271 +++++++++++++--------------- packages/coding-agent/CHANGELOG.md | 2 + packages/tui/CHANGELOG.md | 2 + packages/web-ui/CHANGELOG.md | 2 + 6 files changed, 137 insertions(+), 144 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 177d8673..25bf75bd 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.1] - 2026-05-07 ## [0.73.0] - 2026-05-04 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index fd67dd35..23a14019 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.1] - 2026-05-07 ### Added diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index abca6a23..4fbba0fc 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7586,9 +7586,9 @@ export const MODELS = { "big-pickle": { id: "big-pickle", name: "Big Pickle", - api: "anthropic-messages", + api: "openai-completions", provider: "opencode", - baseUrl: "https://opencode.ai/zen", + baseUrl: "https://opencode.ai/zen/v1", reasoning: true, input: ["text"], cost: { @@ -7599,7 +7599,7 @@ export const MODELS = { }, contextWindow: 200000, maxTokens: 128000, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", @@ -7854,9 +7854,9 @@ export const MODELS = { thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.05, + output: 0.4, + cacheRead: 0.005, cacheWrite: 0, }, contextWindow: 400000, @@ -8342,55 +8342,21 @@ export const MODELS = { } satisfies Model<"openai-completions">, "kimi-k2.6": { id: "kimi-k2.6", - name: "Kimi K2.6 (3x limits)", + name: "Kimi K2.6", api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", reasoning: true, input: ["text", "image"], cost: { - input: 0.32, - output: 1.34, - cacheRead: 0.054, + input: 0.95, + output: 4, + cacheRead: 0.16, cacheWrite: 0, }, contextWindow: 262144, maxTokens: 65536, } satisfies Model<"openai-completions">, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo V2 Omni", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.4, - output: 2, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo V2 Pro", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 128000, - } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo V2.5", @@ -8531,23 +8497,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, - "allenai/olmo-3.1-32b-instruct": { - id: "allenai/olmo-3.1-32b-instruct", - name: "AllenAI: Olmo 3.1 32B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.19999999999999998, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 65536, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "amazon/nova-2-lite-v1": { id: "amazon/nova-2-lite-v1", name: "Amazon: Nova 2 Lite", @@ -8682,7 +8631,7 @@ export const MODELS = { cacheWrite: 3.75, }, contextWindow: 200000, - maxTokens: 128000, + maxTokens: 64000, } satisfies Model<"openai-completions">, "anthropic/claude-3.7-sonnet:thinking": { id: "anthropic/claude-3.7-sonnet:thinking", @@ -8959,6 +8908,23 @@ export const MODELS = { contextWindow: 2000000, maxTokens: 30000, } satisfies Model<"openai-completions">, + "baidu/cobuddy:free": { + id: "baidu/cobuddy:free", + name: "Baidu Qianfan: CoBuddy (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "baidu/ernie-4.5-21b-a3b": { id: "baidu/ernie-4.5-21b-a3b", name: "Baidu: ERNIE 4.5 21B A3B", @@ -9261,13 +9227,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.435, + output: 0.87, + cacheRead: 0.003625, cacheWrite: 0, }, - contextWindow: 131000, - maxTokens: 131000, + contextWindow: 1048576, + maxTokens: 384000, } satisfies Model<"openai-completions">, "essentialai/rnj-1-instruct": { id: "essentialai/rnj-1-instruct", @@ -9677,23 +9643,6 @@ export const MODELS = { contextWindow: 256000, maxTokens: 80000, } satisfies Model<"openai-completions">, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Meta: Llama 3 8B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.03, - output: 0.04, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "meta-llama/llama-3.1-70b-instruct": { id: "meta-llama/llama-3.1-70b-instruct", name: "Meta: Llama 3.1 70B Instruct", @@ -10085,6 +10034,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 4096, } satisfies Model<"openai-completions">, + "mistralai/mistral-medium-3-5": { + id: "mistralai/mistral-medium-3-5", + name: "Mistral: Mistral Medium 3.5", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "mistralai/mistral-medium-3.1": { id: "mistralai/mistral-medium-3.1", name: "Mistral: Mistral Medium 3.1", @@ -10315,13 +10281,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.74, - output: 3.49, - cacheRead: 0.14, + input: 0.75, + output: 3.5, + cacheRead: 0.15, cacheWrite: 0, }, - contextWindow: 262142, - maxTokens: 262142, + contextWindow: 262144, + maxTokens: 16384, } satisfies Model<"openai-completions">, "nex-agi/deepseek-v3.1-nex-n1": { id: "nex-agi/deepseek-v3.1-nex-n1", @@ -11236,6 +11202,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 16384, } satisfies Model<"openai-completions">, + "openai/gpt-chat-latest": { + id: "openai/gpt-chat-latest", + name: "OpenAI: GPT Chat Latest", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "openai/gpt-oss-120b": { id: "openai/gpt-oss-120b", name: "OpenAI: gpt-oss-120b", @@ -11925,7 +11908,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.12, + input: 0.11, output: 0.7999999999999999, cacheRead: 0.07, cacheWrite: 0, @@ -12214,13 +12197,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.15, + input: 0.14, output: 1, cacheRead: 0.049999999999999996, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12248,13 +12231,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.04, output: 0.15, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -12985,7 +12968,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 16384, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5-turbo": { id: "z-ai/glm-5-turbo", @@ -13132,13 +13115,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.74, - output: 3.49, - cacheRead: 0.14, + input: 0.75, + output: 3.5, + cacheRead: 0.15, cacheWrite: 0, }, - contextWindow: 262142, - maxTokens: 262142, + contextWindow: 262144, + maxTokens: 16384, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -15528,8 +15511,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.19999999999999998, cacheWrite: 0, }, @@ -15545,8 +15528,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.19999999999999998, cacheWrite: 0, }, @@ -15562,8 +15545,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.19999999999999998, cacheWrite: 0, }, @@ -15579,8 +15562,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.19999999999999998, cacheWrite: 0, }, @@ -15596,8 +15579,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.19999999999999998, cacheWrite: 0, }, @@ -15613,8 +15596,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 2, - output: 6, + input: 1.25, + output: 2.5, cacheRead: 0.19999999999999998, cacheWrite: 0, }, @@ -16387,8 +16370,8 @@ export const MODELS = { cacheRead: 0.01, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 64000, + contextWindow: 262144, + maxTokens: 65536, } satisfies Model<"anthropic-messages">, "mimo-v2-omni": { id: "mimo-v2-omni", @@ -16404,8 +16387,8 @@ export const MODELS = { cacheRead: 0.08, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 128000, + contextWindow: 262144, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2-pro": { id: "mimo-v2-pro", @@ -16421,8 +16404,8 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 128000, + contextWindow: 1048576, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2.5": { id: "mimo-v2.5", @@ -16431,7 +16414,7 @@ export const MODELS = { provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/anthropic", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 0.4, output: 2, @@ -16448,7 +16431,7 @@ export const MODELS = { provider: "xiaomi", baseUrl: "https://api.xiaomimimo.com/anthropic", reasoning: true, - input: ["text", "image"], + input: ["text"], cost: { input: 1, output: 3, @@ -16474,8 +16457,8 @@ export const MODELS = { cacheRead: 0.01, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 64000, + contextWindow: 262144, + maxTokens: 65536, } satisfies Model<"anthropic-messages">, "mimo-v2-omni": { id: "mimo-v2-omni", @@ -16491,8 +16474,8 @@ export const MODELS = { cacheRead: 0.08, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 128000, + contextWindow: 262144, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2-pro": { id: "mimo-v2-pro", @@ -16508,8 +16491,8 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 128000, + contextWindow: 1048576, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2.5": { id: "mimo-v2.5", @@ -16518,7 +16501,7 @@ export const MODELS = { provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 0.4, output: 2, @@ -16535,7 +16518,7 @@ export const MODELS = { provider: "xiaomi-token-plan-ams", baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic", reasoning: true, - input: ["text", "image"], + input: ["text"], cost: { input: 1, output: 3, @@ -16561,8 +16544,8 @@ export const MODELS = { cacheRead: 0.01, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 64000, + contextWindow: 262144, + maxTokens: 65536, } satisfies Model<"anthropic-messages">, "mimo-v2-omni": { id: "mimo-v2-omni", @@ -16578,8 +16561,8 @@ export const MODELS = { cacheRead: 0.08, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 128000, + contextWindow: 262144, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2-pro": { id: "mimo-v2-pro", @@ -16595,8 +16578,8 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 128000, + contextWindow: 1048576, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2.5": { id: "mimo-v2.5", @@ -16605,7 +16588,7 @@ export const MODELS = { provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 0.4, output: 2, @@ -16622,7 +16605,7 @@ export const MODELS = { provider: "xiaomi-token-plan-cn", baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic", reasoning: true, - input: ["text", "image"], + input: ["text"], cost: { input: 1, output: 3, @@ -16648,8 +16631,8 @@ export const MODELS = { cacheRead: 0.01, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 64000, + contextWindow: 262144, + maxTokens: 65536, } satisfies Model<"anthropic-messages">, "mimo-v2-omni": { id: "mimo-v2-omni", @@ -16665,8 +16648,8 @@ export const MODELS = { cacheRead: 0.08, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 128000, + contextWindow: 262144, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2-pro": { id: "mimo-v2-pro", @@ -16682,8 +16665,8 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 128000, + contextWindow: 1048576, + maxTokens: 131072, } satisfies Model<"anthropic-messages">, "mimo-v2.5": { id: "mimo-v2.5", @@ -16692,7 +16675,7 @@ export const MODELS = { provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 0.4, output: 2, @@ -16709,7 +16692,7 @@ export const MODELS = { provider: "xiaomi-token-plan-sgp", baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", reasoning: true, - input: ["text", "image"], + input: ["text"], cost: { input: 1, output: 3, diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2ab0d692..784dce87 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.1] - 2026-05-07 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ffdd51af..3c7b455c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.1] - 2026-05-07 ### Fixed diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 60e8ea86..d7bf8510 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.73.1] - 2026-05-07 ## [0.73.0] - 2026-05-04