From 22da0e5212e79ab11f91508637dff99585496d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 27 Mar 2026 12:29:41 +0800 Subject: [PATCH 1/3] feat(shell): highlight user input with bright blue for better visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply bright blue color (#007AFF) to user input text in both the prompt buffer and echo output, matching the terminal icon color for unified visual identity. Changes: - echo.py: Use #007AFF for render_user_echo and render_user_echo_text - prompt.py: Add user-input style class with fg:#007AFF Signed-off-by: 刘冲 --- src/kimi_cli/ui/shell/echo.py | 4 ++-- src/kimi_cli/ui/shell/prompt.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/kimi_cli/ui/shell/echo.py b/src/kimi_cli/ui/shell/echo.py index 5e6bbdd7c..422611548 100644 --- a/src/kimi_cli/ui/shell/echo.py +++ b/src/kimi_cli/ui/shell/echo.py @@ -9,9 +9,9 @@ def render_user_echo(message: Message) -> Text: """Render a user message as literal shell transcript text.""" - return Text(f"{PROMPT_SYMBOL} {message_stringify(message)}") + return Text(f"{PROMPT_SYMBOL} {message_stringify(message)}", style="#007AFF") def render_user_echo_text(text: str) -> Text: """Render the local prompt text exactly as the user saw it in the buffer.""" - return Text(f"{PROMPT_SYMBOL} {text}") + return Text(f"{PROMPT_SYMBOL} {text}", style="#007AFF") diff --git a/src/kimi_cli/ui/shell/prompt.py b/src/kimi_cli/ui/shell/prompt.py index ede338e05..dda65f865 100644 --- a/src/kimi_cli/ui/shell/prompt.py +++ b/src/kimi_cli/ui/shell/prompt.py @@ -1500,6 +1500,7 @@ def _(event: KeyPressEvent) -> None: bottom_toolbar=self._render_bottom_toolbar, style=Style.from_dict( { + "user-input": "fg:#007AFF", # User input text - bright blue "bottom-toolbar": "noreverse", "running-prompt-placeholder": "fg:#7c8594 italic", "running-prompt-separator": "fg:#4a5568", @@ -1589,6 +1590,9 @@ def _install_prompt_buffer_visibility(self) -> None: buffer_container.filter = buffer_container.filter & Condition( self._should_render_input_buffer ) + # Apply style class to input buffer + if hasattr(buffer_container, 'content') and isinstance(buffer_container.content, Window): + buffer_container.content.style = "class:user-input" self._prompt_buffer_container = buffer_container def _should_show_slash_completion_menu(self) -> bool: From 38eb0182cf83c5a435bfd104af7124b4570d0052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 27 Mar 2026 13:04:47 +0800 Subject: [PATCH 2/3] feat(shell): highlight user input line with bright blue and separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render user input in bright blue (#007AFF) followed by a full-width separator line to clearly distinguish user messages in the conversation. Changes: - echo.py: Add bright blue styling and separator line to user echo - Update return type from Text to Group - Add helper function _separator_line() for full-width dashed line Signed-off-by: 刘冲 --- src/kimi_cli/ui/shell/echo.py | 22 +++++++++++--- tests/ui_and_conv/test_replay.py | 12 ++++++-- tests/ui_and_conv/test_shell_prompt_echo.py | 30 ++++++++++++------- .../test_shell_run_placeholders.py | 10 ++++++- .../test_visualize_running_prompt.py | 14 +++++++-- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/kimi_cli/ui/shell/echo.py b/src/kimi_cli/ui/shell/echo.py index 422611548..6b787bb26 100644 --- a/src/kimi_cli/ui/shell/echo.py +++ b/src/kimi_cli/ui/shell/echo.py @@ -1,17 +1,31 @@ from __future__ import annotations +import shutil + from kosong.message import Message +from rich.console import Group from rich.text import Text from kimi_cli.ui.shell.prompt import PROMPT_SYMBOL from kimi_cli.utils.message import message_stringify -def render_user_echo(message: Message) -> Text: +def _separator_line() -> Text: + """Return a dashed separator line that spans the full terminal width.""" + try: + width = shutil.get_terminal_size().columns + except OSError: + width = 80 + return Text("-" * width, style="grey50") + + +def render_user_echo(message: Message) -> Group: """Render a user message as literal shell transcript text.""" - return Text(f"{PROMPT_SYMBOL} {message_stringify(message)}", style="#007AFF") + user_line = Text(f"{PROMPT_SYMBOL} {message_stringify(message)}", style="#007AFF") + return Group(user_line, _separator_line()) -def render_user_echo_text(text: str) -> Text: +def render_user_echo_text(text: str) -> Group: """Render the local prompt text exactly as the user saw it in the buffer.""" - return Text(f"{PROMPT_SYMBOL} {text}", style="#007AFF") + user_line = Text(f"{PROMPT_SYMBOL} {text}", style="#007AFF") + return Group(user_line, _separator_line()) diff --git a/tests/ui_and_conv/test_replay.py b/tests/ui_and_conv/test_replay.py index 5eb4b970a..dabe1b85e 100644 --- a/tests/ui_and_conv/test_replay.py +++ b/tests/ui_and_conv/test_replay.py @@ -66,6 +66,14 @@ async def test_build_replay_turns_from_wire_keeps_steer_as_user_turn(tmp_path: P assert turns[1].n_steps == 2 +def _get_first_renderable_plain(text): + """Extract plain text from first renderable of a Group, or fallback to str.""" + from rich.console import Group + if isinstance(text, Group) and text.renderables: + return getattr(text.renderables[0], "plain", str(text.renderables[0])) + return getattr(text, "plain", str(text)) + + @pytest.mark.asyncio async def test_replay_recent_history_falls_back_to_history_when_wire_misses_steer( tmp_path: Path, @@ -86,7 +94,7 @@ async def test_replay_recent_history_falls_back_to_history_when_wire_misses_stee monkeypatch.setattr( replay_module.console, "print", - lambda text: printed.append(getattr(text, "plain", str(text))), + lambda text: printed.append(_get_first_renderable_plain(text)), ) async def fake_visualize(*_args, **_kwargs) -> None: @@ -156,7 +164,7 @@ async def test_replay_recent_history_falls_back_to_history_when_duplicate_text_s monkeypatch.setattr( replay_module.console, "print", - lambda text: printed.append(getattr(text, "plain", str(text))), + lambda text: printed.append(_get_first_renderable_plain(text)), ) async def fake_visualize(*_args, **_kwargs) -> None: diff --git a/tests/ui_and_conv/test_shell_prompt_echo.py b/tests/ui_and_conv/test_shell_prompt_echo.py index 10328fed5..64a53a528 100644 --- a/tests/ui_and_conv/test_shell_prompt_echo.py +++ b/tests/ui_and_conv/test_shell_prompt_echo.py @@ -1,10 +1,11 @@ from kosong.message import Message +from rich.console import Group from rich.text import Text import kimi_cli.ui.shell as shell_module from kimi_cli.ui.shell import Shell from kimi_cli.ui.shell.echo import render_user_echo -from kimi_cli.ui.shell.prompt import PromptMode, UserInput +from kimi_cli.ui.shell.prompt import PROMPT_SYMBOL, PromptMode, UserInput from kimi_cli.utils.slashcmd import SlashCommandCall from kimi_cli.wire.types import AudioURLPart, ImageURLPart, TextPart, VideoURLPart @@ -19,16 +20,18 @@ def _make_user_input(command: str, *, mode: PromptMode = PromptMode.AGENT) -> Us def test_echo_agent_input_prints_stringified_user_message(monkeypatch) -> None: - printed: list[Text] = [] + printed: list[Group] = [] monkeypatch.setattr(shell_module.console, "print", lambda text: printed.append(text)) Shell._echo_agent_input(_make_user_input("hi")) - assert [text.plain for text in printed] == ["✨ hi"] + assert len(printed) == 1 + # Group contains: user line + separator line + assert printed[0].renderables[0].plain == "✨ hi" def test_echo_agent_input_uses_display_command_for_placeholders(monkeypatch) -> None: - printed: list[Text] = [] + printed: list[Group] = [] monkeypatch.setattr(shell_module.console, "print", lambda text: printed.append(text)) user_input = UserInput( @@ -40,13 +43,16 @@ def test_echo_agent_input_uses_display_command_for_placeholders(monkeypatch) -> Shell._echo_agent_input(user_input) - assert [text.plain for text in printed] == ["✨ [Pasted text #1 +3 lines]"] + assert len(printed) == 1 + # Group contains: user line + separator line + assert printed[0].renderables[0].plain == "✨ [Pasted text #1 +3 lines]" def test_render_user_echo_preserves_literal_brackets() -> None: rendered = render_user_echo(Message(role="user", content=[TextPart(text="[brackets]")])) - assert rendered.plain == "✨ [brackets]" + # Group contains: user line + separator line + assert rendered.renderables[0].plain == "✨ [brackets]" def test_render_user_echo_preserves_image_placeholder_literal() -> None: @@ -57,7 +63,8 @@ def test_render_user_echo_preserves_image_placeholder_literal() -> None: ) ) - assert rendered.plain == "✨ [image]" + # Group contains: user line + separator line + assert rendered.renderables[0].plain == "✨ [image]" def test_render_user_echo_preserves_audio_placeholder_literal() -> None: @@ -72,7 +79,8 @@ def test_render_user_echo_preserves_audio_placeholder_literal() -> None: ) ) - assert rendered.plain == "✨ [audio:clip]" + # Group contains: user line + separator line + assert rendered.renderables[0].plain == "✨ [audio:clip]" def test_render_user_echo_preserves_video_placeholder_literal() -> None: @@ -85,7 +93,8 @@ def test_render_user_echo_preserves_video_placeholder_literal() -> None: ) ) - assert rendered.plain == "✨ [video]" + # Group contains: user line + separator line + assert rendered.renderables[0].plain == "✨ [video]" def test_render_user_echo_preserves_mixed_content_order() -> None: @@ -101,7 +110,8 @@ def test_render_user_echo_preserves_mixed_content_order() -> None: ) ) - assert rendered.plain == "✨ look [image][audio][video]" + # Group contains: user line + separator line + assert rendered.renderables[0].plain == "✨ look [image][audio][video]" def test_should_echo_agent_input_for_plain_agent_message() -> None: diff --git a/tests/ui_and_conv/test_shell_run_placeholders.py b/tests/ui_and_conv/test_shell_run_placeholders.py index cd3a529f2..49c88470b 100644 --- a/tests/ui_and_conv/test_shell_run_placeholders.py +++ b/tests/ui_and_conv/test_shell_run_placeholders.py @@ -67,6 +67,14 @@ def _make_fake_soul(): ) +def _get_first_renderable_plain(text): + """Extract plain text from first renderable of a Group, or fallback to str.""" + from rich.console import Group + if isinstance(text, Group) and text.renderables: + return getattr(text.renderables[0], "plain", str(text.renderables[0])) + return getattr(text, "plain", str(text)) + + @pytest.fixture def _patched_shell_run(monkeypatch): _FakePromptSession.instances = [] @@ -81,7 +89,7 @@ def _patched_shell_run(monkeypatch): monkeypatch.setattr( shell_module.console, "print", - lambda text="": printed.append(getattr(text, "plain", str(text))), + lambda text="": printed.append(_get_first_renderable_plain(text)), ) return printed diff --git a/tests/ui_and_conv/test_visualize_running_prompt.py b/tests/ui_and_conv/test_visualize_running_prompt.py index 2e96db1c6..f5f00fe3b 100644 --- a/tests/ui_and_conv/test_visualize_running_prompt.py +++ b/tests/ui_and_conv/test_visualize_running_prompt.py @@ -7,11 +7,19 @@ import pytest from prompt_toolkit.buffer import Buffer from prompt_toolkit.document import Document +from rich.console import Group from rich.text import Text from kimi_cli.ui.shell.prompt import PromptMode, UserInput from kimi_cli.wire.types import ApprovalRequest, StatusUpdate, SteerInput, TextPart + +def _get_first_renderable_plain(text): + """Extract plain text from first renderable of a Group, or fallback to str.""" + if isinstance(text, Group) and text.renderables: + return getattr(text.renderables[0], "plain", str(text.renderables[0])) + return getattr(text, "plain", str(text)) + shell_visualize = importlib.import_module("kimi_cli.ui.shell.visualize") _LiveView = shell_visualize._LiveView _PromptLiveView = shell_visualize._PromptLiveView @@ -157,7 +165,7 @@ def test_live_view_renders_steer_input_as_user_echo(monkeypatch) -> None: monkeypatch.setattr( shell_visualize.console, "print", - lambda text: printed.append(getattr(text, "plain", str(text))), + lambda text: printed.append(_get_first_renderable_plain(text)), ) view.dispatch_wire_message(SteerInput(user_input=[TextPart(text="A steer follow-up")])) @@ -175,7 +183,7 @@ def test_live_view_flushes_current_output_before_printing_steer_input(monkeypatc monkeypatch.setattr( shell_visualize.console, "print", - lambda text: order.append(("print", getattr(text, "plain", str(text)))), + lambda text: order.append(("print", _get_first_renderable_plain(text))), ) view.dispatch_wire_message(SteerInput(user_input=[TextPart(text="A steer follow-up")])) @@ -499,7 +507,7 @@ def test_handle_local_input_echoes_placeholder_display_text_but_steers_expanded_ monkeypatch.setattr( shell_visualize.console, "print", - lambda text: printed.append(getattr(text, "plain", str(text))), + lambda text: printed.append(_get_first_renderable_plain(text)), ) view.handle_local_input( From f679e960ad862366788ec653945c479ee3d6da21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Fri, 27 Mar 2026 13:21:31 +0800 Subject: [PATCH 3/3] chore: fix ruff format issues - Fix ruff format issues in prompt.py and test files --- src/kimi_cli/ui/shell/prompt.py | 2 +- tests/ui_and_conv/test_replay.py | 1 + tests/ui_and_conv/test_shell_prompt_echo.py | 3 +-- tests/ui_and_conv/test_shell_run_placeholders.py | 1 + tests/ui_and_conv/test_visualize_running_prompt.py | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/kimi_cli/ui/shell/prompt.py b/src/kimi_cli/ui/shell/prompt.py index dda65f865..090f6e028 100644 --- a/src/kimi_cli/ui/shell/prompt.py +++ b/src/kimi_cli/ui/shell/prompt.py @@ -1591,7 +1591,7 @@ def _install_prompt_buffer_visibility(self) -> None: self._should_render_input_buffer ) # Apply style class to input buffer - if hasattr(buffer_container, 'content') and isinstance(buffer_container.content, Window): + if hasattr(buffer_container, "content") and isinstance(buffer_container.content, Window): buffer_container.content.style = "class:user-input" self._prompt_buffer_container = buffer_container diff --git a/tests/ui_and_conv/test_replay.py b/tests/ui_and_conv/test_replay.py index dabe1b85e..c29972a6f 100644 --- a/tests/ui_and_conv/test_replay.py +++ b/tests/ui_and_conv/test_replay.py @@ -69,6 +69,7 @@ async def test_build_replay_turns_from_wire_keeps_steer_as_user_turn(tmp_path: P def _get_first_renderable_plain(text): """Extract plain text from first renderable of a Group, or fallback to str.""" from rich.console import Group + if isinstance(text, Group) and text.renderables: return getattr(text.renderables[0], "plain", str(text.renderables[0])) return getattr(text, "plain", str(text)) diff --git a/tests/ui_and_conv/test_shell_prompt_echo.py b/tests/ui_and_conv/test_shell_prompt_echo.py index 64a53a528..9e97f08af 100644 --- a/tests/ui_and_conv/test_shell_prompt_echo.py +++ b/tests/ui_and_conv/test_shell_prompt_echo.py @@ -1,11 +1,10 @@ from kosong.message import Message from rich.console import Group -from rich.text import Text import kimi_cli.ui.shell as shell_module from kimi_cli.ui.shell import Shell from kimi_cli.ui.shell.echo import render_user_echo -from kimi_cli.ui.shell.prompt import PROMPT_SYMBOL, PromptMode, UserInput +from kimi_cli.ui.shell.prompt import PromptMode, UserInput from kimi_cli.utils.slashcmd import SlashCommandCall from kimi_cli.wire.types import AudioURLPart, ImageURLPart, TextPart, VideoURLPart diff --git a/tests/ui_and_conv/test_shell_run_placeholders.py b/tests/ui_and_conv/test_shell_run_placeholders.py index 49c88470b..19b521be3 100644 --- a/tests/ui_and_conv/test_shell_run_placeholders.py +++ b/tests/ui_and_conv/test_shell_run_placeholders.py @@ -70,6 +70,7 @@ def _make_fake_soul(): def _get_first_renderable_plain(text): """Extract plain text from first renderable of a Group, or fallback to str.""" from rich.console import Group + if isinstance(text, Group) and text.renderables: return getattr(text.renderables[0], "plain", str(text.renderables[0])) return getattr(text, "plain", str(text)) diff --git a/tests/ui_and_conv/test_visualize_running_prompt.py b/tests/ui_and_conv/test_visualize_running_prompt.py index f5f00fe3b..86daedf8c 100644 --- a/tests/ui_and_conv/test_visualize_running_prompt.py +++ b/tests/ui_and_conv/test_visualize_running_prompt.py @@ -20,6 +20,7 @@ def _get_first_renderable_plain(text): return getattr(text.renderables[0], "plain", str(text.renderables[0])) return getattr(text, "plain", str(text)) + shell_visualize = importlib.import_module("kimi_cli.ui.shell.visualize") _LiveView = shell_visualize._LiveView _PromptLiveView = shell_visualize._PromptLiveView