diff --git a/src/kimi_cli/soul/kimisoul.py b/src/kimi_cli/soul/kimisoul.py index 6f27b4233..0d11a84a7 100644 --- a/src/kimi_cli/soul/kimisoul.py +++ b/src/kimi_cli/soul/kimisoul.py @@ -66,6 +66,7 @@ from kimi_cli.tools.dmail import NAME as SendDMail_NAME from kimi_cli.tools.utils import ToolRejectedError from kimi_cli.utils.logging import logger +from kimi_cli.utils.message import user_input_to_text from kimi_cli.utils.slashcmd import SlashCommand, parse_slash_command_call from kimi_cli.wire.file import WireFile from kimi_cli.wire.types import ( @@ -605,7 +606,10 @@ async def run( # they are not user input, and a user-configured prompt-blocking # hook would drop the notification and hang the wait loop. if not skip_user_prompt_hook: - text_input_for_hook = user_input if isinstance(user_input, str) else "" + # user_input may be structured content (e.g. from the shell UI), + # not a plain str; extract its text so prompt-matching hooks see + # the actual prompt instead of an empty string. + text_input_for_hook = user_input_to_text(user_input) hook_results = await self._hook_engine.trigger( "UserPromptSubmit", diff --git a/src/kimi_cli/utils/message.py b/src/kimi_cli/utils/message.py index 6ca2c4568..b4221828a 100644 --- a/src/kimi_cli/utils/message.py +++ b/src/kimi_cli/utils/message.py @@ -1,10 +1,23 @@ from __future__ import annotations -from kosong.message import Message +from kosong.message import ContentPart, Message from kimi_cli.wire.types import AudioURLPart, ImageURLPart, TextPart, VideoURLPart +def user_input_to_text(user_input: str | list[ContentPart]) -> str: + """Extract the plain text from user input. + + ``user_input`` may be a plain string or structured content (e.g. the parts + produced by the interactive shell UI). Return the concatenated text in both + cases so text-based consumers (such as prompt-matching hooks) work + regardless of the input shape. + """ + if isinstance(user_input, str): + return user_input + return Message(role="user", content=user_input).extract_text(" ") + + def message_stringify(message: Message) -> str: """Get a string representation of a message.""" # TODO: this should be merged into `kosong.message.Message.extract_text` diff --git a/tests/utils/test_message_utils.py b/tests/utils/test_message_utils.py index f19c7e643..836e11f74 100644 --- a/tests/utils/test_message_utils.py +++ b/tests/utils/test_message_utils.py @@ -4,10 +4,33 @@ from kosong.message import Message -from kimi_cli.utils.message import message_stringify +from kimi_cli.utils.message import message_stringify, user_input_to_text from kimi_cli.wire.types import ImageURLPart, TextPart +def test_user_input_to_text_from_string(): + """A plain string is returned unchanged.""" + assert user_input_to_text("hello world") == "hello world" + + +def test_user_input_to_text_from_content_parts(): + """Structured content (e.g. from the shell UI) yields its text, not ''.""" + user_input = [TextPart(text="hello"), TextPart(text="world")] + assert user_input_to_text(user_input) == "hello world" + + +def test_user_input_to_text_ignores_non_text_parts(): + """Non-text parts are skipped when extracting the text.""" + image_part = ImageURLPart(image_url=ImageURLPart.ImageURL(url="https://example.com/image.jpg")) + user_input = [TextPart(text="describe"), image_part, TextPart(text="this")] + assert user_input_to_text(user_input) == "describe this" + + +def test_user_input_to_text_from_empty_parts(): + """No text parts yields an empty string.""" + assert user_input_to_text([]) == "" + + def test_extract_text_from_string_content(): """Test extracting text from message with string content.""" message = Message(role="user", content="Simple text")