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 <title>` 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<string, string> = new Map(); - #runtimeKeylessProviders: Set<string> = new Set(); #runtimeProvidersBySource: Map<string, Set<string>> = new Map(); #runtimeProviderSourceByName: Map<string, string> = 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<NonNullable<ICursorExecHandlers["diagnostics"]>>[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<never> { 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; +}