Skip to content

Commit a7457f5

Browse files
committed
refactor: better structure around actions
1 parent 309d94a commit a7457f5

File tree

4 files changed

+61
-68
lines changed

4 files changed

+61
-68
lines changed

src/agent_chat_cli/app.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,9 @@ class AgentChatCLIApp(App):
3232
def __init__(self) -> None:
3333
super().__init__()
3434

35-
self.message_bus = MessageBus(self)
36-
37-
self.agent_loop = AgentLoop(
38-
on_message=self.message_bus.handle_agent_message,
39-
)
40-
41-
self.actions = Actions(self)
35+
self.message_bus = MessageBus(app=self)
36+
self.actions = Actions(app=self)
37+
self.agent_loop = AgentLoop(app=self)
4238
self.pending_tool_permission: dict | None = None
4339

4440
def compose(self) -> ComposeResult:

src/agent_chat_cli/components/user_input.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import asyncio
2-
31
from textual.widget import Widget
42
from textual.app import ComposeResult
53
from textual.widgets import Input
64

75
from agent_chat_cli.components.caret import Caret
86
from agent_chat_cli.components.flex import Flex
9-
from agent_chat_cli.components.chat_history import MessagePosted
10-
from agent_chat_cli.components.thinking_indicator import ThinkingIndicator
11-
from agent_chat_cli.components.messages import Message
127
from agent_chat_cli.system.actions import Actions
138
from agent_chat_cli.utils.enums import ControlCommand
149

@@ -49,22 +44,9 @@ async def on_input_submitted(self, event: Input.Submitted) -> None:
4944
await self.actions.new()
5045
return
5146

52-
# Post to chat history
53-
self.post_message(MessagePosted(Message.user(user_message)))
54-
55-
# Run agent query in background
56-
asyncio.create_task(self.query_agent(user_message))
47+
await self.actions.submit_user_message(user_message)
5748

5849
async def on_input_blurred(self, event: Input.Blurred) -> None:
5950
if self.display:
6051
input_widget = self.query_one(Input)
6152
input_widget.focus()
62-
63-
async def query_agent(self, user_input: str) -> None:
64-
thinking_indicator = self.app.query_one(ThinkingIndicator)
65-
thinking_indicator.is_thinking = True
66-
67-
input_widget = self.query_one(Input)
68-
input_widget.cursor_blink = False
69-
70-
await self.actions.query(user_input)
Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,61 @@
1+
from typing import TYPE_CHECKING
2+
13
from textual.widgets import Input
24

3-
from agent_chat_cli.system.agent_loop import AgentLoop
45
from agent_chat_cli.utils.enums import ControlCommand
5-
from agent_chat_cli.components.chat_history import ChatHistory
6+
from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted
7+
from agent_chat_cli.components.messages import Message
68
from agent_chat_cli.components.thinking_indicator import ThinkingIndicator
79
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
810
from agent_chat_cli.utils.logger import log_json
911

12+
if TYPE_CHECKING:
13+
from agent_chat_cli.app import AgentChatCLIApp
14+
1015

1116
class Actions:
12-
def __init__(self, app) -> None:
17+
def __init__(self, app: "AgentChatCLIApp") -> None:
1318
self.app = app
14-
self.agent_loop: AgentLoop = app.agent_loop
1519

1620
def quit(self) -> None:
1721
self.app.exit()
1822

1923
async def query(self, user_input: str) -> None:
20-
await self.agent_loop.query_queue.put(user_input)
24+
await self.app.agent_loop.query_queue.put(user_input)
25+
26+
async def submit_user_message(self, message: str) -> None:
27+
from agent_chat_cli.components.user_input import UserInput
28+
29+
self.app.post_message(MessagePosted(Message.user(message)))
30+
31+
thinking_indicator = self.app.query_one(ThinkingIndicator)
32+
thinking_indicator.is_thinking = True
33+
34+
user_input = self.app.query_one(UserInput)
35+
input_widget = user_input.query_one(Input)
36+
input_widget.cursor_blink = False
37+
38+
await self.query(message)
39+
40+
def post_system_message(self, message: str) -> None:
41+
self.app.post_message(MessagePosted(Message.system(message)))
42+
43+
async def handle_agent_message(self, message) -> None:
44+
await self.app.message_bus.handle_agent_message(message)
2145

2246
async def interrupt(self) -> None:
2347
permission_prompt = self.app.query_one(ToolPermissionPrompt)
2448
if permission_prompt.is_visible:
2549
return
2650

27-
self.agent_loop.interrupting = True
28-
await self.agent_loop.client.interrupt()
51+
self.app.agent_loop.interrupting = True
52+
await self.app.agent_loop.client.interrupt()
2953

3054
thinking_indicator = self.app.query_one(ThinkingIndicator)
3155
thinking_indicator.is_thinking = False
3256

3357
async def new(self) -> None:
34-
await self.agent_loop.query_queue.put(ControlCommand.NEW_CONVERSATION)
58+
await self.app.agent_loop.query_queue.put(ControlCommand.NEW_CONVERSATION)
3559

3660
chat_history = self.app.query_one(ChatHistory)
3761
await chat_history.remove_children()
@@ -49,26 +73,25 @@ async def respond_to_tool_permission(self, response: str) -> None:
4973
}
5074
)
5175

52-
await self.agent_loop.permission_response_queue.put(response)
76+
await self.app.agent_loop.permission_response_queue.put(response)
5377

5478
permission_prompt = self.app.query_one(ToolPermissionPrompt)
5579
permission_prompt.is_visible = False
5680

5781
user_input = self.app.query_one(UserInput)
5882
user_input.display = True
83+
5984
input_widget = user_input.query_one(Input)
6085
input_widget.focus()
6186

87+
thinking_indicator = self.app.query_one(ThinkingIndicator)
88+
thinking_indicator.is_thinking = True
89+
input_widget.cursor_blink = False
90+
6291
# Check if it's a deny or custom response (anything except yes/allow)
6392
normalized = response.lower().strip()
6493
if normalized not in ["y", "yes", "allow", ""]:
65-
# Handle like a normal user query
66-
thinking_indicator = self.app.query_one(ThinkingIndicator)
67-
thinking_indicator.is_thinking = True
68-
input_widget.cursor_blink = False
69-
7094
if normalized in ["n", "no", "deny"]:
71-
denial_message = "The user has denied the tool"
72-
await self.query(denial_message)
95+
await self.query("The user has denied the tool")
7396
else:
74-
await self.query(response)
97+
await self.submit_user_message(response)

src/agent_chat_cli/system/agent_loop.py

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio
2-
from typing import Callable, Awaitable, Any
2+
from typing import Any, TYPE_CHECKING
33
from dataclasses import dataclass
44

55
from claude_agent_sdk import (
@@ -26,6 +26,9 @@
2626
from agent_chat_cli.system.mcp_inference import infer_mcp_servers
2727
from agent_chat_cli.utils.logger import log_json
2828

29+
if TYPE_CHECKING:
30+
from agent_chat_cli.app import AgentChatCLIApp
31+
2932

3033
@dataclass
3134
class AgentMessage:
@@ -36,17 +39,17 @@ class AgentMessage:
3639
class AgentLoop:
3740
def __init__(
3841
self,
39-
on_message: Callable[[AgentMessage], Awaitable[None]],
42+
app: "AgentChatCLIApp",
4043
session_id: str | None = None,
4144
) -> None:
45+
self.app = app
4246
self.config = load_config()
4347
self.session_id = session_id
4448
self.available_servers = get_available_servers()
4549
self.inferred_servers: set[str] = set()
4650

4751
self.client: ClaudeSDKClient
4852

49-
self.on_message = on_message
5053
self.query_queue: asyncio.Queue[str | ControlCommand] = asyncio.Queue()
5154
self.permission_response_queue: asyncio.Queue[str] = asyncio.Queue()
5255
self.permission_lock = asyncio.Lock()
@@ -104,11 +107,8 @@ async def start(self) -> None:
104107
if inference_result["new_servers"]:
105108
server_list = ", ".join(inference_result["new_servers"])
106109

107-
await self.on_message(
108-
AgentMessage(
109-
type=AgentMessageType.SYSTEM,
110-
data=f"Connecting to {server_list}...",
111-
)
110+
self.app.actions.post_system_message(
111+
f"Connecting to {server_list}..."
112112
)
113113

114114
await asyncio.sleep(0.1)
@@ -136,7 +136,9 @@ async def start(self) -> None:
136136

137137
await self._handle_message(message)
138138

139-
await self.on_message(AgentMessage(type=AgentMessageType.RESULT, data=None))
139+
await self.app.actions.handle_agent_message(
140+
AgentMessage(type=AgentMessageType.RESULT, data=None)
141+
)
140142

141143
async def _initialize_client(self, mcp_servers: dict) -> None:
142144
sdk_config = get_sdk_config(self.config)
@@ -174,7 +176,7 @@ async def _handle_message(self, message: Any) -> None:
174176
text_chunk = delta.get("text", "")
175177

176178
if text_chunk:
177-
await self.on_message(
179+
await self.app.actions.handle_agent_message(
178180
AgentMessage(
179181
type=AgentMessageType.STREAM_EVENT,
180182
data={"text": text_chunk},
@@ -202,7 +204,7 @@ async def _handle_message(self, message: Any) -> None:
202204
)
203205

204206
# Finally, post the agent assistant response
205-
await self.on_message(
207+
await self.app.actions.handle_agent_message(
206208
AgentMessage(
207209
type=AgentMessageType.ASSISTANT,
208210
data={"content": content},
@@ -219,7 +221,7 @@ async def _can_use_tool(
219221

220222
# Handle permission request queue
221223
async with self.permission_lock:
222-
await self.on_message(
224+
await self.app.actions.handle_agent_message(
223225
AgentMessage(
224226
type=AgentMessageType.TOOL_PERMISSION_REQUEST,
225227
data={
@@ -252,11 +254,8 @@ async def _can_use_tool(
252254
)
253255

254256
if DENY:
255-
await self.on_message(
256-
AgentMessage(
257-
type=AgentMessageType.SYSTEM,
258-
data=f"Permission denied for {tool_name}",
259-
)
257+
self.app.actions.post_system_message(
258+
f"Permission denied for {tool_name}"
260259
)
261260

262261
return PermissionResultDeny(
@@ -266,14 +265,7 @@ async def _can_use_tool(
266265
)
267266

268267
# If a user instead typed in a message (instead of confirming or denying)
269-
# post it to chat. actions.respond_to_tool_permission will handle querying.
270-
await self.on_message(
271-
AgentMessage(
272-
type=AgentMessageType.USER,
273-
data=user_response,
274-
)
275-
)
276-
268+
# actions.respond_to_tool_permission will handle posting and querying.
277269
return PermissionResultDeny(
278270
behavior="deny",
279271
message=user_response,

0 commit comments

Comments
 (0)