fix(desktop): quote persisted @image: paths so spaced paths render

The unquoted alternative in the directive pattern is `\S+`, so a ref built
by string interpolation truncates at the first space and strands the tail
as loose text next to a broken thumbnail. Composer images live in the app's
userData dir, which on macOS is `~/Library/Application Support/<App>/` — so
every pasted or dropped image hit this.

Adds format_reference_value next to REFERENCE_PATTERN, mirroring
formatRefValue in the desktop's directive-text.tsx, and covers the
round-trip through the parser.
This commit is contained in:
Brooklyn Nicholson 2026-07-24 20:14:12 -05:00
parent 46966123f4
commit 6811a79b62
4 changed files with 57 additions and 4 deletions

View File

@ -19,6 +19,7 @@ REFERENCE_PATTERN = re.compile(
rf"(?<![\w/])@(?:(?P<simple>diff|staged)\b|(?P<kind>file|folder|git|url):(?P<value>{_QUOTED_REFERENCE_VALUE}(?::\d+(?:-\d+)?)?|\S+))"
)
TRAILING_PUNCTUATION = ",.;!?"
_NEEDS_QUOTING = re.compile(r"""[\s()\[\]{}<>"'`]""")
_SENSITIVE_HOME_DIRS = (".ssh", ".aws", ".gnupg", ".kube", ".docker", ".azure", ".config/gh")
_SENSITIVE_HERMES_DIRS = (Path("skills") / ".hub",)
_SENSITIVE_HOME_FILES = (
@ -60,6 +61,21 @@ class ContextReferenceResult:
blocked: bool = False
def format_reference_value(value: str) -> str:
"""Quote a reference value so ``REFERENCE_PATTERN`` reads it back whole.
The unquoted alternative in the pattern is ``\\S+``, so a path containing a
space parses as a truncated ref with the tail left behind as loose text.
Mirrors ``formatRefValue`` in the desktop's directive-text.tsx.
"""
if not _NEEDS_QUOTING.search(value):
return value
for quote in ("`", '"', "'"):
if quote not in value:
return f"{quote}{value}{quote}"
return value
def parse_context_references(message: str) -> list[ContextReference]:
refs: list[ContextReference] = []
if not message:

View File

@ -445,3 +445,25 @@ async def test_canonical_guard_fails_closed_when_lookup_raises(tmp_path: Path, m
"credential deny-list" in warning or "sensitive credential" in warning
for warning in result.warnings
)
@pytest.mark.parametrize(
"value",
[
"/tmp/plain.png",
"/Users/me/Library/Application Support/Hermes/composer-images/a.png",
r"C:\Users\John Doe\Pictures\cat.png",
"/tmp/report (final).pdf",
"/tmp/it's here.png",
'/tmp/say "hi".png',
],
)
def test_format_reference_value_round_trips_through_the_parser(value):
"""Whatever the path contains, the formatted ref must parse back whole —
an unquoted value stops at the first space and strands the tail as text."""
from agent.context_references import REFERENCE_PATTERN, format_reference_value
match = REFERENCE_PATTERN.search(f"@file:{format_reference_value(value)}")
assert match is not None
assert match.group("value").strip("`\"'") == value

View File

@ -13245,6 +13245,21 @@ def test_build_persist_message_with_image_refs_without_text_is_refs_only(monkeyp
assert server._build_persist_message_with_image_refs("", [str(img)]) == f"@image:{img}"
def test_build_persist_message_quotes_paths_containing_spaces(tmp_path):
"""The unquoted alternative in the directive pattern is ``\\S+``, so a path
with a space parses as a truncated ref with the tail left as loose text.
Desktop composer images live in the app's userData dir, which on macOS is
``~/Library/Application Support/...`` a space every time."""
img_dir = tmp_path / "Application Support" / "Hermes" / "composer-images"
img_dir.mkdir(parents=True)
img = img_dir / "cat.png"
img.write_bytes(b"png")
result = server._build_persist_message_with_image_refs("what is this?", [str(img)])
assert result == f"@image:`{img}`\nwhat is this?"
def test_prompt_submit_passes_persist_user_message_to_agent(monkeypatch):
"""#70720: _run_prompt_submit must forward the (image-ref-aware) persisted
user message to run_conversation via persist_user_message, so the gateway

View File

@ -5749,8 +5749,10 @@ def _build_persist_message_with_image_refs(user_text: str, image_paths: list[str
persisted as-is (it silently breaks image rendering after a full
restart, and reorders image/text on live session-switch reconciliation).
"""
from agent.context_references import format_reference_value
text = user_text or ""
refs = "\n".join(f"@image:{p}" for p in image_paths if Path(p).exists())
refs = "\n".join(f"@image:{format_reference_value(p)}" for p in image_paths if Path(p).exists())
if not refs:
return text
return f"{refs}\n{text}" if text else refs
@ -11015,9 +11017,7 @@ def _run_prompt_submit(
"conversation_history": list(history),
"stream_callback": _stream,
"persist_user_message": (
_build_persist_message_with_image_refs(prompt, images)
if images
else prompt
_build_persist_message_with_image_refs(prompt, images) if images else prompt
),
}
try: