diff --git a/packages/server/src/server/terminal-mcp/terminal-manager.ts b/packages/server/src/server/terminal-mcp/terminal-manager.ts index 8df00af65..cb33706a3 100644 --- a/packages/server/src/server/terminal-mcp/terminal-manager.ts +++ b/packages/server/src/server/terminal-mcp/terminal-manager.ts @@ -6,6 +6,7 @@ import { listPanes, findWindowByName, capturePaneContent, + extractExitCodeMarkerFromOutput, sendText as tmuxSendText, sendKeys as tmuxSendKeys, renameWindow, @@ -382,7 +383,9 @@ export class TerminalManager { // Use stored values for dead panes, current values for live ones const workingDirectory = await getStoredWorkingDirectory(window.id, paneId); const currentCommand = await getStoredCommand(window.id, paneId); - const lastLines = await capturePaneContent(paneId, 5, false); + const rawLastLines = await capturePaneContent(paneId, 5, false); + const lastLinesExtracted = extractExitCodeMarkerFromOutput(rawLastLines); + const lastLines = lastLinesExtracted.output; let exitCode: number | null = null; if (isDead) { @@ -394,7 +397,7 @@ export class TerminalManager { "#{pane_dead_status}", ]); const parsed = parseInt(exitCodeStr, 10); - exitCode = Number.isFinite(parsed) ? parsed : null; + exitCode = Number.isFinite(parsed) ? parsed : lastLinesExtracted.exitCode; } commands.push({ @@ -427,9 +430,11 @@ export class TerminalManager { throw new Error(`Session '${this.sessionName}' not found.`); } - const paneId = `${commandId}.0`; + const paneId = `${commandId}.0`; - const output = await capturePaneContent(paneId, lines, false); + const rawOutput = await capturePaneContent(paneId, lines, false); + const extracted = extractExitCodeMarkerFromOutput(rawOutput); + const output = extracted.output; const deadStatus = await executeTmux([ "display-message", @@ -450,7 +455,7 @@ export class TerminalManager { "#{pane_dead_status}", ]); const parsed = parseInt(exitCodeStr, 10); - exitCode = Number.isFinite(parsed) ? parsed : null; + exitCode = Number.isFinite(parsed) ? parsed : extracted.exitCode; } return { diff --git a/packages/server/src/server/terminal-mcp/tmux.ts b/packages/server/src/server/terminal-mcp/tmux.ts index 532d6d3b7..7e507a980 100644 --- a/packages/server/src/server/terminal-mcp/tmux.ts +++ b/packages/server/src/server/terminal-mcp/tmux.ts @@ -49,6 +49,8 @@ let shellConfig: { type: ShellType } = { type: "bash" }; const ANSI_ESCAPE_REGEX = /\u001B[\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; +const EXIT_CODE_MARKER = "__PASEO_EXIT_CODE__:"; + function stripAnsiSequences(value: string): string { if (!value) { return ""; @@ -56,6 +58,33 @@ function stripAnsiSequences(value: string): string { return value.replace(ANSI_ESCAPE_REGEX, ""); } +export function extractExitCodeMarkerFromOutput(output: string): { + exitCode: number | null; + output: string; +} { + if (!output) { + return { exitCode: null, output }; + } + + let exitCode: number | null = null; + const kept: string[] = []; + + for (const line of output.split("\n")) { + const trimmed = line.trim(); + if (trimmed.startsWith(EXIT_CODE_MARKER)) { + const codeStr = trimmed.slice(EXIT_CODE_MARKER.length).trim(); + const parsed = parseInt(codeStr, 10); + if (Number.isFinite(parsed)) { + exitCode = parsed; + } + continue; + } + kept.push(line); + } + + return { exitCode, output: kept.join("\n").trimEnd() }; +} + export function setShellConfig(config: { type: string }): void { // Validate shell type const validShells: ShellType[] = ["bash", "zsh", "fish"]; @@ -559,7 +588,7 @@ export async function executeCommand({ // Execute command via respawn-pane with bash -c wrapper // This ensures all bash features work (pipes, operators, etc.) - const wrappedCommand = `bash -c 'cd "${expandedPath}" && ${command.replace(/'/g, "'\\''")} 2>&1; exit $?'`; + const wrappedCommand = `bash -c 'cd "${expandedPath}" && ${command.replace(/'/g, "'\\''")} 2>&1; code=$?; echo ${EXIT_CODE_MARKER}$code; exit $code'`; await executeTmux(["respawn-pane", "-t", pane.id, "-k", wrappedCommand]); // Wait for command to finish or reach stability @@ -584,6 +613,8 @@ export async function executeCommand({ if (isDead) { // Command finished - capture output and exit code output = await capturePaneContent(pane.id, 1000, false); + const extracted = extractExitCodeMarkerFromOutput(output); + output = extracted.output; const exitCodeStr = await executeTmux([ "display-message", "-p", @@ -592,7 +623,7 @@ export async function executeCommand({ "#{pane_dead_status}", ]); const parsed = parseInt(exitCodeStr, 10); - exitCode = Number.isFinite(parsed) ? parsed : null; + exitCode = Number.isFinite(parsed) ? parsed : extracted.exitCode; break; }