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'")