diff --git a/src/tau_coding/tui/app.py b/src/tau_coding/tui/app.py index 07fc67023..9bbd254b4 100644 --- a/src/tau_coding/tui/app.py +++ b/src/tau_coding/tui/app.py @@ -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: @@ -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 { @@ -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) @@ -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, ) @@ -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() @@ -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}") @@ -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, diff --git a/tests/test_tui_app.py b/tests/test_tui_app.py index 1a7fc01e6..e297b2d75 100644 --- a/tests/test_tui_app.py +++ b/tests/test_tui_app.py @@ -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, @@ -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 ) @@ -2421,10 +2439,13 @@ 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, @@ -2432,16 +2453,25 @@ async def test_tui_app_highlights_prompt_shell_mode() -> None: 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 diff --git a/website/content/guides/tui.md b/website/content/guides/tui.md index 00c33a08b..19d9ebc87 100644 --- a/website/content/guides/tui.md +++ b/website/content/guides/tui.md @@ -72,6 +72,11 @@ You can run a shell command yourself without asking the model: command and output in the conversation context. - `!!` 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.