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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 50993d743d31d9961edd1cf4af0b2ac17dc858ed Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 7 May 2026 01:04:51 +0200 Subject: [PATCH 20/21] 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 6d4d2e928807d90539c608fd72d6bf5604ee01fa Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 7 May 2026 11:13:34 +0200 Subject: [PATCH 21/21] 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();