Commit bca1374a9dad

Vincent Demeester <vincent@sbr.pm>
2026-02-26 11:25:35
pi/extension: skip OSC output in non-interactive modes
Introduced an isInteractive flag set at session_start by checking ctx.hasUI. Terminal title updates, notifications, and bell are now skipped when running in RPC or JSON mode, preventing OSC escape sequences from corrupting output.
1 parent c61167f
Changed files (1)
dots
pi
agent
dots/pi/agent/extensions/terminal-status.ts
@@ -37,11 +37,16 @@ let currentBranch = "";
 // Terminal Title
 // =============================================================================
 
+/** Whether we're running in interactive TUI mode (not RPC/JSON/print) */
+let isInteractive = !process.argv.includes("--mode");
+
 /**
  * Set terminal tab/window title using ANSI escape codes
  * OSC 0 = icon + title, OSC 2 = window title, OSC 30 = tab title
+ * Skipped in non-interactive modes (RPC, JSON) to avoid corrupting output
  */
 function setTerminalTitle(title: string): void {
+  if (!isInteractive) return;
   process.stderr.write(`\x1b]0;${title}\x07`);
   process.stderr.write(`\x1b]2;${title}\x07`);
   process.stderr.write(`\x1b]30;${title}\x07`);
@@ -260,6 +265,7 @@ function detectTerminal(): "kitty" | "wsl" | "osc777" | "linux-desktop" {
 }
 
 function notify(title: string, body: string): void {
+  if (!isInteractive) return;
   const terminal = detectTerminal();
 
   switch (terminal) {
@@ -285,8 +291,10 @@ function notify(title: string, body: string): void {
 
 /**
  * Ring terminal bell (works with kitty bell_on_tab, etc.)
+ * Skipped in non-interactive modes to avoid corrupting output
  */
 function ringBell(): void {
+  if (!isInteractive) return;
   process.stderr.write("\x07");
 }
 
@@ -297,6 +305,9 @@ function ringBell(): void {
 export default function (pi: ExtensionAPI) {
   // Set initial title on session start
   pi.on("session_start", async (_event, ctx) => {
+    // Detect non-interactive modes (RPC, JSON, print) to skip terminal output
+    // process.argv check in init covers most cases; ctx.hasUI is a backup
+    if (ctx.hasUI === false) isInteractive = false;
     currentBranch = getGitBranch();
     // Get initial model if available
     if (ctx.model) {