Resolve ``@path`` mentions and build context attachments. Mirrors ``processAtMentionedFiles`` in ``typescript/src/utils/attachments.ts``: if a mention resolves to a directory we build a ``Listed directory`` attachment containing its entries (up to 1000); if it resolves to a readable
(
text: str,
*,
cwd: str | None = None,
)
| 483 | |
| 484 | |
| 485 | def expand_at_mentions( |
| 486 | text: str, |
| 487 | *, |
| 488 | cwd: str | None = None, |
| 489 | ) -> tuple[str, list[dict[str, Any]]]: |
| 490 | """Resolve ``@path`` mentions and build context attachments. |
| 491 | |
| 492 | Mirrors ``processAtMentionedFiles`` in |
| 493 | ``typescript/src/utils/attachments.ts``: if a mention resolves to a |
| 494 | directory we build a ``Listed directory`` attachment containing its |
| 495 | entries (up to 1000); if it resolves to a readable image file |
| 496 | (png/jpg/jpeg/gif/webp) we attach a ``kind="image"`` attachment with |
| 497 | base64 + media_type via the image pipeline, so the REPL can inline it |
| 498 | as a real image content block in the user message instead of mojibake |
| 499 | in a system-reminder. Other readable files we attach the |
| 500 | file's contents verbatim. The returned ``text`` is left unchanged — the |
| 501 | caller prepends / appends the attachments before sending to the model. |
| 502 | """ |
| 503 | cwd = cwd or os.getcwd() |
| 504 | seen: set[str] = set() |
| 505 | attachments: list[dict[str, Any]] = [] |
| 506 | |
| 507 | for match in _FILE_MENTION_RE.finditer(text): |
| 508 | raw = match.group(1).rstrip(".,!?:;\"'`)]}") |
| 509 | if not raw: |
| 510 | continue |
| 511 | # Skip bare @word mentions (no path separator / dot / home marker). |
| 512 | if not ( |
| 513 | raw.startswith(("/", "~", "./", "../")) |
| 514 | or "/" in raw |
| 515 | or "." in raw |
| 516 | ): |
| 517 | continue |
| 518 | |
| 519 | # C5: the search dialog inserts ``@file#Lline`` (TS |
| 520 | # attachments.ts:2859-2861 parses the ``#L10-20`` fragment). |
| 521 | # Degraded port: strip the fragment so the FILE attaches — the |
| 522 | # line reference stays visible to the model in the prompt text; |
| 523 | # range slicing is a noted follow-up. |
| 524 | fragment_match = re.search(r"#L\d+(?:-\d+)?$", raw) |
| 525 | if fragment_match: |
| 526 | raw = raw[: fragment_match.start()] |
| 527 | if not raw: |
| 528 | continue |
| 529 | |
| 530 | expanded = os.path.expanduser(raw) |
| 531 | if not os.path.isabs(expanded): |
| 532 | expanded = os.path.abspath(os.path.join(cwd, expanded)) |
| 533 | if expanded in seen: |
| 534 | continue |
| 535 | seen.add(expanded) |
| 536 | |
| 537 | try: |
| 538 | if os.path.isdir(expanded): |
| 539 | entries = sorted(os.listdir(expanded)) |
| 540 | truncated = len(entries) > _MAX_DIR_ENTRIES |
| 541 | shown = entries[:_MAX_DIR_ENTRIES] |
| 542 | if truncated: |