Skip to content

Commit f4fff47

Browse files
authored
Update input.py
1 parent beec95c commit f4fff47

1 file changed

Lines changed: 65 additions & 5 deletions

File tree

‎python_agent_harness/tui/input.py‎

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from prompt_toolkit.formatted_text import FormattedText
1717
from prompt_toolkit.history import FileHistory
1818
from prompt_toolkit.key_binding import KeyBindings
19+
from prompt_toolkit.keys import Keys
1920
from prompt_toolkit.patch_stdout import patch_stdout
2021
from rich.console import Console
2122
from rich.text import Text
@@ -78,13 +79,20 @@ def _history_path() -> str:
7879
return str(d / "input_history")
7980

8081

81-
def _make_key_bindings() -> KeyBindings:
82+
def _make_key_bindings(on_image_paste: Callable[[str], None] | None = None) -> KeyBindings:
8283
"""Esc+Enter (or Alt+Enter) submits; plain Enter inserts a newline.
8384
8485
Tab triggers completion explicitly (first Tab inserts the common
8586
part / opens the menu, further Tabs cycle), Shift+Tab cycles
8687
backwards — prompt_toolkit's defaults don't reliably bind Tab in
8788
every mode/version.
89+
90+
``on_image_paste`` (when given) is called with the temp-file path of
91+
a clipboard image captured on paste; the TUI records it as a pending
92+
attachment and a human-readable marker is inserted into the buffer.
93+
Passing the path via the callback (not an ``@path`` text token)
94+
avoids the whitespace-delimited ``@file`` parser truncating a temp
95+
path that contains spaces (e.g. macOS ``/var/folders/.../T/``).
8896
"""
8997
kb = KeyBindings()
9098

@@ -108,11 +116,57 @@ def _complete_backward(event: Any) -> None:
108116
else:
109117
b.start_completion(select_first=True)
110118

119+
@kb.add(Keys.BracketedPaste)
120+
def _paste(event: Any) -> None:
121+
"""Handle a terminal paste.
122+
123+
A terminal paste delivers text, never image bytes. So on paste
124+
we check the OS clipboard out-of-band: if it holds an image, it
125+
is written to a temp file and handed to ``on_image_paste`` (the
126+
TUI records it as a pending attachment); a short marker is
127+
inserted so the user sees the image was captured. Otherwise the
128+
pasted text is inserted as usual, preserving default paste
129+
behavior.
130+
131+
Image capture is best-effort and must never break paste: any
132+
failure falls back to inserting the pasted text.
133+
"""
134+
buffer = event.current_buffer
135+
data = event.data or ""
136+
# Only inspect the clipboard for an image when the paste carried
137+
# no text: an image paste delivers empty/whitespace bracketed
138+
# data, while a text paste delivers the text. This keeps text
139+
# pastes (the common case) from spawning a clipboard subprocess.
140+
if on_image_paste is not None and not data.strip():
141+
try:
142+
from ..clipboard import grab_clipboard_image
143+
144+
path = grab_clipboard_image()
145+
except Exception: # noqa: BLE001 - capture must never break paste
146+
path = None
147+
if path:
148+
on_image_paste(path)
149+
# A non-@ marker: the path is tracked out-of-band, so it
150+
# need not (and must not) survive the @file parser. The
151+
# trailing space lets the user keep typing a prompt.
152+
from ..attachments import clipboard_image_marker
153+
154+
buffer.insert_text(clipboard_image_marker(path))
155+
return
156+
# Normal text paste: normalise line endings exactly like
157+
# prompt_toolkit's default BracketedPaste handler (some terminals
158+
# paste \r\n / \r), which this binding overrides.
159+
data = data.replace("\r\n", "\n").replace("\r", "\n")
160+
buffer.insert_text(data)
161+
111162
return kb
112163

113164

114165
def _make_prompt_session(
115-
history: FileHistory, completer: Completer, **kwargs: Any
166+
history: FileHistory,
167+
completer: Completer,
168+
on_image_paste: Callable[[str], None] | None = None,
169+
**kwargs: Any,
116170
) -> PromptSession:
117171
"""Create the TUI's input session.
118172
@@ -122,6 +176,9 @@ def _make_prompt_session(
122176
then bails out without inserting the common part). Tab must be the
123177
single, deterministic trigger.
124178
179+
``on_image_paste`` is forwarded to the key bindings so a clipboard
180+
image captured on paste is recorded as a pending attachment.
181+
125182
``enable_suspend`` is off on Windows: Ctrl-Z (suspend) is a Unix
126183
terminal feature with no Windows equivalent.
127184
@@ -135,7 +192,7 @@ def _make_prompt_session(
135192
try:
136193
return PromptSession(
137194
history=history,
138-
key_bindings=_make_key_bindings(),
195+
key_bindings=_make_key_bindings(on_image_paste),
139196
completer=completer,
140197
complete_while_typing=False,
141198
multiline=True,
@@ -148,7 +205,7 @@ def _make_prompt_session(
148205

149206
return PromptSession(
150207
history=history,
151-
key_bindings=_make_key_bindings(),
208+
key_bindings=_make_key_bindings(on_image_paste),
152209
completer=completer,
153210
complete_while_typing=False,
154211
multiline=True,
@@ -236,7 +293,10 @@ def get_completions(self, document: Any, complete_event: Any):
236293
yield from self._complete_paths(arg)
237294
return
238295
token = text.rsplit(" ", 1)[-1] if " " in text else text
239-
if token.startswith("~") or "/" in token:
296+
if token.startswith("@"):
297+
# @file reference: complete the path after @
298+
yield from self._complete_paths(token[1:])
299+
elif token.startswith("~") or "/" in token:
240300
yield from self._complete_paths(token)
241301

242302

0 commit comments

Comments
 (0)