diff --git a/nanobot/utils/tool_hints.py b/nanobot/utils/tool_hints.py index 9b6d29911..9758700b1 100644 --- a/nanobot/utils/tool_hints.py +++ b/nanobot/utils/tool_hints.py @@ -19,9 +19,11 @@ _TOOL_FORMATS: dict[str, tuple[list[str], str, bool, bool]] = { "list_dir": (["path"], "ls {}", True, False), } -# Matches file paths embedded in shell commands (Windows drive, ~/, or absolute after space) +# Matches file paths embedded in shell commands, including quoted paths with spaces. _PATH_IN_CMD_RE = re.compile( - r"(?:[A-Za-z]:[/\\]|~/|(?<=\s)/)[^\s;&|<>\"']+" + r'"(?P(?:[A-Za-z]:[/\\]|~/|/)[^"]+)"' + r"|'(?P(?:[A-Za-z]:[/\\]|~/|/)[^']+)'" + r"|(?P(?:[A-Za-z]:[/\\]|~/|(?<=\s)/)[^\s;&|<>\"']+)" ) @@ -92,9 +94,14 @@ def _fmt_known(tc, fmt: tuple) -> str: def _abbreviate_command(cmd: str, max_len: int = 40) -> str: """Abbreviate paths in a command string, then truncate.""" - abbreviated = _PATH_IN_CMD_RE.sub( - lambda m: abbreviate_path(m.group(), max_len=25), cmd - ) + def _replace_path(match: re.Match[str]) -> str: + if match.group("double") is not None: + return f'"{abbreviate_path(match.group("double"), max_len=25)}"' + if match.group("single") is not None: + return f"'{abbreviate_path(match.group('single'), max_len=25)}'" + return abbreviate_path(match.group("bare"), max_len=25) + + abbreviated = _PATH_IN_CMD_RE.sub(_replace_path, cmd) if len(abbreviated) <= max_len: return abbreviated return abbreviated[:max_len - 1] + "\u2026" diff --git a/tests/agent/test_tool_hint.py b/tests/agent/test_tool_hint.py index 080a0b1e3..b8ba99284 100644 --- a/tests/agent/test_tool_hint.py +++ b/tests/agent/test_tool_hint.py @@ -72,6 +72,22 @@ class TestToolHintKnownTools: result = _hint([_tc("exec", {"command": cmd})]) assert "\u2026/" in result + def test_exec_abbreviates_quoted_linux_paths_with_spaces(self): + """Quoted Unix paths with spaces should still be folded.""" + cmd = 'cd "/home/user/My Documents/project" && pytest tests/' + result = _hint([_tc("exec", {"command": cmd})]) + assert "\u2026/" in result + assert '"/home/user/My Documents/project"' not in result + assert '"' in result + + def test_exec_abbreviates_quoted_windows_paths_with_spaces(self): + """Quoted Windows paths with spaces should still be folded.""" + cmd = 'cd "C:/Program Files/Git/project" && git status' + result = _hint([_tc("exec", {"command": cmd})]) + assert "\u2026/" in result + assert '"C:/Program Files/Git/project"' not in result + assert '"' in result + def test_exec_short_command_unchanged(self): result = _hint([_tc("exec", {"command": "npm install typescript"})]) assert result == "$ npm install typescript"