core/cli+config: attach files to a turn with @path mentions

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.
This commit is contained in:
2026-10-21 18:52:05 +08:00
parent e9ee71e853
commit 154509b929
11 changed files with 577 additions and 24 deletions
+40
View File
@@ -170,6 +170,46 @@ def test_completer_stream_on_off(tmp_path: object, monkeypatch: object) -> None:
assert completer("o", 1) == "off"
def test_completer_path_mentions(tmp_path: object, monkeypatch: object) -> None:
"""An ``@path`` token completes files, wherever it sits in the line."""
import readline
from pathlib import Path
import pytest
assert isinstance(tmp_path, Path)
assert isinstance(monkeypatch, pytest.MonkeyPatch)
(tmp_path / "shot.png").write_bytes(b"")
(tmp_path / "screens").mkdir()
state = _minimal_state(tmp_path)
completer = build_completer(state)
monkeypatch.setattr(readline, "get_line_buffer", lambda: "look at @sh")
monkeypatch.setattr(readline, "get_begidx", lambda: len("look at "))
monkeypatch.chdir(tmp_path)
found = [item for index in range(10) if (item := completer("@sh", index)) is not None]
assert found == ["@shot.png"]
def test_a_plain_word_is_still_a_slash_argument(tmp_path: object, monkeypatch: object) -> None:
"""The mention branch must not swallow ordinary argument completion."""
import readline
from pathlib import Path
import pytest
assert isinstance(tmp_path, Path)
assert isinstance(monkeypatch, pytest.MonkeyPatch)
state = _minimal_state(tmp_path)
completer = build_completer(state)
monkeypatch.setattr(readline, "get_line_buffer", lambda: "/provider r")
monkeypatch.setattr(readline, "get_begidx", lambda: len("/provider "))
assert completer("r", 0) == "remote"
def test_complete_slash_args_from_registry(tmp_path: object) -> None:
from pathlib import Path