diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md
index 8da535e4d..57ac5f55a 100644
--- a/packages/ai/CHANGELOG.md
+++ b/packages/ai/CHANGELOG.md
@@ -1,12 +1,11 @@
# Changelog
## [Unreleased]
-
### Fixed
+- Fixed shell execution failure responses to preserve all result fields when sanitizing, preventing truncated metadata in stream results
- Fixed context overflow detection to recognize `model_context_window_exceeded` from z.ai / GLM providers, preventing infinite retry loops when context window is exceeded ([#638](https://github.com/can1357/oh-my-pi/issues/638))
-
## [14.1.0] - 2026-04-11
### Added
diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts
index b5c1061df..fee40c691 100644
--- a/packages/ai/src/providers/cursor.ts
+++ b/packages/ai/src/providers/cursor.ts
@@ -106,6 +106,7 @@ import {
type ShellArgs,
ShellFailureSchema,
ShellRejectedSchema,
+ type ShellResult,
ShellResultSchema,
type ShellStream,
ShellStreamExitSchema,
@@ -675,9 +676,7 @@ function sendShellStreamEvent(
sendExecClientMessage(h2Request, execMsg, "shellStream", create(ShellStreamSchema, { event }));
}
-function sanitizeShellExecResult(execResult: { result: { case?: string; value?: any } }): {
- result: { case?: string; value?: any };
-} {
+function sanitizeShellExecResult(execResult: ShellResult): ShellResult {
const result = execResult.result;
if (!result) return execResult;
@@ -686,6 +685,7 @@ function sanitizeShellExecResult(execResult: { result: { case?: string; value?:
case "failure": {
const value = result.value;
return {
+ ...execResult,
result: {
case: result.case,
value: {
@@ -694,7 +694,7 @@ function sanitizeShellExecResult(execResult: { result: { case?: string; value?:
stderr: value.stderr ? sanitizeText(value.stderr) : value.stderr,
},
},
- };
+ } as ShellResult;
}
default:
return execResult;
@@ -854,7 +854,7 @@ async function handleShellStreamArgs(
function sendShellStreamExitFromResult(
h2Request: http2.ClientHttp2Stream,
execMsg: ExecServerMessage,
- execResult: { result: { case?: string; value?: any } },
+ execResult: ShellResult,
sendBufferedOutput: boolean,
): void {
const result = execResult.result;
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 3849fdd6d..e59874145 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -1,27 +1,24 @@
# Changelog
## [Unreleased]
+
### Added
- Added support for `computeHashlineDiff` to accept hashline edits with `loc` and `content` payloads without requiring pre-resolved `op` fields
+- Added `/rename
` slash command to set an explicit session name, updating the session header and terminal tab title ([#658](https://github.com/can1357/oh-my-pi/issues/658))
+- Added `session_name` status line segment: displays the session name in the status bar right side with a stable hash-derived accent color unique to each name; shown in all presets when a name is set
### Changed
- Changed eager todo enforcement to only apply on the first user message of a conversation, skipping subsequent user turns that may correct, clarify, or redirect the prior task
-### Added
-
-- Added `/rename ` slash command to set an explicit session name, updating the session header and terminal tab title ([#658](https://github.com/can1357/oh-my-pi/issues/658))
-- Added `session_name` status line segment: displays the session name in the status bar right side with a stable hash-derived accent color unique to each name; shown in all presets when a name is set
-
### Fixed
+- Fixed tool execution output to return the original `toolResult` text content from tools instead of sanitizing it before sending completion messages
+- Fixed session accent rendering in the status line and editor to reset only foreground color (`\x1b[39m`) so applying a session color no longer clears other ANSI styles
- Session name sanitization: strip C0/C1 control characters (including ANSI ESC) from session names at storage time and in status line rendering, preventing escape sequence injection into TUI output
- Auto-generated session titles no longer overwrite a name set via `/rename`: `setSessionName` now tracks whether the name was set by the user or auto-generated and silently ignores auto titles once a user name is in place; terminal title follows the same guard
- Session accent border color now applied on session resume and after auto-title generation, not only after an explicit `/rename`
-
-### Fixed
-
- Fixed retained Python kernel ownership so `AgentSession.dispose()` only shuts down kernels owned by that session, including warmup-created kernels
## [14.1.0] - 2026-04-11
diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json
index 1f21f0bb1..7a28cb92f 100644
--- a/packages/coding-agent/package.json
+++ b/packages/coding-agent/package.json
@@ -65,7 +65,7 @@
"puppeteer": "catalog:",
"turndown": "catalog:",
"turndown-plugin-gfm": "catalog:",
- "zod": "catalog:pinned"
+ "zod": "catalog:"
},
"devDependencies": {
"@types/bun": "catalog:",
diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts
index 4d2db4f92..edb9f4b3f 100644
--- a/packages/coding-agent/src/config/model-registry.ts
+++ b/packages/coding-agent/src/config/model-registry.ts
@@ -769,7 +769,6 @@ export class ModelRegistry {
// models registered by extensions survive the model selector's offline reload.
#runtimeModelOverlays: CustomModelOverlay[] = [];
#runtimeProviderApiKeys: Map = new Map();
- #runtimeKeylessProviders: Set = new Set();
#runtimeProvidersBySource: Map> = new Map();
#runtimeProviderSourceByName: Map = new Map();
@@ -847,11 +846,6 @@ export class ModelRegistry {
this.#configError = undefined;
this.#providerDiscoveryStates.clear();
this.#loadModels();
- // Restore runtime keyless providers AFTER #loadModels, because #loadModels
- // replaces this.#keylessProviders via = with a new Set from models.yml.
- for (const k of this.#runtimeKeylessProviders) {
- this.#keylessProviders.add(k);
- }
}
/**
@@ -1938,7 +1932,6 @@ export class ModelRegistry {
}
this.#runtimeProviderSourceByName.delete(providerName);
this.#runtimeProviderApiKeys.delete(providerName);
- this.#runtimeKeylessProviders.delete(providerName);
this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
}
this.#reloadStaticModels();
diff --git a/packages/coding-agent/src/cursor.ts b/packages/coding-agent/src/cursor.ts
index 7a823dcae..6e4fe597f 100644
--- a/packages/coding-agent/src/cursor.ts
+++ b/packages/coding-agent/src/cursor.ts
@@ -33,7 +33,7 @@ function createToolResultMessage(
role: "toolResult",
toolCallId,
toolName,
- content: result.content.map(c => (c.type === "text" ? { ...c, text: sanitizeText(c.text) } : c)),
+ content: result.content,
details: result.details,
isError,
timestamp: Date.now(),
@@ -100,7 +100,7 @@ async function executeTool(
};
options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result: sanitizedFinalResult, isError });
- return createToolResultMessage(toolCallId, toolName, sanitizedFinalResult, isError);
+ return createToolResultMessage(toolCallId, toolName, result, isError);
}
async function executeDelete(options: CursorExecBridgeOptions, pathArg: string, toolCallId: string) {
@@ -315,7 +315,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
result: sanitizedFinalResult,
isError,
});
- return createToolResultMessage(toolCallId, toolName, sanitizedFinalResult, isError);
+ return createToolResultMessage(toolCallId, toolName, result, isError);
}
async diagnostics(args: Parameters>[0]) {
diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts
index f9bfb68ac..6339b21c7 100644
--- a/packages/coding-agent/src/modes/acp/acp-agent.ts
+++ b/packages/coding-agent/src/modes/acp/acp-agent.ts
@@ -1150,7 +1150,7 @@ export class AcpAgent implements Agent {
setThinkingLevel: level => record.session.setThinkingLevel(level),
getSessionName: () => record.session.sessionManager.getSessionName(),
setSessionName: async name => {
- await record.session.sessionManager.setSessionName(name);
+ await record.session.sessionManager.setSessionName(name, "user");
},
},
{
diff --git a/packages/coding-agent/src/modes/components/status-line.ts b/packages/coding-agent/src/modes/components/status-line.ts
index 420a94580..dfe7df2c4 100644
--- a/packages/coding-agent/src/modes/components/status-line.ts
+++ b/packages/coding-agent/src/modes/components/status-line.ts
@@ -9,7 +9,7 @@ import { theme } from "../../modes/theme/theme";
import type { AgentSession } from "../../session/agent-session";
import { calculatePromptTokens } from "../../session/compaction/compaction";
import * as git from "../../utils/git";
-import { getSessionAccentHexForTitle } from "../../utils/session-color";
+import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../../utils/session-color";
import { sanitizeStatusText } from "../shared";
import {
canReuseCachedPr,
@@ -476,10 +476,8 @@ export class StatusLineComponent implements Component {
this.session.sessionManager?.getSessionName(),
this.session.sessionManager?.titleSource,
);
- const gapColor = accentHex
- ? (Bun.color(accentHex, "ansi-16m") ?? theme.getFgAnsi("border"))
- : theme.getFgAnsi("border");
- const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[0m`;
+ const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
+ const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
return leftGroup + gapFill + rightGroup;
}
diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts
index fc243e277..44f328e5c 100644
--- a/packages/coding-agent/src/modes/components/status-line/segments.ts
+++ b/packages/coding-agent/src/modes/components/status-line/segments.ts
@@ -5,7 +5,7 @@ import { TERMINAL } from "@oh-my-pi/pi-tui";
import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
import { theme } from "../../../modes/theme/theme";
import { shortenPath } from "../../../tools/render-utils";
-import { getSessionAccentHex } from "../../../utils/session-color";
+import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
import { sanitizeStatusText } from "../../shared";
import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
@@ -362,8 +362,7 @@ const sessionNameSegment: StatusLineSegment = {
const name = ctx.session.sessionManager?.getSessionName();
if (!name) return { content: "", visible: false };
- const hex = getSessionAccentHex(name);
- const ansi = Bun.color(hex, "ansi-16m") ?? theme.getFgAnsi("accent");
+ const ansi = getSessionAccentAnsi(getSessionAccentHex(name)) ?? theme.getFgAnsi("accent");
return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
},
};
diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts
index 2f3495d32..a97159093 100644
--- a/packages/coding-agent/src/modes/interactive-mode.ts
+++ b/packages/coding-agent/src/modes/interactive-mode.ts
@@ -39,7 +39,7 @@ import { STTController, type SttState } from "../stt";
import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
import type { EventBus } from "../utils/event-bus";
import { getEditorCommand, openInEditor } from "../utils/external-editor";
-import { getSessionAccentHexForTitle } from "../utils/session-color";
+import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../utils/session-color";
import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
import type { AssistantMessageComponent } from "./components/assistant-message";
import type { BashExecutionComponent } from "./components/bash-execution";
@@ -534,14 +534,9 @@ export class InteractiveMode implements InteractiveModeContext {
this.editor.borderColor = theme.getPythonModeBorderColor();
} else {
const hex = getSessionAccentHexForTitle(this.sessionManager.getSessionName(), this.sessionManager.titleSource);
- if (hex) {
- const ansi = Bun.color(hex, "ansi-16m");
- if (ansi) {
- this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[0m`;
- } else {
- const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
- this.editor.borderColor = theme.getThinkingBorderColor(level);
- }
+ const ansi = getSessionAccentAnsi(hex);
+ if (ansi) {
+ this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
} else {
const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
this.editor.borderColor = theme.getThinkingBorderColor(level);
diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts
index 643e0192c..d7a94c144 100644
--- a/packages/coding-agent/src/modes/print-mode.ts
+++ b/packages/coding-agent/src/modes/print-mode.ts
@@ -75,7 +75,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
setThinkingLevel: level => session.setThinkingLevel(level),
getSessionName: () => session.sessionManager.getSessionName(),
setSessionName: async name => {
- await session.sessionManager.setSessionName(name);
+ await session.sessionManager.setSessionName(name, "user");
},
},
// ExtensionContextActions
diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts
index 3affb1acd..1a21ab842 100644
--- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts
+++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts
@@ -442,7 +442,7 @@ export async function runRpcMode(session: AgentSession): Promise {
setThinkingLevel: level => session.setThinkingLevel(level),
getSessionName: () => session.sessionManager.getSessionName(),
setSessionName: async name => {
- await session.sessionManager.setSessionName(name);
+ await session.sessionManager.setSessionName(name, "user");
},
},
// ExtensionContextActions
diff --git a/packages/coding-agent/src/utils/session-color.ts b/packages/coding-agent/src/utils/session-color.ts
index 5f86ff190..a2de529f3 100644
--- a/packages/coding-agent/src/utils/session-color.ts
+++ b/packages/coding-agent/src/utils/session-color.ts
@@ -44,3 +44,12 @@ export function getSessionAccentHexForTitle(
if (!name || titleSource === "auto") return undefined;
return getSessionAccentHex(name);
}
+
+/**
+ * Convert a hex accent color to an ANSI-16m foreground escape sequence.
+ * Returns `undefined` if `hex` is nullish or Bun.color conversion fails.
+ */
+export function getSessionAccentAnsi(hex: string | undefined): string | undefined {
+ if (!hex) return undefined;
+ return Bun.color(hex, "ansi-16m") ?? undefined;
+}
diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md
index 43f6bf519..a582167ea 100644
--- a/packages/natives/CHANGELOG.md
+++ b/packages/natives/CHANGELOG.md
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
+### Added
+
+- Added support for honoring the `ZIG` environment variable when resolving the Zig executable for native builds
## [14.0.5] - 2026-04-11
### Breaking Changes
diff --git a/packages/natives/scripts/build-native.ts b/packages/natives/scripts/build-native.ts
index d32807577..e7225fb09 100644
--- a/packages/natives/scripts/build-native.ts
+++ b/packages/natives/scripts/build-native.ts
@@ -1,7 +1,7 @@
-import * as fsSync from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { $ } from "bun";
+import { detectHostAvx2Support } from "./host-detect";
const repoRoot = path.join(import.meta.dir, "../../..");
const rustDir = path.join(repoRoot, "crates/pi-natives");
@@ -34,48 +34,6 @@ if (configuredVariantRaw) {
configuredVariant = configuredVariantRaw;
}
-function runCommand(command: string, args: string[]): string | null {
- try {
- const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" });
- if (result.exitCode !== 0) return null;
- return result.stdout.toString("utf-8").trim();
- } catch {
- return null;
- }
-}
-
-function detectHostAvx2Support(): boolean {
- if (process.arch !== "x64") return false;
-
- if (process.platform === "linux") {
- try {
- const cpuInfo = fsSync.readFileSync("/proc/cpuinfo", "utf8");
- return /\bavx2\b/i.test(cpuInfo);
- } catch {
- return false;
- }
- }
-
- if (process.platform === "darwin") {
- const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
- if (leaf7 && /\bAVX2\b/i.test(leaf7)) return true;
- const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
- return Boolean(features && /\bAVX2\b/i.test(features));
- }
-
- if (process.platform === "win32") {
- const output = runCommand("powershell.exe", [
- "-NoProfile",
- "-NonInteractive",
- "-Command",
- "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
- ]);
- return output?.toLowerCase() === "true";
- }
-
- return false;
-}
-
function resolveEffectiveVariant(): X64Variant | null {
if (targetArch !== "x64") return null;
if (configuredVariant) return configuredVariant;
@@ -101,7 +59,7 @@ function resolveSafeHostZigBuildConfig(): SafeHostZigBuildConfig | null {
return null;
}
- const realZigPath = Bun.which("zig");
+ const realZigPath = Bun.env.ZIG ?? Bun.which("zig");
if (!realZigPath) {
return null;
}
diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md
index 1dcf831d7..445a7fc18 100644
--- a/packages/tui/CHANGELOG.md
+++ b/packages/tui/CHANGELOG.md
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
+### Changed
+
+- Changed truncation debug logging to run only when `debugRedraw` is enabled
## [14.0.5] - 2026-04-11
diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts
index a2113306d..2348398b4 100644
--- a/packages/tui/src/tui.ts
+++ b/packages/tui/src/tui.ts
@@ -1196,19 +1196,18 @@ export class TUI extends Container {
let truncatedLine = line;
const isImage = TERMINAL.isImageLine(line);
if (!isImage && visibleWidth(line) > width) {
- // Debug log for component developers (not a crash)
- const debugLogPath = getDebugLogPath();
- const debugData = [
- `[TUI Truncate] ${new Date().toISOString()}`,
- `Line ${i} truncated: ${visibleWidth(line)} > ${width}`,
- `Content preview: ${line.slice(0, 100)}...`,
- "",
- ].join("\n");
- try {
- fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
- fs.appendFileSync(debugLogPath, debugData);
- } catch {
- // Ignore write errors - truncation should still work
+ if (debugRedraw) {
+ const debugData = [
+ `[TUI Truncate] ${new Date().toISOString()}`,
+ `Line ${i} truncated: ${visibleWidth(line)} > ${width}`,
+ `Content preview: ${line.slice(0, 100)}...`,
+ "",
+ ].join("\n");
+ try {
+ fs.appendFileSync(getDebugLogPath(), debugData);
+ } catch {
+ // Ignore write errors - truncation should still work
+ }
}
truncatedLine = truncateToWidth(line, width, Ellipsis.Omit);
}
diff --git a/scripts/ci-build-native.ts b/scripts/ci-build-native.ts
index ed3131797..256be3baa 100644
--- a/scripts/ci-build-native.ts
+++ b/scripts/ci-build-native.ts
@@ -1,8 +1,8 @@
#!/usr/bin/env bun
-import * as fs from "node:fs";
import * as path from "node:path";
import { $ } from "bun";
+import { detectHostAvx2Support } from "../packages/natives/scripts/host-detect";
interface NativeBuildVariant {
name: "baseline" | "modern";
@@ -37,48 +37,6 @@ function parseTargetVariants(): NativeBuildVariant[] {
});
}
-function runCommand(command: string, args: string[]): string | null {
- try {
- const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" });
- if (result.exitCode !== 0) return null;
- return result.stdout.toString("utf-8").trim();
- } catch {
- return null;
- }
-}
-
-function detectHostAvx2Support(): boolean {
- if (process.arch !== "x64") return false;
-
- if (process.platform === "linux") {
- try {
- const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
- return /\bavx2\b/i.test(cpuInfo);
- } catch {
- return false;
- }
- }
-
- if (process.platform === "darwin") {
- const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
- if (leaf7 && /\bAVX2\b/i.test(leaf7)) return true;
- const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
- return Boolean(features && /\bAVX2\b/i.test(features));
- }
-
- if (process.platform === "win32") {
- const output = runCommand("powershell.exe", [
- "-NoProfile",
- "-NonInteractive",
- "-Command",
- "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
- ]);
- return output?.toLowerCase() === "true";
- }
-
- return false;
-}
-
function resolveExpectedAddons(variants: NativeBuildVariant[]): string[] {
if (variants.length > 0) {
return variants.map(variant => `${targetPlatform}-${targetArch}-${variant.name}`);
diff --git a/scripts/host-detect.ts b/scripts/host-detect.ts
new file mode 100644
index 000000000..2976a174b
--- /dev/null
+++ b/scripts/host-detect.ts
@@ -0,0 +1,43 @@
+import * as fs from "node:fs";
+
+function runCommand(command: string, args: string[]): string | null {
+ try {
+ const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" });
+ if (result.exitCode !== 0) return null;
+ return result.stdout.toString("utf-8").trim();
+ } catch {
+ return null;
+ }
+}
+
+export function detectHostAvx2Support(): boolean {
+ if (process.arch !== "x64") return false;
+
+ if (process.platform === "linux") {
+ try {
+ const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
+ return /\bavx2\b/i.test(cpuInfo);
+ } catch {
+ return false;
+ }
+ }
+
+ if (process.platform === "darwin") {
+ const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
+ if (leaf7 && /\bAVX2\b/i.test(leaf7)) return true;
+ const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
+ return Boolean(features && /\bAVX2\b/i.test(features));
+ }
+
+ if (process.platform === "win32") {
+ const output = runCommand("powershell.exe", [
+ "-NoProfile",
+ "-NonInteractive",
+ "-Command",
+ "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
+ ]);
+ return output?.toLowerCase() === "true";
+ }
+
+ return false;
+}