diff --git a/features/tui_input_modes.feature b/features/tui_input_modes.feature index 63a4f72c6..be4fa926f 100644 --- a/features/tui_input_modes.feature +++ b/features/tui_input_modes.feature @@ -42,3 +42,8 @@ Feature: TUI input modes Given a TUI prompt value "inspect @reso" When I clear pending TUI reference token via escape Then the TUI prompt value should be "inspect" + + Scenario: Escape removes pending reference token embedded in text + Given a TUI prompt value "draft notes about @README.md formatting" + When I clear pending TUI reference token via escape + Then the TUI prompt value should be "draft notes about formatting" diff --git a/src/cleveragents/tui/app.py b/src/cleveragents/tui/app.py index 36c36f431..1021f8457 100644 --- a/src/cleveragents/tui/app.py +++ b/src/cleveragents/tui/app.py @@ -61,11 +61,13 @@ class SessionView: def _strip_pending_reference_token(value: str) -> str: - """Remove the trailing @ reference trigger (if any). + """Remove the most recent ``@`` reference trigger (if any). - This trims whitespace and drops the last token that begins with ``@`` when the - token does not contain any embedded whitespace. Escaped triggers (``\\@``) - remain untouched. + This trims whitespace and removes the final token that begins with ``@``. The + token is removed even when additional text follows it so prompts such as + ``"draft @README.md summary"`` normalise to ``"draft summary"``. Escaped + triggers (``\\@``) are preserved to avoid stripping literal ``@`` + characters. """ if not isinstance(value, str): @@ -82,11 +84,24 @@ def _strip_pending_reference_token(value: str) -> str: if last_at > 0 and candidate[last_at - 1] == "\\": return candidate - suffix = candidate[last_at:] - if any(ch.isspace() for ch in suffix): - return candidate + prefix = candidate[:last_at] + suffix = candidate[last_at + 1 :] - return candidate[:last_at].rstrip() + token_end = 0 + while token_end < len(suffix) and not suffix[token_end].isspace(): + token_end += 1 + + suffix_after = suffix[token_end:] + prefix_clean = prefix.rstrip() + suffix_clean = suffix_after.lstrip() + + if prefix_clean and suffix_clean: + return f"{prefix_clean} {suffix_clean}" + if prefix_clean: + return prefix_clean + if suffix_clean: + return suffix_clean + return "" class _CommandRouter(Protocol): @@ -202,12 +217,10 @@ if _TEXTUAL_AVAILABLE: ref_picker = self.query_one("#reference-picker", ReferencePickerOverlay) if getattr(ref_picker, "visible", False): - self._remove_reference_trigger(prompt) - ref_picker.hide() - prompt = self.query_one("#prompt", PromptInput) prompt_value = getattr(prompt, "value", "") if isinstance(prompt_value, str): prompt.value = _strip_pending_reference_token(prompt_value) + ref_picker.hide() focus_method = getattr(prompt, "focus", None) if callable(focus_method): focus_method() @@ -217,34 +230,6 @@ if _TEXTUAL_AVAILABLE: if callable(focus_method): focus_method() - @staticmethod - def _remove_reference_trigger(prompt: PromptInput) -> None: - value = getattr(prompt, "value", "") - if not value: - return - - trigger_index = value.rfind("@") - if trigger_index == -1: - return - - prefix = value[:trigger_index] - suffix = value[trigger_index + 1 :] - - token_end = 0 - while token_end < len(suffix) and not suffix[token_end].isspace(): - token_end += 1 - - suffix_after = suffix[token_end:] - prefix_clean = prefix.rstrip() - suffix_clean = suffix_after.lstrip() - - if prefix_clean and suffix_clean: - prompt.value = f"{prefix_clean} {suffix_clean}" - elif prefix_clean: - prompt.value = prefix_clean - else: - prompt.value = suffix_clean - def _refresh_persona_bar(self) -> None: persona = self._persona_state.active_persona(self._session.session_id) preset = self._persona_state.current_preset(self._session.session_id)