diff --git a/backend/packages/harness/deerflow/mcp/tools.py b/backend/packages/harness/deerflow/mcp/tools.py index f6b2d9603c..de11cba254 100644 --- a/backend/packages/harness/deerflow/mcp/tools.py +++ b/backend/packages/harness/deerflow/mcp/tools.py @@ -288,6 +288,50 @@ def _rewrite_unique_bare_filenames( return rewritten +def _rewrite_changed_paths_with_spaces( + text: str, + *, + changed_files: Iterable[Path], + thread_id: str, + user_id: str, + source_base_dir: Path | None, +) -> str: + """Rewrite changed paths with spaces only at unambiguous text boundaries.""" + candidates: dict[str, set[str]] = {} + for path in changed_files: + spellings = [str(path)] + if path.anchor == "/": + # Some servers print file URIs without percent-encoding spaces. + spellings.extend((f"file:{path}", f"file://{path}")) + if source_base_dir is not None: + try: + relative = path.relative_to(source_base_dir).as_posix() + except ValueError: + pass + else: + spellings.extend((relative, f"./{relative}")) + matching_spellings = [spelling for spelling in spellings if any(char.isspace() for char in spelling) and spelling in text] + if not matching_spellings: + continue + virtual_path = _local_uri_to_virtual_path(str(path), thread_id=thread_id, user_id=user_id) + if virtual_path is None: + continue + for spelling in matching_spellings: + candidates.setdefault(spelling, set()).add(virtual_path) + + rewritten = text + for spelling in sorted(candidates, key=len, reverse=True): + destinations = candidates[spelling] + if len(destinations) != 1 or spelling not in rewritten: + continue + # Whitespace after an unquoted spelling may continue a longer filename. + # Require punctuation or the end of the text instead of rewriting a prefix. + pattern = re.compile(rf"(?