mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
154509b929
Typing `@shot.png` in the line attaches that picture to the turn — no separate command — and `@notes.txt` appends the file as text under a `--- @path ---` header, capped like a tool result with a resumable `truncate_token`. `@"my shot.png"` is the quoted form, and Tab completes the path after `@` (directories end in `/`, a name with a space completes quoted). An image is skipped, with a note naming the key to set, on a model without `image_in`; a missing or denylisted path is a note too, and the turn still goes. The default catalogs mark what can take a picture (`deepseek-flash` yes, `deepseek-v4-pro` no, the OpenAI defaults yes). `Agent.run` takes the parts, so the turn is stored with its images and shows up as `[image]` wherever only prose is wanted.
89 lines
2.8 KiB
Python
89 lines
2.8 KiB
Python
"""``@path`` mentions: parsing, token spelling, and Tab completion."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from plyngent.cli.mentions import (
|
|
complete_mention_paths,
|
|
mention_completions,
|
|
mention_token_for,
|
|
parse_mentions,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
def _paths(line: str) -> list[str]:
|
|
return [mention.path for mention in parse_mentions(line)]
|
|
|
|
|
|
def test_a_mention_is_a_whitespace_delimited_token() -> None:
|
|
assert _paths("look at @shot.png please") == ["shot.png"]
|
|
assert _paths("@a.png and @b/c.txt") == ["a.png", "b/c.txt"]
|
|
assert _paths("@a.png\n@b.png") == ["a.png", "b.png"]
|
|
|
|
|
|
def test_an_email_address_is_not_a_mention() -> None:
|
|
assert _paths("mail me at someone@example.com") == []
|
|
|
|
|
|
def test_a_quoted_mention_may_hold_spaces() -> None:
|
|
assert _paths('look at @"screens/my shot.png"') == ["screens/my shot.png"]
|
|
assert _paths('@"a b.png" and @c.png') == ["a b.png", "c.png"]
|
|
|
|
|
|
def test_an_unterminated_quote_is_left_alone() -> None:
|
|
assert _paths('still typing @"my sh') == []
|
|
|
|
|
|
def test_a_bare_at_sign_is_not_a_mention() -> None:
|
|
assert _paths("@ and @@@") == []
|
|
|
|
|
|
def test_a_token_quotes_a_path_with_spaces() -> None:
|
|
assert mention_token_for("shot.png") == "@shot.png"
|
|
assert mention_token_for("my shot.png") == '@"my shot.png"'
|
|
|
|
|
|
def test_completion_lists_a_directory(tmp_path: Path) -> None:
|
|
(tmp_path / "shot.png").write_bytes(b"")
|
|
(tmp_path / "screens").mkdir()
|
|
|
|
completions = complete_mention_paths("sc", cwd=tmp_path)
|
|
|
|
assert completions == ["screens/"]
|
|
|
|
|
|
def test_completion_continues_through_a_directory(tmp_path: Path) -> None:
|
|
(tmp_path / "screens").mkdir()
|
|
(tmp_path / "screens" / "shot.png").write_bytes(b"")
|
|
|
|
assert complete_mention_paths("screens/sh", cwd=tmp_path) == ["screens/shot.png"]
|
|
assert complete_mention_paths("screens/", cwd=tmp_path) == ["screens/shot.png"]
|
|
|
|
|
|
def test_completion_hides_dot_files_unless_asked(tmp_path: Path) -> None:
|
|
(tmp_path / ".env").write_text("x", encoding="utf-8")
|
|
|
|
assert complete_mention_paths("", cwd=tmp_path) == []
|
|
assert complete_mention_paths(".", cwd=tmp_path) == [".env"]
|
|
|
|
|
|
def test_completion_ignores_a_missing_directory(tmp_path: Path) -> None:
|
|
assert complete_mention_paths("nope/x", cwd=tmp_path) == []
|
|
|
|
|
|
def test_completions_keep_the_at_sign_and_quote_spaces(tmp_path: Path) -> None:
|
|
(tmp_path / "my shot.png").write_bytes(b"")
|
|
(tmp_path / "shots").mkdir()
|
|
|
|
assert mention_completions("@my", cwd=tmp_path) == ['@"my shot.png"']
|
|
assert mention_completions("@shots", cwd=tmp_path) == ["@shots/"]
|
|
assert mention_completions("@", cwd=tmp_path) == ['@"my shot.png"', "@shots/"]
|
|
|
|
|
|
def test_mention_completions_ignores_a_plain_word() -> None:
|
|
assert mention_completions("shot") == []
|