diff --git a/CHANGELOG.md b/CHANGELOG.md index 032bdcdd8..c33225c51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Tools: default `exec` exit notifications and auto-migrate legacy `tools.bash` to `tools.exec`. - Tools: add tmux-style `process send-keys` and bracketed paste helpers for PTY sessions. - Tools: add `process submit` helper to send CR for PTY sessions. +- Tools: respond to PTY cursor position queries to unblock interactive TUIs. - Status: trim `/status` to current-provider usage only and drop the OAuth/token block. - Directory: unify `clawdbot directory` across channels and plugin channels. - UI: allow deleting sessions from the Control UI. diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index d09e14df4..9f4f1481f 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -29,6 +29,7 @@ import { truncateMiddle, } from "./bash-tools.shared.js"; import { getShellConfig, sanitizeBinaryOutput } from "./shell-utils.js"; +import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js"; const DEFAULT_MAX_OUTPUT = clampNumber( readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), @@ -451,7 +452,17 @@ export function createExecTool( }; if (pty) { - pty.onData(handleStdout); + const cursorResponse = buildCursorPositionResponse(); + pty.onData((data) => { + const raw = data.toString(); + const { cleaned, requests } = stripDsrRequests(raw); + if (requests > 0) { + for (let i = 0; i < requests; i += 1) { + pty.write(cursorResponse); + } + } + handleStdout(cleaned); + }); } else if (child) { child.stdout.on("data", handleStdout); child.stderr.on("data", handleStderr); diff --git a/src/agents/pty-dsr.test.ts b/src/agents/pty-dsr.test.ts new file mode 100644 index 000000000..f2c629cfe --- /dev/null +++ b/src/agents/pty-dsr.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "vitest"; + +import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js"; + +test("stripDsrRequests removes cursor queries and counts them", () => { + const input = "hi\x1b[6nthere\x1b[?6n"; + const { cleaned, requests } = stripDsrRequests(input); + expect(cleaned).toBe("hithere"); + expect(requests).toBe(2); +}); + +test("buildCursorPositionResponse returns CPR sequence", () => { + expect(buildCursorPositionResponse()).toBe("\x1b[1;1R"); + expect(buildCursorPositionResponse(12, 34)).toBe("\x1b[12;34R"); +}); diff --git a/src/agents/pty-dsr.ts b/src/agents/pty-dsr.ts new file mode 100644 index 000000000..0a3533ec6 --- /dev/null +++ b/src/agents/pty-dsr.ts @@ -0,0 +1,14 @@ +const DSR_PATTERN = /\x1b\[\??6n/g; + +export function stripDsrRequests(input: string): { cleaned: string; requests: number } { + let requests = 0; + const cleaned = input.replace(DSR_PATTERN, () => { + requests += 1; + return ""; + }); + return { cleaned, requests }; +} + +export function buildCursorPositionResponse(row = 1, col = 1): string { + return `\x1b[${row};${col}R`; +}