fix(tui): ensure escape clears reference token

Refs: #6450
This commit is contained in:
2026-04-11 03:26:37 +00:00
parent ada862158e
commit 1091c76479
2 changed files with 29 additions and 39 deletions
+5
View File
@@ -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"
+24 -39
View File
@@ -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)