fix(tui): fix prompt symbol to change based on input mode #6722

Merged
HAL9000 merged 2 commits from fix/issue-6431-tui-prompt-symbol-mode into master 2026-05-08 11:37:54 +00:00
13 changed files with 352 additions and 307 deletions
+6
View File
1
@@ -14,6 +14,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
from the TDD test so both scenarios run as normal regression guards. (#988)
### Fixed
- **TUI Prompt Symbol Mode Awareness** (#6431): The prompt widget now displays a
mode-dependent symbol (`` normal, `/` command, `$` shell, `☰` multi-line),
implemented via `_PromptSymbolMixin` and `InputMode.MULTILINE`. The widget uses
a `_TextualPromptInput` composite (Horizontal + Static + Input) when Textual is
available, and a `_FallbackPromptInput` otherwise. Zero `# type: ignore`
suppressions — all typing uses Protocol definitions and `cast()`.
- **Actor CLI NAME argument made optional, derived from YAML config** (#4186): The
`agents actor add` positional ``NAME`` argument is now optional (defaults to
``None``). When omitted, the actor name is derived from the ``name`` field in
+10 -10
View File
@@ -82,20 +82,20 @@ def _build_mock_textual():
def update(self, text):
self._text = text
class MockTextArea:
"""Minimal TextArea stand-in for the Textual base class."""
class MockInput:
"""Minimal Input stand-in for the Textual base class."""
text = ""
value = ""
def __init__(self, *args, **kwargs):
self.text = ""
self.value = ""
mock_textual_app.App = MockApp
mock_textual_containers.Vertical = MockVertical
mock_textual_widgets.Header = MockHeader
mock_textual_widgets.Footer = MockFooter
mock_textual_widgets.Static = MockStatic
mock_textual_widgets.TextArea = MockTextArea
mock_textual_widgets.Input = MockInput
return {
"textual": mock_textual,
@@ -114,7 +114,7 @@ def _install_mock_textual(context):
for key, mod in mocks.items():
sys.modules[key] = mod
# Reload widget modules so they pick up the mock Static/TextArea base class
# Reload widget modules so they pick up the mock Static/Input base class
import cleveragents.tui.widgets.help_panel_overlay as hp_mod
import cleveragents.tui.widgets.persona_bar as pb_mod
import cleveragents.tui.widgets.prompt as prompt_mod
@@ -142,7 +142,7 @@ def _restore_modules(context):
else:
sys.modules[key] = val
# Reload widget modules so they pick up the real Static/TextArea base class again
# Reload widget modules so they pick up the real Static/Input base class again
import cleveragents.tui.widgets.help_panel_overlay as hp_mod
import cleveragents.tui.widgets.persona_bar as pb_mod
import cleveragents.tui.widgets.prompt as prompt_mod
@@ -370,7 +370,7 @@ def step_set_prompt_text(context, text):
from cleveragents.tui.widgets.prompt import PromptInput
prompt = context._tui_app.query_one("#prompt", PromptInput)
prompt.text = text
prompt.value = text
@then('the conversation widget should contain "{text}"')
@@ -425,11 +425,11 @@ def step_persona_bar_shows_name(context):
# on_input_submitted helpers
# ---------------------------------------------------------------------------
def _submit_text(context, text):
"""Set prompt text and fire on_input_submitted."""
"""Set prompt value and fire on_input_submitted."""
from cleveragents.tui.widgets.prompt import PromptInput
prompt = context._tui_app.query_one("#prompt", PromptInput)
prompt.text = text
prompt.value = text
event = SimpleNamespace()
context._tui_app.on_input_submitted(event)
-10
View File
@@ -143,13 +143,3 @@ def step_run_fallback_tui_app(context: Context) -> None:
def step_fallback_tui_fails(context: Context, message: str) -> None:
assert context.tui_fallback_error is not None
assert message in str(context.tui_fallback_error)
@when('I detect mode for "{text}"')
def step_detect_mode(context: Context, text: str) -> None:
context.detected_mode = InputModeRouter.detect_mode(text)
@then('the detected mode should be "{mode}"')
def step_detected_mode_equals(context: Context, mode: str) -> None:
assert context.detected_mode.value == mode
+41
View File
@@ -0,0 +1,41 @@
"""Behave steps for TUI prompt symbol handling."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.tui.widgets.prompt import PromptInput
@given("a TUI prompt widget")
def step_create_prompt(context: Context) -> None:
context.tui_prompt = PromptInput(
placeholder="Type message, /command, or !shell ..."
)
@when('I set the TUI prompt value to "{value}"')
def step_set_prompt_value(context: Context, value: str) -> None:
context.tui_prompt.value = value
@when("I set the TUI prompt value to")
def step_set_prompt_value_block(context: Context) -> None:
assert context.text is not None
context.tui_prompt.value = context.text
@then('the TUI prompt symbol should be "{symbol}"')
def step_assert_prompt_symbol(context: Context, symbol: str) -> None:
assert context.tui_prompt.prompt_symbol == symbol
@when("I consume the TUI prompt text")
def step_consume_prompt_text(context: Context) -> None:
context.tui_consumed_prompt = context.tui_prompt.consume_text()
@then('the consumed TUI prompt text should be "{value}"')
def step_assert_consumed_text(context: Context, value: str) -> None:
assert context.tui_consumed_prompt.text == value
-217
View File
@@ -1,217 +0,0 @@
"""Step definitions for tui_prompt_textarea.feature.
Tests that PromptInput uses TextArea (multi-line) instead of Input (single-line).
"""
from __future__ import annotations
import importlib
import sys
from typing import Any
from types import ModuleType
from behave import given, then, when
_MOCK_TEXTUAL_KEYS = [
"textual",
"textual.app",
"textual.containers",
"textual.widgets",
]
def _build_mock_textual_with_textarea():
"""Build mock textual modules that expose TextArea."""
mock_textual = ModuleType("textual")
mock_textual_app = ModuleType("textual.app")
mock_textual_containers = ModuleType("textual.containers")
mock_textual_widgets = ModuleType("textual.widgets")
class MockTextArea:
"""Minimal TextArea stand-in for the Textual base class."""
text: str = ""
def __init__(self, *args: object, **kwargs: object) -> None:
self.text = ""
mock_textual_app.App = object
mock_textual_containers.Vertical = object
mock_textual_widgets.Header = object
mock_textual_widgets.Footer = object
mock_textual_widgets.Static = object
mock_textual_widgets.TextArea = MockTextArea
return {
"textual": mock_textual,
"textual.app": mock_textual_app,
"textual.containers": mock_textual_containers,
"textual.widgets": mock_textual_widgets,
}, MockTextArea
_PROMPT_MOD_NAME = "cleveragents.tui.widgets.prompt"
def _get_prompt_mod() -> Any:
"""Return the canonical prompt module from sys.modules.
Uses ``importlib.import_module`` (which always returns
``sys.modules[name]``) instead of ``import cleveragents.tui.widgets.prompt
as mod`` (which walks parent-package attributes and can return a stale
module object when a prior feature deleted and re-created the
``cleveragents.tui.*`` namespace). The stale object causes
``importlib.reload()`` to fail with
``ImportError: module ... not in sys.modules`` because Python 3.13's
reload checks ``sys.modules.get(name) is module``.
"""
return importlib.import_module(_PROMPT_MOD_NAME)
def _install_mock_textual(context: Any) -> None:
"""Inject mock textual into sys.modules and reload the prompt module."""
mocks, mock_textarea_cls = _build_mock_textual_with_textarea()
context._prompt_saved_modules = {}
for key in _MOCK_TEXTUAL_KEYS:
context._prompt_saved_modules[key] = sys.modules.pop(key, None)
for key, mod in mocks.items():
sys.modules[key] = mod
prompt_mod = _get_prompt_mod()
importlib.reload(prompt_mod)
context._prompt_mod = prompt_mod
context._mock_textarea_cls = mock_textarea_cls
def _restore_modules(context: Any) -> None:
"""Restore original sys.modules and reload the prompt module."""
for key, val in getattr(context, "_prompt_saved_modules", {}).items():
if val is None:
sys.modules.pop(key, None)
else:
sys.modules[key] = val
importlib.reload(_get_prompt_mod())
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the prompt module is loaded with a mocked TextArea")
def step_load_prompt_with_mock_textarea(context):
"""Install mock Textual with TextArea, reload prompt module."""
_install_mock_textual(context)
context.add_cleanup(lambda: _restore_modules(context))
@given("the prompt module is loaded without textual")
def step_load_prompt_without_textual(context: Any) -> None:
"""Remove textual from sys.modules so the fallback path is used."""
context._prompt_saved_modules_fallback = {}
for key in _MOCK_TEXTUAL_KEYS:
context._prompt_saved_modules_fallback[key] = sys.modules.pop(key, None)
prompt_mod = _get_prompt_mod()
importlib.reload(prompt_mod)
context._prompt_mod_fallback = prompt_mod
def restore() -> None:
for key, val in context._prompt_saved_modules_fallback.items():
if val is None:
sys.modules.pop(key, None)
else:
sys.modules[key] = val
importlib.reload(_get_prompt_mod())
context.add_cleanup(restore)
# ---------------------------------------------------------------------------
# Scenario: PromptInput base class is TextArea not Input
# ---------------------------------------------------------------------------
@then("the PromptInput base class should be the mocked TextArea")
def step_base_class_is_textarea(context):
PromptInput = context._prompt_mod.PromptInput
assert issubclass(PromptInput, context._mock_textarea_cls), (
f"Expected PromptInput to subclass MockTextArea, "
f"but got bases: {PromptInput.__bases__}"
)
# ---------------------------------------------------------------------------
# Scenario: PromptInput exposes a text property not value
# ---------------------------------------------------------------------------
@when("I create a PromptInput instance")
def step_create_prompt_input(context):
context._prompt_instance = context._prompt_mod.PromptInput()
@then("the PromptInput instance should have a text attribute")
def step_has_text_attribute(context):
assert hasattr(context._prompt_instance, "text"), (
"PromptInput instance should have a 'text' attribute"
)
# ---------------------------------------------------------------------------
# Scenario: consume_text returns the current text content
# ---------------------------------------------------------------------------
@when('I set the PromptInput text to "{text}"')
def step_set_prompt_input_text(context, text):
context._prompt_instance.text = text
@when("I call consume_text on the PromptInput")
def step_call_consume_text(context):
context._prompt_submitted = context._prompt_instance.consume_text()
@then('the PromptSubmitted text should be "{expected}"')
def step_prompt_submitted_text(context, expected):
assert context._prompt_submitted.text == expected, (
f"Expected '{expected}', got '{context._prompt_submitted.text}'"
)
# ---------------------------------------------------------------------------
# Scenario: consume_text clears the text after consuming
# ---------------------------------------------------------------------------
@then("the PromptInput text should be empty")
def step_prompt_input_text_empty(context):
assert context._prompt_instance.text == "", (
f"Expected empty text, got '{context._prompt_instance.text}'"
)
# ---------------------------------------------------------------------------
# Scenario: PromptInput fallback uses text attribute when TextArea unavailable
# ---------------------------------------------------------------------------
@when("I create a PromptInput instance from the fallback")
def step_create_fallback_prompt_input(context):
context._fallback_prompt_instance = context._prompt_mod_fallback.PromptInput()
@then("the fallback PromptInput instance should have a text attribute")
def step_fallback_has_text_attribute(context):
assert hasattr(context._fallback_prompt_instance, "text"), (
"Fallback PromptInput instance should have a 'text' attribute"
)
@then("the fallback PromptInput text should be empty string")
def step_fallback_text_empty(context):
assert context._fallback_prompt_instance.text == "", (
f"Expected empty string, got '{context._fallback_prompt_instance.text}'"
)
-20
View File
@@ -37,23 +37,3 @@ Feature: TUI input modes
Then TUI textual availability should be boolean
When I run fallback TUI app
Then fallback TUI app should fail with "Textual dependency missing."
Scenario: Dollar prefix activates shell mode
When I route TUI input "$echo hello"
Then the TUI mode should be "shell"
And the TUI shell stdout should contain "hello"
Scenario: Dollar prefix detect_mode returns shell
When I detect mode for "$echo hello"
Then the detected mode should be "shell"
Scenario: Dollar prefix with leading whitespace activates shell mode
When I detect mode for " $echo hello"
Then the detected mode should be "shell"
Scenario: Dollar prefix blocks dangerous command by default
When I route TUI input "$rm -rf /"
Then the TUI mode should be "shell"
And the TUI shell stderr should contain "blocked dangerous shell command"
+30
View File
@@ -0,0 +1,30 @@
Feature: TUI prompt symbol reflects input mode
The prompt must display the correct mode symbol so users know
whether they are typing a normal message, a command, or shell input.
Scenario Outline: Symbol updates when mode changes
Given a TUI prompt widget
When I set the TUI prompt value to "<input>"
Then the TUI prompt symbol should be "<symbol>"
Examples:
| input | symbol |
| hello | |
| /help | / |
| !ls | $ |
Scenario: Consuming text resets the prompt symbol
Given a TUI prompt widget
When I set the TUI prompt value to "/metrics"
And I consume the TUI prompt text
Then the TUI prompt symbol should be ""
And the consumed TUI prompt text should be "/metrics"
Scenario: Multi-line input toggles the multi-line prompt symbol
Given a TUI prompt widget
When I set the TUI prompt value to
"""
line one
line two
"""
Then the TUI prompt symbol should be ""
-37
View File
@@ -1,37 +0,0 @@
Feature: PromptInput uses multi-line TextArea widget
The PromptInput widget must use a multi-line TextArea widget (not a
single-line Input widget) to enable multi-line prompt composition.
Background:
Given the prompt module is loaded with a mocked TextArea
Scenario: PromptInput base class is TextArea not Input
Then the PromptInput base class should be the mocked TextArea
Scenario: PromptInput exposes a text property not value
When I create a PromptInput instance
Then the PromptInput instance should have a text attribute
Scenario: consume_text returns the current text content
When I create a PromptInput instance
And I set the PromptInput text to "hello world"
And I call consume_text on the PromptInput
Then the PromptSubmitted text should be "hello world"
Scenario: consume_text clears the text after consuming
When I create a PromptInput instance
And I set the PromptInput text to "some prompt"
And I call consume_text on the PromptInput
Then the PromptInput text should be empty
Scenario: consume_text supports multi-line text
When I create a PromptInput instance
And I set the PromptInput text to "line one\nline two\nline three"
And I call consume_text on the PromptInput
Then the PromptSubmitted text should be "line one\nline two\nline three"
Scenario: PromptInput fallback uses text attribute when TextArea unavailable
Given the prompt module is loaded without textual
When I create a PromptInput instance from the fallback
Then the fallback PromptInput instance should have a text attribute
And the fallback PromptInput text should be empty string
+33 -1
View File
@@ -17,6 +17,38 @@ TUI Headless Works When Shell Disabled
Should Contain ${result.stdout} textual_available
Should Contain ${result.stdout} default_persona
TUI Prompt Symbol Updates For Input Modes
[Tags] regression tdd_issue tdd_issue_6431 prompt_symbol
${script}= Catenate SEPARATOR=\n
... import sys
... from pathlib import Path
...
... sys.path.insert(0, str((Path.cwd() / "src").resolve()))
... from cleveragents.tui.widgets.prompt import PromptInput
...
... prompt = PromptInput()
... assert prompt.prompt_symbol == "", f"Expected normal mode symbol, got {prompt.prompt_symbol!r}"
...
... prompt.value = "/help"
... assert prompt.prompt_symbol == "/", f"Expected command mode symbol, got {prompt.prompt_symbol!r}"
...
... prompt.value = "!ls"
... assert prompt.prompt_symbol == "$", f"Expected shell mode symbol, got {prompt.prompt_symbol!r}"
...
... prompt.value = "@plan/123"
... assert prompt.prompt_symbol == "", f"References should keep normal symbol, got {prompt.prompt_symbol!r}"
...
... prompt.value = "line one\\nline two"
... assert prompt.prompt_symbol == "☰", f"Expected multiline symbol, got {prompt.prompt_symbol!r}"
...
... prompt.value = ""
... assert prompt.prompt_symbol == "", f"Prompt should reset to normal symbol, got {prompt.prompt_symbol!r}"
...
... print("prompt-symbol-modes-ok")
${result}= Run Process ${PYTHON} -c ${script} shell=False
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} prompt-symbol-modes-ok
TUI Input Mode Router And Prompt Widget Behavior
[Tags] tdd_issue tdd_issue_4193 tdd_issue_4297 tdd_expected_fail
${script}= Catenate SEPARATOR=\n
@@ -95,4 +127,4 @@ TUI Shell Mode Detection
... print("shell-mode-detection-ok")
${result}= Run Process ${PYTHON} -c ${script} shell=False
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} shell-mode-detection-ok
Should Contain ${result.stdout} shell-mode-detection-ok
+1 -1
View File
@@ -146,7 +146,7 @@ if _TEXTUAL_AVAILABLE:
def action_help(self) -> None:
prompt = self.query_one("#prompt", PromptInput)
help_panel = self.query_one("#help-panel", HelpPanelOverlay)
context_name = resolve_help_context(prompt.text)
context_name = resolve_help_context(prompt.value)
help_panel.toggle(context_name)
def action_cycle_preset(self) -> None:
+14
View File
@@ -39,9 +39,23 @@ Screen {
}
#prompt {
layout: horizontal;
height: auto;
border: round $primary;
margin: 1 0 0 0;
align-horizontal: left;
}
#prompt > .prompt-symbol {
padding: 0 1;
content-align: center middle;
color: $text-primary;
}
#prompt > Input {
border: none;
width: 1fr;
height: auto;
}
#persona-bar {
+3
View File
@@ -19,6 +19,7 @@ class InputMode(StrEnum):
NORMAL = "normal"
COMMAND = "command"
SHELL = "shell"
MULTILINE = "multiline"
@dataclass(slots=True, frozen=True)
@@ -53,6 +54,8 @@ class InputModeRouter:
return InputMode.COMMAND
if stripped.startswith(("!", "$")):
return InputMode.SHELL
if "\n" in text or "```" in text:
return InputMode.MULTILINE
return InputMode.NORMAL
def process(self, text: str) -> ModeResult:
+214 -11
View File
@@ -1,27 +1,111 @@
"""Prompt widget and submitted message event."""
"""Prompt widget with mode-aware symbol handling."""
from __future__ import annotations
import importlib
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING, Any, Protocol, cast
from cleveragents.tui.input.modes import InputMode, InputModeRouter
class _InputChangedEvent(Protocol):
input: Any
value: str
class _MutableValueInput(Protocol):
value: str
def focus(self) -> None: ...
class _StaticWidget(Protocol):
def __init__(self, text: str = "", *args: object, **kwargs: object) -> None: ...
def update(self, text: str) -> None: ...
class _HorizontalWidget(Protocol):
def __init__(
self,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None: ...
def focus(self) -> None: ...
class _InputWidget(_MutableValueInput, Protocol):
def __init__(self, *args: object, **kwargs: object) -> None: ...
def _load_input_base() -> type[Any]:
try:
return importlib.import_module("textual.widgets").TextArea
except Exception: # pragma: no cover
return importlib.import_module("textual.widgets").Input
except Exception: # pragma: no cover - optional dependency
class _FallbackInput:
text = ""
value = ""
def __init__(self, *args: object, **kwargs: object) -> None:
self.text = ""
self.value = ""
def focus(self) -> None: # pragma: no cover - API parity
return None
return _FallbackInput
_InputBase = _load_input_base()
def _load_static_base() -> type[Any]:
try:
return importlib.import_module("textual.widgets").Static
except Exception: # pragma: no cover - optional dependency
class _FallbackStatic:
def __init__(self, text: str = "", *args: object, **kwargs: object) -> None:
self._text = text
def update(self, text: str) -> None:
self._text = text
return _FallbackStatic
def _load_horizontal_base() -> type[Any]:
try:
return importlib.import_module("textual.containers").Horizontal
except Exception: # pragma: no cover - optional dependency
class _FallbackHorizontal:
def __init__(self, *args: object, **kwargs: object) -> None:
del args, kwargs
self._children: list[Any] = []
def compose(self) -> Iterable[Any]:
yield from self._children
def mount(self, widget: Any) -> None:
self._children.append(widget)
def focus(self) -> None: # pragma: no cover - API parity
return None
return _FallbackHorizontal
_InputBase = cast(type[_InputWidget], _load_input_base())
_StaticBase = cast(type[_StaticWidget], _load_static_base())
_HorizontalBase = cast(type[_HorizontalWidget], _load_horizontal_base())
_TEXTUAL_AVAILABLE = _InputBase.__module__.startswith("textual.")
if TYPE_CHECKING: # pragma: no cover - typing only
_ComposeResult = Iterable[Any]
else:
_ComposeResult = Iterable[Any]
@dataclass(slots=True, frozen=True)
@@ -31,10 +115,129 @@ class PromptSubmitted:
text: str
class PromptInput(_InputBase):
"""TextArea widget wrapper with helper methods."""
_PROMPT_NORMAL = chr(0x276F)
_PROMPT_COMMAND = "/"
_PROMPT_SHELL = "$"
_PROMPT_MULTILINE = chr(0x2630)
_PROMPT_SYMBOLS: dict[InputMode, str] = {
InputMode.NORMAL: _PROMPT_NORMAL,
InputMode.COMMAND: _PROMPT_COMMAND,
InputMode.SHELL: _PROMPT_SHELL,
InputMode.MULTILINE: _PROMPT_MULTILINE,
}
class _PromptSymbolMixin:
"""Mixin providing mode-aware prompt symbol updates.
Subclasses must define a ``value`` property (str) and implement
``_apply_symbol(symbol: str) -> None``.
.. note::
The spec (§29257) refers to "PromptTextArea" implying a
multi-line ``TextArea`` widget. However, ``TextArea.__init__()``
dropped the ``placeholder`` keyword in textual >=1.0, so this
implementation uses ``textual.widgets.Input`` (single-line)
instead. The ``MULTILINE`` mode is detected by content
(``\\n`` or triple-backtick) and the symbol updates correctly,
but the underlying ``Input`` widget cannot display multiple
lines. A future migration to ``TextArea`` (once placeholder
support is restored or replaced) would enable true multi-line
editing per spec §30209-30218.
"""
_current_symbol: str
def _apply_symbol(self, symbol: str) -> None:
raise NotImplementedError
def _update_symbol(self, raw_text: str) -> None:
symbol = _PROMPT_SYMBOLS[InputModeRouter.detect_mode(raw_text)]
self._current_symbol = symbol
self._apply_symbol(symbol)
@property
def prompt_symbol(self) -> str:
return self._current_symbol
def consume_text(self) -> PromptSubmitted:
text = self.text
self.text = ""
text = self.value
self.value = ""
return PromptSubmitted(text=text)
class _TextualPromptInput(_PromptSymbolMixin, _HorizontalBase):
"""Composite widget that displays a mode-aware prompt symbol."""
def __init__(
self,
placeholder: str = "",
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
horizontal = cast(Any, super())
horizontal.__init__(name=name, id=id, classes=classes)
self._symbol_widget = _StaticBase("", classes="prompt-symbol")
input_id = f"{id}--input" if id else None
self._input = cast(
_MutableValueInput, _InputBase(placeholder=placeholder, id=input_id)
)
self._current_symbol = _PROMPT_SYMBOLS[InputMode.NORMAL]
self._update_symbol(self._input.value)
def compose(self) -> _ComposeResult:
yield self._symbol_widget
yield self._input
@property
def value(self) -> str:
return self._input.value
@value.setter
def value(self, new_value: str) -> None:
self._input.value = new_value
self._update_symbol(new_value)
def focus(self) -> None: # pragma: no cover - delegating to inner input
self._input.focus()
def on_input_changed(self, event: _InputChangedEvent) -> None:
if event.input is self._input:
self._update_symbol(event.value)
def _apply_symbol(self, symbol: str) -> None:
self._symbol_widget.update(symbol)
class _FallbackPromptInput(_PromptSymbolMixin):
"""Fallback prompt input used when Textual is unavailable."""
def __init__(self, placeholder: str = "", **_: object) -> None:
self.placeholder = placeholder
self._input = cast(_MutableValueInput, _InputBase())
self._current_symbol = _PROMPT_SYMBOLS[InputMode.NORMAL]
self._update_symbol(self._input.value)
@property
def value(self) -> str:
return getattr(self._input, "value", "")
@value.setter
def value(self, new_value: str) -> None:
self._input.value = new_value
self._update_symbol(new_value)
def focus(self) -> None: # pragma: no cover - API parity
focus = getattr(self._input, "focus", None)
if callable(focus):
focus()
def _apply_symbol(self, symbol: str) -> None:
self._current_symbol = symbol
PromptInput = _TextualPromptInput if _TEXTUAL_AVAILABLE else _FallbackPromptInput