Skip to content

Commit a338dbf

Browse files
committed
refactor: similify message pipeline
1 parent 270f393 commit a338dbf

File tree

9 files changed

+129
-41
lines changed

9 files changed

+129
-41
lines changed

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ Modal prompt for tool permission requests:
118118
- Manages focus to prevent input elsewhere while visible
119119

120120
**ChatHistory** (`components/chat_history.py`)
121-
Container for message widgets, handles `MessagePosted` events.
121+
Container for message widgets.
122122

123123
**ThinkingIndicator** (`components/thinking_indicator.py`)
124124
Animated indicator shown during agent processing.

src/agent_chat_cli/app.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from textual.binding import Binding
66

77
from agent_chat_cli.components.header import Header
8-
from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted
8+
from agent_chat_cli.components.chat_history import ChatHistory
99
from agent_chat_cli.components.thinking_indicator import ThinkingIndicator
1010
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
1111
from agent_chat_cli.components.user_input import UserInput
@@ -49,9 +49,6 @@ def compose(self) -> ComposeResult:
4949
async def on_mount(self) -> None:
5050
asyncio.create_task(self.agent_loop.start())
5151

52-
async def on_message_posted(self, event: MessagePosted) -> None:
53-
await self.message_bus.on_message_posted(event)
54-
5552
async def action_interrupt(self) -> None:
5653
await self.actions.interrupt()
5754

src/agent_chat_cli/components/chat_history.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
from textual.containers import Container
3-
from textual.message import Message as TextualMessage
43

54
from agent_chat_cli.components.messages import (
65
AgentMessage,
@@ -48,9 +47,3 @@ def _create_message_widget(
4847
tool_widget.tool_input = {"raw": message.content}
4948

5049
return tool_widget
51-
52-
53-
class MessagePosted(TextualMessage):
54-
def __init__(self, message: Message) -> None:
55-
self.message = message
56-
super().__init__()

src/agent_chat_cli/core/actions.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import asyncio
12
from typing import TYPE_CHECKING
23

4+
from textual.containers import VerticalScroll
5+
36
from agent_chat_cli.utils.enums import ControlCommand
4-
from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted
5-
from agent_chat_cli.components.messages import Message
7+
from agent_chat_cli.components.chat_history import ChatHistory
8+
from agent_chat_cli.components.messages import Message, MessageType
69
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
710
from agent_chat_cli.utils.logger import log_json
811

@@ -17,13 +20,33 @@ def __init__(self, app: "AgentChatCLIApp") -> None:
1720
def quit(self) -> None:
1821
self.app.exit()
1922

23+
async def add_message_to_chat(self, type: MessageType, content: str) -> None:
24+
match type:
25+
case MessageType.USER:
26+
message = Message.user(content)
27+
case MessageType.SYSTEM:
28+
message = Message.system(content)
29+
case MessageType.AGENT:
30+
message = Message.agent(content)
31+
case _:
32+
raise ValueError(f"Unsupported message type: {type}")
33+
34+
chat_history = self.app.query_one(ChatHistory)
35+
chat_history.add_message(message)
36+
await self._scroll_to_bottom()
37+
38+
async def _scroll_to_bottom(self) -> None:
39+
await asyncio.sleep(0.1)
40+
container = self.app.query_one(VerticalScroll)
41+
container.scroll_end(animate=False, immediate=True)
42+
2043
async def submit_user_message(self, message: str) -> None:
21-
self.app.post_message(MessagePosted(Message.user(message)))
44+
await self.add_message_to_chat(MessageType.USER, message)
2245
self.app.ui_state.start_thinking()
2346
await self._query(message)
2447

25-
def post_system_message(self, message: str) -> None:
26-
self.app.post_message(MessagePosted(Message.system(message)))
48+
async def post_system_message(self, message: str) -> None:
49+
await self.add_message_to_chat(MessageType.SYSTEM, message)
2750

2851
async def handle_agent_message(self, message) -> None:
2952
await self.app.message_bus.handle_agent_message(message)

src/agent_chat_cli/core/agent_loop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ async def _can_use_tool(
213213
)
214214

215215
if rejected_tool:
216-
self.app.actions.post_system_message(
216+
await self.app.actions.post_system_message(
217217
f"Permission denied for {tool_name}"
218218
)
219219

src/agent_chat_cli/core/message_bus.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from textual.widgets import Markdown
55
from textual.containers import VerticalScroll
66

7-
from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted
7+
from agent_chat_cli.components.chat_history import ChatHistory
88
from agent_chat_cli.components.messages import (
99
AgentMessage as AgentMessageWidget,
10-
Message,
10+
MessageType,
1111
ToolMessage,
1212
)
1313
from agent_chat_cli.core.agent_loop import AgentMessage
@@ -44,12 +44,6 @@ async def handle_agent_message(self, message: AgentMessage) -> None:
4444
case AgentMessageType.RESULT:
4545
await self._handle_result()
4646

47-
async def on_message_posted(self, event: MessagePosted) -> None:
48-
chat_history = self.app.query_one(ChatHistory)
49-
chat_history.add_message(event.message)
50-
51-
await self._scroll_to_bottom()
52-
5347
async def _handle_stream_event(self, message: AgentMessage) -> None:
5448
text_chunk = message.data.get("text", "")
5549

@@ -103,20 +97,13 @@ async def _handle_system(self, message: AgentMessage) -> None:
10397
system_content = (
10498
message.data if isinstance(message.data, str) else str(message.data)
10599
)
106-
107-
# Dispatch message
108-
self.app.post_message(MessagePosted(Message.system(system_content)))
109-
110-
await self._scroll_to_bottom()
100+
await self.app.actions.add_message_to_chat(MessageType.SYSTEM, system_content)
111101

112102
async def _handle_user(self, message: AgentMessage) -> None:
113103
user_content = (
114104
message.data if isinstance(message.data, str) else str(message.data)
115105
)
116-
117-
self.app.post_message(MessagePosted(Message.user(user_content)))
118-
119-
await self._scroll_to_bottom()
106+
await self.app.actions.add_message_to_chat(MessageType.USER, user_content)
120107

121108
async def _handle_tool_permission_request(self, message: AgentMessage) -> None:
122109
log_json(

src/agent_chat_cli/docs/architecture.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ User Input
7474
7575
UserInput.on_input_submitted
7676
77-
MessagePosted event → ChatHistory (immediate UI update)
77+
Actions.add_message_to_chat → ChatHistory (immediate UI update)
7878
7979
Actions.query(user_input) → AgentLoop.query_queue.put()
8080
@@ -299,10 +299,8 @@ SDK reconnects to previous session with full history
299299

300300
### User Message Flow
301301
1. User submits text → UserInput
302-
2. MessagePosted event → App
303-
3. App → MessageBus.on_message_posted
304-
4. MessageBus → ChatHistory.add_message
305-
5. MessageBus → Scroll to bottom
302+
2. Actions.add_message_to_chat → ChatHistory.add_message
303+
3. Scroll to bottom
306304

307305
### Agent Response Flow
308306
1. AgentLoop receives SDK message

tests/core/test_actions.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
from agent_chat_cli.app import AgentChatCLIApp
55
from agent_chat_cli.components.chat_history import ChatHistory
6+
from agent_chat_cli.components.messages import (
7+
MessageType,
8+
SystemMessage,
9+
UserMessage,
10+
AgentMessage,
11+
)
612
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
713
from agent_chat_cli.utils.enums import ControlCommand
814

@@ -30,6 +36,90 @@ def mock_config():
3036
yield mock
3137

3238

39+
class TestActionsAddMessageToChat:
40+
async def test_adds_user_message(self, mock_agent_loop, mock_config):
41+
app = AgentChatCLIApp()
42+
async with app.run_test():
43+
chat_history = app.query_one(ChatHistory)
44+
45+
await app.actions.add_message_to_chat(MessageType.USER, "Hello")
46+
47+
widgets = chat_history.query(UserMessage)
48+
assert len(widgets) == 1
49+
assert widgets.first().message == "Hello"
50+
51+
async def test_adds_system_message(self, mock_agent_loop, mock_config):
52+
app = AgentChatCLIApp()
53+
async with app.run_test():
54+
chat_history = app.query_one(ChatHistory)
55+
56+
await app.actions.add_message_to_chat(MessageType.SYSTEM, "System alert")
57+
58+
widgets = chat_history.query(SystemMessage)
59+
assert len(widgets) == 1
60+
assert widgets.first().message == "System alert"
61+
62+
async def test_adds_agent_message(self, mock_agent_loop, mock_config):
63+
app = AgentChatCLIApp()
64+
async with app.run_test():
65+
chat_history = app.query_one(ChatHistory)
66+
67+
await app.actions.add_message_to_chat(MessageType.AGENT, "I can help")
68+
69+
widgets = chat_history.query(AgentMessage)
70+
assert len(widgets) == 1
71+
assert widgets.first().message == "I can help"
72+
73+
async def test_raises_for_unsupported_type(self, mock_agent_loop, mock_config):
74+
app = AgentChatCLIApp()
75+
async with app.run_test():
76+
with pytest.raises(ValueError, match="Unsupported message type"):
77+
await app.actions.add_message_to_chat(MessageType.TOOL, "tool content")
78+
79+
80+
class TestActionsPostSystemMessage:
81+
async def test_adds_system_message_to_chat(self, mock_agent_loop, mock_config):
82+
app = AgentChatCLIApp()
83+
async with app.run_test():
84+
chat_history = app.query_one(ChatHistory)
85+
86+
await app.actions.post_system_message("Connection established")
87+
88+
widgets = chat_history.query(SystemMessage)
89+
assert len(widgets) == 1
90+
assert widgets.first().message == "Connection established"
91+
92+
93+
class TestActionsSubmitUserMessage:
94+
async def test_adds_user_message_to_chat(self, mock_agent_loop, mock_config):
95+
app = AgentChatCLIApp()
96+
async with app.run_test():
97+
chat_history = app.query_one(ChatHistory)
98+
99+
await app.actions.submit_user_message("Hello agent")
100+
101+
widgets = chat_history.query(UserMessage)
102+
assert len(widgets) == 1
103+
assert widgets.first().message == "Hello agent"
104+
105+
async def test_starts_thinking_indicator(self, mock_agent_loop, mock_config):
106+
from agent_chat_cli.components.thinking_indicator import ThinkingIndicator
107+
108+
app = AgentChatCLIApp()
109+
async with app.run_test():
110+
await app.actions.submit_user_message("Hello agent")
111+
112+
thinking_indicator = app.query_one(ThinkingIndicator)
113+
assert thinking_indicator.is_thinking is True
114+
115+
async def test_queues_message_to_agent_loop(self, mock_agent_loop, mock_config):
116+
app = AgentChatCLIApp()
117+
async with app.run_test():
118+
await app.actions.submit_user_message("Hello agent")
119+
120+
mock_agent_loop.query_queue.put.assert_called_with("Hello agent")
121+
122+
33123
class TestActionsInterrupt:
34124
async def test_sets_interrupting_flag(self, mock_agent_loop, mock_config):
35125
app = AgentChatCLIApp()

tests/core/test_agent_loop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def mock_app():
2121
app.ui_state = MagicMock()
2222
app.actions = MagicMock()
2323
app.actions.handle_agent_message = AsyncMock()
24-
app.actions.post_system_message = MagicMock()
24+
app.actions.post_system_message = AsyncMock()
2525
return app
2626

2727

0 commit comments

Comments
 (0)