Group multiline terminal commands

Execute multiline terminal input as one current-shell compound so intermediate prompts cannot complete queued work early. Preserve shell state and cover Bash and PowerShell grouping with regression tests.

Alessandro committed Jul 10, 2026 at 18:53 UTC 9837ff34329c3915aba1933f6454a820b9ae1382
4 files changed +22
plugins/_code_execution/AGENTS.md
+1
@@ -14,6 +14,7 @@
14 ## Local Contracts
15
16 - Keep session concurrency, timeout, streaming, and reset behavior predictable.
17 +- Execute multi-line terminal input as one current-shell compound so intermediate prompts cannot mark queued work complete; preserve `cd`, exports, and other shell state.
18 - Terminal reset/close must not hang on foreground commands or shells that ignore SIGTERM.
19 - Explicitly target local versus SSH execution runtimes.
20 - Do not hardcode secrets, SSH credentials, or local user paths.
plugins/_code_execution/README.md
+1
@@ -17,6 +17,7 @@ This plugin provides the code execution tool used by agents for development task
17
18 - **Persistent shells**
19 - Maintains per-session shell state in agent data so subsequent calls can reuse the same terminal session.
20 + - Groups multi-line terminal input in the current shell so only the final prompt marks the command complete.
21 - **Multiple runtimes**
22 - Dispatches requests based on `runtime`: `terminal`, `python`, `nodejs`, `output`, or `reset`.
23 - **Remote execution support**
plugins/_code_execution/tools/code_execution_tool.py
+11
@@ -27,6 +27,14 @@ def _is_closed_pty_error(exc: BaseException) -> bool:
27 return False
28
29
30 +def _group_multiline_command(command: str, powershell: bool = False) -> str:
31 + body = command.rstrip("\n")
32 + if "\n" not in body:
33 + return body
34 + opener = ". {" if powershell else "{"
35 + return f"{opener}\n{body}\n}}"
36 +
37 +
38 @dataclass
39 class ShellWrap:
40 id: int
@@ -165,6 +173,9 @@ class CodeExecution(Tool):
173 + self.format_command_for_output(command)
174 + "\n\n"
175 )
176 + command = _group_multiline_command(
177 + command, powershell=runtime.is_windows() and not cfg["ssh_enabled"]
178 + )
179 return await self.terminal_session(cfg, session, command, reset, prefix)
180
181 async def terminal_session(
tests/test_code_execution_pager.py
+9
@@ -8,6 +8,7 @@ import asyncio
8
9 from plugins._code_execution.helpers import shell_local, shell_ssh
10 from plugins._code_execution.helpers.tty_session import TTYSession
11 +from plugins._code_execution.tools.code_execution_tool import _group_multiline_command
12
13
14 def test_local_env_disables_pagers_and_preserves_existing():
@@ -35,6 +36,14 @@ def test_ssh_command_disables_pagers():
36 assert "PAGER=cat" in shell_ssh.PAGER_DISABLE_COMMAND
37
38
39 +def test_multiline_terminal_commands_are_one_current_shell_compound():
40 + assert _group_multiline_command("pwd") == "pwd"
41 + assert _group_multiline_command("cd /tmp\npwd") == "{\ncd /tmp\npwd\n}"
42 + assert _group_multiline_command("$env:FOO='bar'\n$env:FOO", powershell=True) == (
43 + ". {\n$env:FOO='bar'\n$env:FOO\n}"
44 + )
45 +
46 +
47 def test_tty_close_kills_term_resistant_process():
48 async def run():
49 session = TTYSession("bash -lc 'trap \"\" TERM; sleep 30'")