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:
parent
46966123f4
commit
6811a79b62
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue