Skip to content

Commit 18ab1cd

Browse files
authored
Update commands.py
1 parent 94a4419 commit 18ab1cd

1 file changed

Lines changed: 50 additions & 9 deletions

File tree

python_agent_harness/tui/commands.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
from rich.live import Live
1818

1919
from .. import config
20+
from ..attachments import image_placeholder, parse_at_references, reattach_images
2021
from ..commands import find_command
21-
from ..models import Message
22+
from ..models import ImagePart, Message, TextPart
2223
from ..persistence import (
2324
SessionPersistence,
2425
escape_role_headers,
@@ -57,7 +58,7 @@ class CommandMixin:
5758

5859
def _start_agent(
5960
self,
60-
text: str,
61+
text: str | Message,
6162
system: str | None = None,
6263
restore: Callable[[], None] | None = None,
6364
) -> None: ...
@@ -258,14 +259,39 @@ def _restore() -> None:
258259

259260
restore = _restore
260261
self.console.print(f"[cyan]/{name}: {kickoff.strip()}[/cyan]")
261-
self._start_agent(kickoff, system=system, restore=restore)
262+
# Parse @file references in the kickoff (e.g. "/review @diff.patch"
263+
# or "/explain @client.py"): images become ImagePart attachments,
264+
# text files become TextPart attachments, and the @path token is
265+
# stripped from the text. Validation errors are shown to the user.
266+
cleaned_kickoff, attachments, errors = parse_at_references(
267+
kickoff, str(self.session.project_dir)
268+
)
269+
for err in errors:
270+
self.console.print(f"[red]@{err.path}: {err.message}[/red]")
271+
if attachments:
272+
parts: list[Any] = []
273+
if cleaned_kickoff.strip():
274+
parts.append(TextPart(text=cleaned_kickoff))
275+
for att in attachments:
276+
parts.append(att.part)
277+
kickoff_msg = Message(role="user", content=parts)
278+
else:
279+
kickoff_msg = Message(role="user", content=cleaned_kickoff)
280+
self._start_agent(kickoff_msg, system=system, restore=restore)
262281

263282
def _conversation_text(self) -> str:
264283
msgs = self.session.last_messages or []
265284
parts = []
266285
for m in msgs:
267286
# escaped: see persistence.escape_role_headers
268287
body = escape_role_headers(m.text())
288+
# Mark messages that contained image attachments (see
289+
# Session._conversation_text for details)
290+
if isinstance(m.content, list):
291+
image_count = sum(1 for p in m.content if isinstance(p, ImagePart))
292+
if image_count:
293+
paths = [p.path for p in m.content if isinstance(p, ImagePart) and p.path]
294+
body = f"{image_placeholder(image_count, paths)}\n{body}"
269295
if body:
270296
parts.append(f"**{m.role}**: {body}")
271297
return "\n\n".join(parts)
@@ -740,6 +766,25 @@ def _parse_saved_body(body: str) -> list[Message]:
740766
current_role: str | None = None
741767
current_lines: list[str] = []
742768

769+
def _flush(role: str, lines: list[str]) -> None:
770+
content = "\n".join(lines).strip()
771+
if not content:
772+
return
773+
# A leading image-attachment placeholder (written on save)
774+
# is re-attached when the file still exists, so the restored
775+
# message is multimodal again; otherwise it stays as text.
776+
new_content, parts = reattach_images(content)
777+
if parts:
778+
remainder = new_content.split("\n", 1)
779+
rest_text = remainder[1].strip() if len(remainder) > 1 else ""
780+
content_parts: list = []
781+
if rest_text:
782+
content_parts.append(TextPart(text=rest_text))
783+
content_parts.extend(parts)
784+
messages.append(Message(role=role, content=content_parts))
785+
else:
786+
messages.append(Message(role=role, content=content))
787+
743788
for line in body.splitlines():
744789
# Check for a role header: **user**: ... or **assistant**: ...
745790
header = split_role_header(line)
@@ -749,19 +794,15 @@ def _parse_saved_body(body: str) -> list[Message]:
749794
# tool_call_id/name; system blocks would duplicate the
750795
# live system prompt the client prepends per request)
751796
if current_role is not None and current_role not in ("tool", "system"):
752-
content = "\n".join(current_lines).strip()
753-
if content:
754-
messages.append(Message(role=current_role, content=content))
797+
_flush(current_role, current_lines)
755798
current_role = role
756799
current_lines = [unescape_role_header(rest)]
757800
continue
758801
current_lines.append(unescape_role_header(line))
759802

760803
# Don't forget the last block (tool/system blocks dropped, see above)
761804
if current_role is not None and current_role not in ("tool", "system"):
762-
content = "\n".join(current_lines).strip()
763-
if content:
764-
messages.append(Message(role=current_role, content=content))
805+
_flush(current_role, current_lines)
765806

766807
return messages
767808

0 commit comments

Comments
 (0)