feat: added /rename command to set session titles for status/header/tab display

- Added `/rename <title>` slash command to set explicit session names and update header/tab titles.
- Added `session_name` status segment with hash-derived accent color for session titles.
- Fixed shell execution failure sanitization to preserve all `execResult` fields after redacting stderr.
- Fixed tool execution completion output to pass original `toolResult` text instead of sanitized `content`.
This commit is contained in:
can1357
2026-04-13 01:09:38 +02:00
parent debb76723d
commit d2caf2770c
19 changed files with 100 additions and 146 deletions
+1 -2
View File
@@ -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
+5 -5
View File
@@ -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;
+5 -8
View File
@@ -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 <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
### 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
+1 -1
View File
@@ -65,7 +65,7 @@
"puppeteer": "catalog:",
"turndown": "catalog:",
"turndown-plugin-gfm": "catalog:",
"zod": "catalog:pinned"
"zod": "catalog:"
},
"devDependencies": {
"@types/bun": "catalog:",
@@ -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();
+3 -3
View File
@@ -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]) {
@@ -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");
},
},
{
@@ -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;
}
@@ -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 };
},
};
@@ -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);
@@ -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
@@ -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
@@ -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;
}
+3
View File
@@ -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
+2 -44
View File
@@ -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;
}
+3
View File
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
### Changed
- Changed truncation debug logging to run only when `debugRedraw` is enabled
## [14.0.5] - 2026-04-11
+12 -13
View File
@@ -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);
}
+1 -43
View File
@@ -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}`);
+43
View File
@@ -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;
}