diff --git a/src/agent_chat_cli/app.py b/src/agent_chat_cli/app.py index bcd4953..7ad045b 100644 --- a/src/agent_chat_cli/app.py +++ b/src/agent_chat_cli/app.py @@ -32,13 +32,9 @@ class AgentChatCLIApp(App): def __init__(self) -> None: super().__init__() - self.message_bus = MessageBus(self) - - self.agent_loop = AgentLoop( - on_message=self.message_bus.handle_agent_message, - ) - - self.actions = Actions(self) + self.message_bus = MessageBus(app=self) + self.actions = Actions(app=self) + self.agent_loop = AgentLoop(app=self) self.pending_tool_permission: dict | None = None def compose(self) -> ComposeResult: diff --git a/src/agent_chat_cli/components/user_input.py b/src/agent_chat_cli/components/user_input.py index 685c289..bf14bd6 100644 --- a/src/agent_chat_cli/components/user_input.py +++ b/src/agent_chat_cli/components/user_input.py @@ -1,14 +1,9 @@ -import asyncio - from textual.widget import Widget from textual.app import ComposeResult from textual.widgets import Input from agent_chat_cli.components.caret import Caret from agent_chat_cli.components.flex import Flex -from agent_chat_cli.components.chat_history import MessagePosted -from agent_chat_cli.components.thinking_indicator import ThinkingIndicator -from agent_chat_cli.components.messages import Message from agent_chat_cli.system.actions import Actions from agent_chat_cli.utils.enums import ControlCommand @@ -49,22 +44,9 @@ async def on_input_submitted(self, event: Input.Submitted) -> None: await self.actions.new() return - # Post to chat history - self.post_message(MessagePosted(Message.user(user_message))) - - # Run agent query in background - asyncio.create_task(self.query_agent(user_message)) + await self.actions.submit_user_message(user_message) async def on_input_blurred(self, event: Input.Blurred) -> None: if self.display: input_widget = self.query_one(Input) input_widget.focus() - - async def query_agent(self, user_input: str) -> None: - thinking_indicator = self.app.query_one(ThinkingIndicator) - thinking_indicator.is_thinking = True - - input_widget = self.query_one(Input) - input_widget.cursor_blink = False - - await self.actions.query(user_input) diff --git a/src/agent_chat_cli/system/actions.py b/src/agent_chat_cli/system/actions.py index f1a839b..1d71c84 100644 --- a/src/agent_chat_cli/system/actions.py +++ b/src/agent_chat_cli/system/actions.py @@ -1,37 +1,61 @@ +from typing import TYPE_CHECKING + from textual.widgets import Input -from agent_chat_cli.system.agent_loop import AgentLoop from agent_chat_cli.utils.enums import ControlCommand -from agent_chat_cli.components.chat_history import ChatHistory +from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted +from agent_chat_cli.components.messages import Message from agent_chat_cli.components.thinking_indicator import ThinkingIndicator from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt from agent_chat_cli.utils.logger import log_json +if TYPE_CHECKING: + from agent_chat_cli.app import AgentChatCLIApp + class Actions: - def __init__(self, app) -> None: + def __init__(self, app: "AgentChatCLIApp") -> None: self.app = app - self.agent_loop: AgentLoop = app.agent_loop def quit(self) -> None: self.app.exit() async def query(self, user_input: str) -> None: - await self.agent_loop.query_queue.put(user_input) + await self.app.agent_loop.query_queue.put(user_input) + + async def submit_user_message(self, message: str) -> None: + from agent_chat_cli.components.user_input import UserInput + + self.app.post_message(MessagePosted(Message.user(message))) + + thinking_indicator = self.app.query_one(ThinkingIndicator) + thinking_indicator.is_thinking = True + + user_input = self.app.query_one(UserInput) + input_widget = user_input.query_one(Input) + input_widget.cursor_blink = False + + await self.query(message) + + def post_system_message(self, message: str) -> None: + self.app.post_message(MessagePosted(Message.system(message))) + + async def handle_agent_message(self, message) -> None: + await self.app.message_bus.handle_agent_message(message) async def interrupt(self) -> None: permission_prompt = self.app.query_one(ToolPermissionPrompt) if permission_prompt.is_visible: return - self.agent_loop.interrupting = True - await self.agent_loop.client.interrupt() + self.app.agent_loop.interrupting = True + await self.app.agent_loop.client.interrupt() thinking_indicator = self.app.query_one(ThinkingIndicator) thinking_indicator.is_thinking = False async def new(self) -> None: - await self.agent_loop.query_queue.put(ControlCommand.NEW_CONVERSATION) + await self.app.agent_loop.query_queue.put(ControlCommand.NEW_CONVERSATION) chat_history = self.app.query_one(ChatHistory) await chat_history.remove_children() @@ -49,26 +73,25 @@ async def respond_to_tool_permission(self, response: str) -> None: } ) - await self.agent_loop.permission_response_queue.put(response) + await self.app.agent_loop.permission_response_queue.put(response) permission_prompt = self.app.query_one(ToolPermissionPrompt) permission_prompt.is_visible = False user_input = self.app.query_one(UserInput) user_input.display = True + input_widget = user_input.query_one(Input) input_widget.focus() + thinking_indicator = self.app.query_one(ThinkingIndicator) + thinking_indicator.is_thinking = True + input_widget.cursor_blink = False + # Check if it's a deny or custom response (anything except yes/allow) normalized = response.lower().strip() if normalized not in ["y", "yes", "allow", ""]: - # Handle like a normal user query - thinking_indicator = self.app.query_one(ThinkingIndicator) - thinking_indicator.is_thinking = True - input_widget.cursor_blink = False - if normalized in ["n", "no", "deny"]: - denial_message = "The user has denied the tool" - await self.query(denial_message) + await self.query("The user has denied the tool") else: - await self.query(response) + await self.submit_user_message(response) diff --git a/src/agent_chat_cli/system/agent_loop.py b/src/agent_chat_cli/system/agent_loop.py index d3fe4a1..81e7e6c 100644 --- a/src/agent_chat_cli/system/agent_loop.py +++ b/src/agent_chat_cli/system/agent_loop.py @@ -1,5 +1,5 @@ import asyncio -from typing import Callable, Awaitable, Any +from typing import Any, TYPE_CHECKING from dataclasses import dataclass from claude_agent_sdk import ( @@ -26,6 +26,9 @@ from agent_chat_cli.system.mcp_inference import infer_mcp_servers from agent_chat_cli.utils.logger import log_json +if TYPE_CHECKING: + from agent_chat_cli.app import AgentChatCLIApp + @dataclass class AgentMessage: @@ -36,9 +39,10 @@ class AgentMessage: class AgentLoop: def __init__( self, - on_message: Callable[[AgentMessage], Awaitable[None]], + app: "AgentChatCLIApp", session_id: str | None = None, ) -> None: + self.app = app self.config = load_config() self.session_id = session_id self.available_servers = get_available_servers() @@ -46,7 +50,6 @@ def __init__( self.client: ClaudeSDKClient - self.on_message = on_message self.query_queue: asyncio.Queue[str | ControlCommand] = asyncio.Queue() self.permission_response_queue: asyncio.Queue[str] = asyncio.Queue() self.permission_lock = asyncio.Lock() @@ -104,11 +107,8 @@ async def start(self) -> None: if inference_result["new_servers"]: server_list = ", ".join(inference_result["new_servers"]) - await self.on_message( - AgentMessage( - type=AgentMessageType.SYSTEM, - data=f"Connecting to {server_list}...", - ) + self.app.actions.post_system_message( + f"Connecting to {server_list}..." ) await asyncio.sleep(0.1) @@ -136,7 +136,9 @@ async def start(self) -> None: await self._handle_message(message) - await self.on_message(AgentMessage(type=AgentMessageType.RESULT, data=None)) + await self.app.actions.handle_agent_message( + AgentMessage(type=AgentMessageType.RESULT, data=None) + ) async def _initialize_client(self, mcp_servers: dict) -> None: sdk_config = get_sdk_config(self.config) @@ -174,7 +176,7 @@ async def _handle_message(self, message: Any) -> None: text_chunk = delta.get("text", "") if text_chunk: - await self.on_message( + await self.app.actions.handle_agent_message( AgentMessage( type=AgentMessageType.STREAM_EVENT, data={"text": text_chunk}, @@ -202,7 +204,7 @@ async def _handle_message(self, message: Any) -> None: ) # Finally, post the agent assistant response - await self.on_message( + await self.app.actions.handle_agent_message( AgentMessage( type=AgentMessageType.ASSISTANT, data={"content": content}, @@ -219,7 +221,7 @@ async def _can_use_tool( # Handle permission request queue async with self.permission_lock: - await self.on_message( + await self.app.actions.handle_agent_message( AgentMessage( type=AgentMessageType.TOOL_PERMISSION_REQUEST, data={ @@ -252,11 +254,8 @@ async def _can_use_tool( ) if DENY: - await self.on_message( - AgentMessage( - type=AgentMessageType.SYSTEM, - data=f"Permission denied for {tool_name}", - ) + self.app.actions.post_system_message( + f"Permission denied for {tool_name}" ) return PermissionResultDeny( @@ -266,14 +265,7 @@ async def _can_use_tool( ) # If a user instead typed in a message (instead of confirming or denying) - # post it to chat. actions.respond_to_tool_permission will handle querying. - await self.on_message( - AgentMessage( - type=AgentMessageType.USER, - data=user_response, - ) - ) - + # actions.respond_to_tool_permission will handle posting and querying. return PermissionResultDeny( behavior="deny", message=user_response,