Skip to content
30 changes: 20 additions & 10 deletions src/tau_coding/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,15 +590,15 @@ def action_clear_prompt(self) -> None:
self._clear_pending_paste()

def get_line(self, line_index: int) -> Text:
"""Retrieve one prompt line with shell prefixes highlighted."""
"""Retrieve one prompt line, coloring terminal commands like a running tool."""
line = super().get_line(line_index)
if line_index != 0 or not self.shell_mode_style:
if not self.shell_mode_style:
return line
span = _terminal_command_prefix_span(self.text)
if span is None:
return line
start, end = span
line.stylize(self.shell_mode_style, start, end)
start, _ = span
line.stylize(self.shell_mode_style, start if line_index == 0 else 0)
return line

async def action_submit_follow_up(self) -> None:
Expand Down Expand Up @@ -2978,7 +2978,7 @@ class TauTuiApp(App[None]):
}

#prompt.-shell-mode {
border-left: tall $tau-accent;
border-left: tall $tau-tool-running;
}

#compact-session-info {
Expand Down Expand Up @@ -3516,7 +3516,7 @@ def compose(self) -> ComposeResult:
async def on_mount(self) -> None:
"""Focus the prompt when the app starts."""
prompt = self.query_one(PromptInput)
prompt.shell_mode_style = self.tui_settings.resolved_theme.accent
prompt.shell_mode_style = self.tui_settings.resolved_theme.role_styles["tool"].border
self._sync_prompt_shell_mode(prompt.text)
prompt.focus()
self._update_responsive_layout(self.size.width, self.size.height)
Expand Down Expand Up @@ -5642,6 +5642,7 @@ def _apply_activity_indicator(self) -> None:
theme,
frame=self._activity_frame,
running=self.state.running,
shell_mode=shell_mode,
),
layout=False,
)
Expand Down Expand Up @@ -5750,7 +5751,7 @@ def _refresh_footer_bindings(self) -> None:

def _sync_prompt_shell_mode(self, text: str) -> None:
prompt = self.query_one("#prompt", PromptInput)
prompt.shell_mode_style = self.tui_settings.resolved_theme.accent
prompt.shell_mode_style = self.tui_settings.resolved_theme.role_styles["tool"].border
prompt.set_class(_is_terminal_command_prompt(text), "-shell-mode")
prompt.refresh()
self._apply_activity_indicator()
Expand All @@ -5766,12 +5767,20 @@ def _activity_prompt_border_color(
"""Return the prompt border color for the current activity animation frame."""
del frame, running
if shell_mode:
return theme.accent
return theme.role_styles["tool"].border
return theme.prompt_border


def _render_activity_indicator(theme: TuiTheme, *, frame: int, running: bool) -> Text:
"""Render the prompt prefix, turning Tau into a moving square while running."""
def _render_activity_indicator(
theme: TuiTheme,
*,
frame: int,
running: bool,
shell_mode: bool = False,
) -> Text:
"""Render the prompt prefix: a moving square while running, ``$`` in shell mode."""
if shell_mode and not running:
return Text("$", style=f"bold {theme.role_styles['tool'].border}")
if not running:
return Text("τ", style=f"bold {theme.accent}")

Expand Down Expand Up @@ -6231,6 +6240,7 @@ def _theme_css_variables(theme: TuiTheme) -> dict[str, str]:
"tau-prompt-border": theme.prompt_border,
"tau-autocomplete-background": theme.autocomplete_background,
"tau-accent": theme.accent,
"tau-tool-running": theme.role_styles["tool"].border,
"tau-highlight-background": theme.highlight_background,
"tau-highlight-text": theme.highlight_text,
"tau-markdown-highlight": theme.markdown_heading,
Expand Down
40 changes: 35 additions & 5 deletions tests/test_tui_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
TreePickerScreen,
_activity_prompt_border_color,
_completion_selected_render_line,
_render_activity_indicator,
_terminal_command_prefix_span,
_textual_theme_for_tau_theme,
_theme_css_variables,
Expand Down Expand Up @@ -2406,12 +2407,29 @@ def test_terminal_command_prefix_span_detects_shell_mode_prefix() -> None:
assert _terminal_command_prefix_span("hello ! pwd") is None


def test_activity_prompt_border_uses_theme_accent_color_in_shell_mode() -> None:
def test_activity_indicator_shows_dollar_sign_in_shell_mode() -> None:
theme = TAU_LIGHT_THEME

rendered = _render_activity_indicator(theme, frame=0, running=False, shell_mode=True)

assert rendered.plain == "$"
assert rendered.style == f"bold {theme.role_styles['tool'].border}"


def test_activity_indicator_keeps_running_animation_in_shell_mode() -> None:
theme = TAU_LIGHT_THEME

rendered = _render_activity_indicator(theme, frame=0, running=True, shell_mode=True)

assert rendered.plain != "$"


def test_activity_prompt_border_uses_tool_running_color_in_shell_mode() -> None:
theme = TAU_LIGHT_THEME

assert (
_activity_prompt_border_color(theme, frame=0, running=False, shell_mode=True)
== theme.accent
== theme.role_styles["tool"].border
)


Expand All @@ -2421,27 +2439,39 @@ async def test_tui_app_highlights_prompt_shell_mode() -> None:

async with app.run_test(size=(120, 30)) as pilot:
prompt = app.query_one("#prompt", PromptInput)
indicator = app.query_one("#prompt-prefix", Static)
prompt.value = "!! pwd"
await pilot.pause()

assert prompt.has_class("-shell-mode")
assert indicator.render().plain == "$"
tool_running_color = app.tui_settings.resolved_theme.role_styles["tool"].border
assert (
_activity_prompt_border_color(
app.tui_settings.resolved_theme,
frame=0,
running=False,
shell_mode=prompt.has_class("-shell-mode"),
)
== app.tui_settings.resolved_theme.accent
== tool_running_color
)
assert prompt.get_line(0).spans[-1].start == 0
assert prompt.get_line(0).spans[-1].end == 2
assert str(prompt.get_line(0).spans[-1].style) == app.tui_settings.resolved_theme.accent
assert prompt.get_line(0).spans[-1].end == len("!! pwd")
assert str(prompt.get_line(0).spans[-1].style) == tool_running_color

prompt.value = "! pwd\nls -la"
await pilot.pause()

assert prompt.get_line(1).spans[-1].start == 0
assert prompt.get_line(1).spans[-1].end == len("ls -la")
assert str(prompt.get_line(1).spans[-1].style) == tool_running_color

prompt.value = "ask tau"
await pilot.pause()

assert not prompt.has_class("-shell-mode")
assert prompt.get_line(0).spans == []
assert indicator.render().plain == "τ"


@pytest.mark.anyio
Expand Down
5 changes: 5 additions & 0 deletions website/content/guides/tui.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ You can run a shell command yourself without asking the model:
command and output in the conversation context.
- `!!<command>` runs it and shows the output **without** adding it to context.

As soon as the input starts with `!`, the whole input and its left border turn
the same amber/orange color as a tool while it is running, and the `τ` prompt
prefix becomes a matching `$`, so you can tell at a glance that submitting will
execute a shell command instead of messaging the model.

While typing a path after `!`/`!!`, press **Tab** to complete filenames from the
working directory.

Expand Down