mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 00:41:43 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4540fb949e |
15
cli.py
15
cli.py
@@ -1203,6 +1203,11 @@ def _format_image_attachment_badges(attached_images: list[Path], image_counter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
def _collect_query_images(query: str | None, image_arg: str | None = None) -> tuple[str, list[Path]]:
|
def _collect_query_images(query: str | None, image_arg: str | None = None) -> tuple[str, list[Path]]:
|
||||||
"""Collect local image attachments for single-query CLI flows."""
|
"""Collect local image attachments for single-query CLI flows."""
|
||||||
message = query or ""
|
message = query or ""
|
||||||
@@ -6282,6 +6287,9 @@ class HermesCLI:
|
|||||||
|
|
||||||
if result.get("success") and result.get("transcript", "").strip():
|
if result.get("success") and result.get("transcript", "").strip():
|
||||||
transcript = result["transcript"].strip()
|
transcript = result["transcript"].strip()
|
||||||
|
self._attached_images.clear()
|
||||||
|
if hasattr(self, '_app') and self._app:
|
||||||
|
self._app.invalidate()
|
||||||
self._pending_input.put(transcript)
|
self._pending_input.put(transcript)
|
||||||
submitted = True
|
submitted = True
|
||||||
elif result.get("success"):
|
elif result.get("success"):
|
||||||
@@ -8006,8 +8014,9 @@ class HermesCLI:
|
|||||||
"""Handle terminal paste — detect clipboard images.
|
"""Handle terminal paste — detect clipboard images.
|
||||||
|
|
||||||
When the terminal supports bracketed paste, Ctrl+V / Cmd+V
|
When the terminal supports bracketed paste, Ctrl+V / Cmd+V
|
||||||
triggers this with the pasted text. We also check the
|
triggers this with the pasted text. We only auto-attach a
|
||||||
clipboard for an image on every paste event.
|
clipboard image for image-only/empty paste gestures so text
|
||||||
|
pastes and dictation do not accidentally attach stale images.
|
||||||
|
|
||||||
Large pastes (5+ lines) are collapsed to a file reference
|
Large pastes (5+ lines) are collapsed to a file reference
|
||||||
placeholder while preserving any existing user text in the
|
placeholder while preserving any existing user text in the
|
||||||
@@ -8017,7 +8026,7 @@ class HermesCLI:
|
|||||||
# Normalise line endings — Windows \r\n and old Mac \r both become \n
|
# Normalise line endings — Windows \r\n and old Mac \r both become \n
|
||||||
# so the 5-line collapse threshold and display are consistent.
|
# so the 5-line collapse threshold and display are consistent.
|
||||||
pasted_text = pasted_text.replace('\r\n', '\n').replace('\r', '\n')
|
pasted_text = pasted_text.replace('\r\n', '\n').replace('\r', '\n')
|
||||||
if self._try_attach_clipboard_image():
|
if _should_auto_attach_clipboard_image_on_paste(pasted_text) and self._try_attach_clipboard_image():
|
||||||
event.app.invalidate()
|
event.app.invalidate()
|
||||||
if pasted_text:
|
if pasted_text:
|
||||||
line_count = pasted_text.count('\n')
|
line_count = pasted_text.count('\n')
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from hermes_cli.clipboard import (
|
|||||||
_windows_has_image,
|
_windows_has_image,
|
||||||
_convert_to_png,
|
_convert_to_png,
|
||||||
)
|
)
|
||||||
|
from cli import _should_auto_attach_clipboard_image_on_paste
|
||||||
|
|
||||||
FAKE_PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
|
FAKE_PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
|
||||||
FAKE_BMP = b"BM" + b"\x00" * 100
|
FAKE_BMP = b"BM" + b"\x00" * 100
|
||||||
@@ -919,6 +920,48 @@ class TestTryAttachClipboardImage:
|
|||||||
assert path.suffix == ".png"
|
assert path.suffix == ".png"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoAttachClipboardImageOnPaste:
|
||||||
|
def test_skips_auto_attach_for_plain_text_paste(self):
|
||||||
|
assert _should_auto_attach_clipboard_image_on_paste("hello world") is False
|
||||||
|
|
||||||
|
def test_skips_auto_attach_for_whitespace_and_text_paste(self):
|
||||||
|
assert _should_auto_attach_clipboard_image_on_paste(" hello world ") is False
|
||||||
|
|
||||||
|
def test_allows_auto_attach_for_empty_paste(self):
|
||||||
|
assert _should_auto_attach_clipboard_image_on_paste("") is True
|
||||||
|
|
||||||
|
def test_allows_auto_attach_for_whitespace_only_paste(self):
|
||||||
|
assert _should_auto_attach_clipboard_image_on_paste(" \n\t ") is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestVoiceSubmission:
|
||||||
|
@pytest.fixture
|
||||||
|
def cli(self):
|
||||||
|
from cli import HermesCLI
|
||||||
|
cli_obj = HermesCLI.__new__(HermesCLI)
|
||||||
|
cli_obj._attached_images = [Path("/tmp/stale.png")]
|
||||||
|
cli_obj._pending_input = queue.Queue()
|
||||||
|
cli_obj._voice_lock = MagicMock()
|
||||||
|
cli_obj._voice_processing = True
|
||||||
|
cli_obj._voice_recording = True
|
||||||
|
cli_obj._voice_continuous = False
|
||||||
|
cli_obj._no_speech_count = 0
|
||||||
|
cli_obj._voice_recorder = MagicMock()
|
||||||
|
cli_obj._voice_recorder.stop.return_value = "/tmp/fake.wav"
|
||||||
|
cli_obj._app = None
|
||||||
|
return cli_obj
|
||||||
|
|
||||||
|
def test_voice_transcript_clears_stale_attached_images(self, cli):
|
||||||
|
with patch("tools.voice_mode.play_beep"):
|
||||||
|
with patch("tools.voice_mode.transcribe_recording", return_value={"success": True, "transcript": "hello"}):
|
||||||
|
with patch("os.path.isfile", return_value=False):
|
||||||
|
with patch("cli._cprint"):
|
||||||
|
cli._voice_stop_and_transcribe()
|
||||||
|
|
||||||
|
assert cli._attached_images == []
|
||||||
|
assert cli._pending_input.get_nowait() == "hello"
|
||||||
|
|
||||||
|
|
||||||
# ═════════════════════════════════════════════════════════════════════════
|
# ═════════════════════════════════════════════════════════════════════════
|
||||||
# Level 4: Queue routing — tuple unpacking in process_loop
|
# Level 4: Queue routing — tuple unpacking in process_loop
|
||||||
# ═════════════════════════════════════════════════════════════════════════
|
# ═════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user