Skip to content

Commit 24354b0

Browse files
committed
chore: more organization
1 parent a338dbf commit 24354b0

File tree

9 files changed

+96
-100
lines changed

9 files changed

+96
-100
lines changed

docs/architecture.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ src/agent_chat_cli/
1010
├── core/
1111
│ ├── actions.py # User action handlers
1212
│ ├── agent_loop.py # Claude Agent SDK client wrapper
13-
│ ├── message_bus.py # Message routing from agent to UI
13+
│ ├── renderer.py # Message routing from agent to UI
1414
│ ├── ui_state.py # Centralized UI state management
1515
│ └── styles.tcss # Textual CSS styles
1616
├── components/
@@ -43,7 +43,7 @@ The application follows a loosely coupled architecture with four main orchestrat
4343
┌─────────────────────────────────────────────────────────────┐
4444
│ AgentChatCLIApp │
4545
│ ┌───────────┐ ┌───────────┐ ┌─────────┐ ┌───────────┐ │
46-
│ │ UIState │ │MessageBus │ │ Actions │ │ AgentLoop │ │
46+
│ │ UIState │ │ Renderer │ │ Actions │ │ AgentLoop │ │
4747
│ └─────┬─────┘ └─────┬─────┘ └────┬────┘ └─────┬─────┘ │
4848
│ │ │ │ │ │
4949
│ └──────────────┴─────────────┴──────────────┘ │
@@ -63,9 +63,9 @@ Centralized management of UI state behaviors. Handles:
6363
- Tool permission prompt display/hide
6464
- Interrupt state tracking
6565

66-
This class was introduced in PR #9 to consolidate scattered UI state logic from Actions and MessageBus into a single cohesive module.
66+
This class was introduced in PR #9 to consolidate scattered UI state logic from Actions and Renderer into a single cohesive module.
6767

68-
**MessageBus** (`core/message_bus.py`)
68+
**Renderer** (`core/renderer.py`)
6969
Routes messages from the AgentLoop to appropriate UI components:
7070
- `STREAM_EVENT`: Streaming text chunks to AgentMessage widgets
7171
- `ASSISTANT`: Complete assistant responses with tool use blocks
@@ -92,7 +92,7 @@ Manages the Claude Agent SDK client lifecycle:
9292
1. User types in `UserInput` and presses Enter
9393
2. `Actions.submit_user_message()` posts to UI and enqueues to `AgentLoop.query_queue`
9494
3. `AgentLoop` sends query to Claude Agent SDK and streams responses
95-
4. Responses flow through `MessageBus.handle_agent_message()` to update UI
95+
4. Responses flow through `Actions.render_message()` to update UI
9696
5. Tool use triggers permission prompt via `UIState.show_permission_prompt()`
9797
6. User response flows back through `Actions.respond_to_tool_permission()`
9898

src/agent_chat_cli/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
1111
from agent_chat_cli.components.user_input import UserInput
1212
from agent_chat_cli.core.agent_loop import AgentLoop
13-
from agent_chat_cli.core.message_bus import MessageBus
13+
from agent_chat_cli.core.renderer import Renderer
1414
from agent_chat_cli.core.actions import Actions
1515
from agent_chat_cli.core.ui_state import UIState
1616
from agent_chat_cli.utils.logger import setup_logging
@@ -34,7 +34,7 @@ def __init__(self) -> None:
3434
super().__init__()
3535

3636
self.ui_state = UIState(app=self)
37-
self.message_bus = MessageBus(app=self)
37+
self.renderer = Renderer(app=self)
3838
self.actions = Actions(app=self)
3939
self.agent_loop = AgentLoop(app=self)
4040

src/agent_chat_cli/core/actions.py

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

4-
from textual.containers import VerticalScroll
5-
63
from agent_chat_cli.utils.enums import ControlCommand
74
from agent_chat_cli.components.chat_history import ChatHistory
85
from agent_chat_cli.components.messages import Message, MessageType
@@ -33,23 +30,19 @@ async def add_message_to_chat(self, type: MessageType, content: str) -> None:
3330

3431
chat_history = self.app.query_one(ChatHistory)
3532
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)
4233

4334
async def submit_user_message(self, message: str) -> None:
44-
await self.add_message_to_chat(MessageType.USER, message)
35+
chat_history = self.app.query_one(ChatHistory)
36+
chat_history.add_message(Message.user(message))
4537
self.app.ui_state.start_thinking()
38+
await self.app.ui_state.scroll_to_bottom()
4639
await self._query(message)
4740

4841
async def post_system_message(self, message: str) -> None:
4942
await self.add_message_to_chat(MessageType.SYSTEM, message)
5043

51-
async def handle_agent_message(self, message) -> None:
52-
await self.app.message_bus.handle_agent_message(message)
44+
async def render_message(self, message) -> None:
45+
await self.app.renderer.render_message(message)
5346

5447
async def interrupt(self) -> None:
5548
permission_prompt = self.app.query_one(ToolPermissionPrompt)

src/agent_chat_cli/core/agent_loop.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def start(self) -> None:
9292

9393
await self._handle_message(message)
9494

95-
await self.app.actions.handle_agent_message(
95+
await self.app.actions.render_message(
9696
AgentMessage(type=AgentMessageType.RESULT, data=None)
9797
)
9898

@@ -135,7 +135,7 @@ async def _handle_message(self, message: Message) -> None:
135135
text_chunk = delta.get("text", "")
136136

137137
if text_chunk:
138-
await self.app.actions.handle_agent_message(
138+
await self.app.actions.render_message(
139139
AgentMessage(
140140
type=AgentMessageType.STREAM_EVENT,
141141
data={"text": text_chunk},
@@ -163,7 +163,7 @@ async def _handle_message(self, message: Message) -> None:
163163
)
164164

165165
# Finally, post the agent assistant response
166-
await self.app.actions.handle_agent_message(
166+
await self.app.actions.render_message(
167167
AgentMessage(
168168
type=AgentMessageType.ASSISTANT,
169169
data={"content": content},
@@ -180,7 +180,7 @@ async def _can_use_tool(
180180

181181
# Handle permission request queue sequentially
182182
async with self.permission_lock:
183-
await self.app.actions.handle_agent_message(
183+
await self.app.actions.render_message(
184184
AgentMessage(
185185
type=AgentMessageType.TOOL_PERMISSION_REQUEST,
186186
data={
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import asyncio
1+
from dataclasses import dataclass
22
from typing import TYPE_CHECKING
33

44
from textual.widgets import Markdown
5-
from textual.containers import VerticalScroll
65

76
from agent_chat_cli.components.chat_history import ChatHistory
87
from agent_chat_cli.components.messages import (
@@ -18,68 +17,76 @@
1817
from agent_chat_cli.app import AgentChatCLIApp
1918

2019

21-
class MessageBus:
20+
@dataclass
21+
class StreamBuffer:
22+
widget: AgentMessageWidget | None = None
23+
text: str = ""
24+
25+
def reset(self) -> None:
26+
self.widget = None
27+
self.text = ""
28+
29+
30+
class Renderer:
2231
def __init__(self, app: "AgentChatCLIApp") -> None:
2332
self.app = app
24-
self.current_agent_message: AgentMessageWidget | None = None
25-
self.current_response_text = ""
33+
self._stream = StreamBuffer()
2634

27-
async def handle_agent_message(self, message: AgentMessage) -> None:
35+
async def render_message(self, message: AgentMessage) -> None:
2836
match message.type:
2937
case AgentMessageType.STREAM_EVENT:
30-
await self._handle_stream_event(message)
38+
await self._render_stream_event(message)
3139

3240
case AgentMessageType.ASSISTANT:
33-
await self._handle_assistant(message)
41+
await self._render_assistant_message(message)
3442

3543
case AgentMessageType.SYSTEM:
36-
await self._handle_system(message)
44+
await self._render_system_message(message)
3745

3846
case AgentMessageType.USER:
39-
await self._handle_user(message)
47+
await self._render_user_message(message)
4048

4149
case AgentMessageType.TOOL_PERMISSION_REQUEST:
42-
await self._handle_tool_permission_request(message)
50+
await self._render_tool_permission_request(message)
4351

4452
case AgentMessageType.RESULT:
45-
await self._handle_result()
53+
await self._on_complete()
4654

47-
async def _handle_stream_event(self, message: AgentMessage) -> None:
55+
if message.type is not AgentMessageType.RESULT:
56+
await self.app.ui_state.scroll_to_bottom()
57+
58+
async def _render_stream_event(self, message: AgentMessage) -> None:
4859
text_chunk = message.data.get("text", "")
4960

5061
if not text_chunk:
5162
return
5263

5364
chat_history = self.app.query_one(ChatHistory)
5465

55-
if self.current_agent_message is None:
56-
self.current_response_text = text_chunk
66+
if self._stream.widget is None:
67+
self._stream.text = text_chunk
5768

5869
agent_msg = AgentMessageWidget()
5970
agent_msg.message = text_chunk
6071

61-
# Append to chat history
62-
chat_history.mount(agent_msg)
63-
self.current_agent_message = agent_msg
72+
await chat_history.mount(agent_msg)
73+
self._stream.widget = agent_msg
6474
else:
65-
self.current_response_text += text_chunk
66-
67-
markdown = self.current_agent_message.query_one(Markdown)
68-
markdown.update(self.current_response_text)
75+
self._stream.text += text_chunk
6976

70-
await self._scroll_to_bottom()
77+
markdown = self._stream.widget.query_one(Markdown)
78+
markdown.update(self._stream.text)
7179

72-
async def _handle_assistant(self, message: AgentMessage) -> None:
80+
async def _render_assistant_message(self, message: AgentMessage) -> None:
7381
content_blocks = message.data.get("content", [])
7482
chat_history = self.app.query_one(ChatHistory)
7583

7684
for block in content_blocks:
7785
block_type = block.get("type")
7886

7987
if block_type == ContentType.TOOL_USE.value:
80-
if self.current_agent_message is not None:
81-
self.current_agent_message = None
82-
self.current_response_text = ""
88+
if self._stream.widget is not None:
89+
self._stream.reset()
8390

8491
tool_name = block.get("name", "unknown")
8592
tool_input = block.get("input", {})
@@ -88,24 +95,23 @@ async def _handle_assistant(self, message: AgentMessage) -> None:
8895
tool_msg.tool_name = tool_name
8996
tool_msg.tool_input = tool_input
9097

91-
# Append to chat history
92-
chat_history.mount(tool_msg)
93-
94-
await self._scroll_to_bottom()
98+
await chat_history.mount(tool_msg)
9599

96-
async def _handle_system(self, message: AgentMessage) -> None:
100+
async def _render_system_message(self, message: AgentMessage) -> None:
97101
system_content = (
98102
message.data if isinstance(message.data, str) else str(message.data)
99103
)
104+
100105
await self.app.actions.add_message_to_chat(MessageType.SYSTEM, system_content)
101106

102-
async def _handle_user(self, message: AgentMessage) -> None:
107+
async def _render_user_message(self, message: AgentMessage) -> None:
103108
user_content = (
104109
message.data if isinstance(message.data, str) else str(message.data)
105110
)
111+
106112
await self.app.actions.add_message_to_chat(MessageType.USER, user_content)
107113

108-
async def _handle_tool_permission_request(self, message: AgentMessage) -> None:
114+
async def _render_tool_permission_request(self, message: AgentMessage) -> None:
109115
log_json(
110116
{
111117
"event": "showing_permission_prompt",
@@ -118,19 +124,9 @@ async def _handle_tool_permission_request(self, message: AgentMessage) -> None:
118124
tool_input=message.data.get("tool_input", {}),
119125
)
120126

121-
await self._scroll_to_bottom()
122-
123-
async def _handle_result(self) -> None:
127+
async def _on_complete(self) -> None:
124128
if not self.app.agent_loop.query_queue.empty():
125129
return
126130

127131
self.app.ui_state.stop_thinking()
128-
129-
self.current_agent_message = None
130-
self.current_response_text = ""
131-
132-
async def _scroll_to_bottom(self) -> None:
133-
await asyncio.sleep(0.1)
134-
135-
container = self.app.query_one(VerticalScroll)
136-
container.scroll_end(animate=False, immediate=True)
132+
self._stream.reset()

src/agent_chat_cli/core/ui_state.py

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

4+
from textual.containers import VerticalScroll
35
from textual.widgets import TextArea
46

57
from agent_chat_cli.components.thinking_indicator import ThinkingIndicator
@@ -69,3 +71,8 @@ def clear_input(self) -> None:
6971
user_input = self.app.query_one(UserInput)
7072
input_widget = user_input.query_one(TextArea)
7173
input_widget.clear()
74+
75+
async def scroll_to_bottom(self) -> None:
76+
await asyncio.sleep(0.1)
77+
container = self.app.query_one(VerticalScroll)
78+
container.scroll_end(animate=False, immediate=True)

src/agent_chat_cli/docs/architecture.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Manages the conversation loop with Claude SDK:
3030
- Uses `permission_lock` (asyncio.Lock) to serialize parallel permission requests
3131
- Manages `permission_response_queue` for user responses to tool permission prompts
3232

33-
#### Message Bus (`system/message_bus.py`)
33+
#### Renderer (`core/renderer.py`)
3434
Routes agent messages to appropriate UI components:
3535
- Handles streaming text updates
3636
- Mounts tool use messages
@@ -82,7 +82,7 @@ Claude SDK (all enabled servers pre-connected at startup)
8282
8383
AgentLoop._handle_message
8484
85-
AgentMessage (typed message) → MessageBus.handle_agent_message
85+
AgentMessage (typed message) → Actions.render_message
8686
8787
Match on AgentMessageType:
8888
- STREAM_EVENT → Update streaming message widget
@@ -182,7 +182,7 @@ AgentLoop._can_use_tool (callback with permission_lock acquired)
182182
183183
Emit SYSTEM AgentMessage with tool_permission_request data
184184
185-
MessageBus._handle_system detects permission request
185+
Renderer._handle_tool_permission_request shows permission prompt
186186
187187
Show ToolPermissionPrompt, hide UserInput
188188
@@ -305,14 +305,14 @@ SDK reconnects to previous session with full history
305305
### Agent Response Flow
306306
1. AgentLoop receives SDK message
307307
2. Parse into AgentMessage with AgentMessageType
308-
3. MessageBus.handle_agent_message (match/case on type)
308+
3. Actions.render_message → Renderer (match/case on type)
309309
4. Update UI components based on type
310310
5. Scroll to bottom
311311

312312
## Notes
313313

314314
- Two distinct MessageType enums exist for different purposes (UI vs Agent events)
315-
- Message bus manages stateful streaming (tracks current_agent_message)
315+
- Renderer manages stateful streaming via StreamBuffer
316316
- Config loading combines multiple prompts into final system_prompt
317317
- Tool names follow format: `mcp__servername__toolname`
318318
- Actions class provides single interface for all user-initiated operations

0 commit comments

Comments
 (0)