Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2355,6 +2355,40 @@ def _trunc(name: str, limit: int) -> str:
)


def _prepend_turn_note_to_message(note: str, message):
"""Prepend a one-shot CLI note to text or native multimodal content."""
note = str(note or "").strip()
if not note:
return message

if isinstance(message, str):
return f"{note}\n\n{message}" if message else note

if isinstance(message, list):
updated = []
inserted = False
for part in message:
if (
not inserted
and isinstance(part, dict)
and part.get("type") == "text"
and isinstance(part.get("text"), str)
):
new_part = dict(part)
text = new_part.get("text", "")
new_part["text"] = f"{note}\n\n{text}" if text else note
updated.append(new_part)
inserted = True
else:
updated.append(part)

if not inserted:
return [{"type": "text", "text": note}, *updated]
return updated

return f"{note}\n\n{message}"


def _should_auto_attach_clipboard_image_on_paste(pasted_text: str) -> bool:
"""Auto-attach clipboard images only for image-only paste gestures."""
return not pasted_text.strip()
Expand Down Expand Up @@ -12138,14 +12172,14 @@ def run_agent():
# Prepend pending model switch note so the model knows about the switch
_msn = getattr(self, '_pending_model_switch_note', None)
if _msn:
agent_message = _msn + "\n\n" + agent_message
agent_message = _prepend_turn_note_to_message(_msn, agent_message)
self._pending_model_switch_note = None
# Prepend pending /reload-skills note so the model sees which
# skills were added/removed before handling this turn. Same
# one-shot queue pattern as the model-switch note above.
_srn = getattr(self, '_pending_skills_reload_note', None)
if _srn:
agent_message = _srn + "\n\n" + agent_message
agent_message = _prepend_turn_note_to_message(_srn, agent_message)
self._pending_skills_reload_note = None
try:
result = self.agent.run_conversation(
Expand Down
31 changes: 31 additions & 0 deletions tests/cli/test_cli_image_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
HermesCLI,
_collect_query_images,
_format_image_attachment_badges,
_prepend_turn_note_to_message,
_termux_example_image_path,
)

Expand Down Expand Up @@ -107,3 +108,33 @@ def test_compact_badges_summarize_multiple_images(self, tmp_path):
badges = _format_image_attachment_badges([img1, img2], image_counter=2, width=45)

assert badges == "[πŸ“Ž 2 images attached]"


class TestPrependTurnNoteToMessage:
def test_prepends_note_to_text_message(self):
message = _prepend_turn_note_to_message("[MODEL SWITCHED]", "describe this")

assert message == "[MODEL SWITCHED]\n\ndescribe this"

def test_prepends_note_to_native_image_text_part_without_mutating_original(self):
original = [
{"type": "text", "text": "What do you see?\n\n[Image attached at: /tmp/a.png]"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}},
]

message = _prepend_turn_note_to_message("[MODEL SWITCHED]", original)

assert isinstance(message, list)
assert message[0]["text"].startswith("[MODEL SWITCHED]\n\nWhat do you see?")
assert message[1] == original[1]
assert original[0]["text"].startswith("What do you see?")

def test_inserts_text_part_when_native_message_has_only_images(self):
original = [
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}},
]

message = _prepend_turn_note_to_message("[MODEL SWITCHED]", original)

assert message[0] == {"type": "text", "text": "[MODEL SWITCHED]"}
assert message[1] == original[0]
Loading