Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ src/agent_chat_cli/
├── core/
│ ├── actions.py # User action handlers
│ ├── agent_loop.py # Claude Agent SDK client wrapper
│ ├── message_bus.py # Message routing from agent to UI
│ ├── renderer.py # Message routing from agent to UI
│ ├── ui_state.py # Centralized UI state management
│ └── styles.tcss # Textual CSS styles
├── components/
Expand Down Expand Up @@ -43,7 +43,7 @@ The application follows a loosely coupled architecture with four main orchestrat
┌─────────────────────────────────────────────────────────────┐
│ AgentChatCLIApp │
│ ┌───────────┐ ┌───────────┐ ┌─────────┐ ┌───────────┐ │
│ │ UIState │ │MessageBus │ │ Actions │ │ AgentLoop │ │
│ │ UIState │ │ Renderer │ │ Actions │ │ AgentLoop │ │
│ └─────┬─────┘ └─────┬─────┘ └────┬────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ └──────────────┴─────────────┴──────────────┘ │
Expand All @@ -63,9 +63,9 @@ Centralized management of UI state behaviors. Handles:
- Tool permission prompt display/hide
- Interrupt state tracking

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

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

Expand All @@ -118,7 +118,7 @@ Modal prompt for tool permission requests:
- Manages focus to prevent input elsewhere while visible

**ChatHistory** (`components/chat_history.py`)
Container for message widgets, handles `MessagePosted` events.
Container for message widgets.

**ThinkingIndicator** (`components/thinking_indicator.py`)
Animated indicator shown during agent processing.
Expand Down
9 changes: 3 additions & 6 deletions src/agent_chat_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from textual.binding import Binding

from agent_chat_cli.components.header import Header
from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted
from agent_chat_cli.components.chat_history import ChatHistory
from agent_chat_cli.components.thinking_indicator import ThinkingIndicator
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
from agent_chat_cli.components.user_input import UserInput
from agent_chat_cli.core.agent_loop import AgentLoop
from agent_chat_cli.core.message_bus import MessageBus
from agent_chat_cli.core.renderer import Renderer
from agent_chat_cli.core.actions import Actions
from agent_chat_cli.core.ui_state import UIState
from agent_chat_cli.utils.logger import setup_logging
Expand All @@ -34,7 +34,7 @@ def __init__(self) -> None:
super().__init__()

self.ui_state = UIState(app=self)
self.message_bus = MessageBus(app=self)
self.renderer = Renderer(app=self)
self.actions = Actions(app=self)
self.agent_loop = AgentLoop(app=self)

Expand All @@ -49,9 +49,6 @@ def compose(self) -> ComposeResult:
async def on_mount(self) -> None:
asyncio.create_task(self.agent_loop.start())

async def on_message_posted(self, event: MessagePosted) -> None:
await self.message_bus.on_message_posted(event)

async def action_interrupt(self) -> None:
await self.actions.interrupt()

Expand Down
7 changes: 0 additions & 7 deletions src/agent_chat_cli/components/chat_history.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
from textual.containers import Container
from textual.message import Message as TextualMessage

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

return tool_widget


class MessagePosted(TextualMessage):
def __init__(self, message: Message) -> None:
self.message = message
super().__init__()
30 changes: 23 additions & 7 deletions src/agent_chat_cli/core/actions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import TYPE_CHECKING

from agent_chat_cli.utils.enums import ControlCommand
from agent_chat_cli.components.chat_history import ChatHistory, MessagePosted
from agent_chat_cli.components.messages import Message
from agent_chat_cli.components.chat_history import ChatHistory
from agent_chat_cli.components.messages import Message, MessageType
from agent_chat_cli.components.tool_permission_prompt import ToolPermissionPrompt
from agent_chat_cli.utils.logger import log_json

Expand All @@ -17,16 +17,32 @@ def __init__(self, app: "AgentChatCLIApp") -> None:
def quit(self) -> None:
self.app.exit()

async def add_message_to_chat(self, type: MessageType, content: str) -> None:
match type:
case MessageType.USER:
message = Message.user(content)
case MessageType.SYSTEM:
message = Message.system(content)
case MessageType.AGENT:
message = Message.agent(content)
case _:
raise ValueError(f"Unsupported message type: {type}")

chat_history = self.app.query_one(ChatHistory)
chat_history.add_message(message)

async def submit_user_message(self, message: str) -> None:
self.app.post_message(MessagePosted(Message.user(message)))
chat_history = self.app.query_one(ChatHistory)
chat_history.add_message(Message.user(message))
self.app.ui_state.start_thinking()
await self.app.ui_state.scroll_to_bottom()
await self._query(message)

def post_system_message(self, message: str) -> None:
self.app.post_message(MessagePosted(Message.system(message)))
async def post_system_message(self, message: str) -> None:
await self.add_message_to_chat(MessageType.SYSTEM, message)

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

async def interrupt(self) -> None:
permission_prompt = self.app.query_one(ToolPermissionPrompt)
Expand Down
10 changes: 5 additions & 5 deletions src/agent_chat_cli/core/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async def start(self) -> None:

await self._handle_message(message)

await self.app.actions.handle_agent_message(
await self.app.actions.render_message(
AgentMessage(type=AgentMessageType.RESULT, data=None)
)

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

if text_chunk:
await self.app.actions.handle_agent_message(
await self.app.actions.render_message(
AgentMessage(
type=AgentMessageType.STREAM_EVENT,
data={"text": text_chunk},
Expand Down Expand Up @@ -163,7 +163,7 @@ async def _handle_message(self, message: Message) -> None:
)

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

# Handle permission request queue sequentially
async with self.permission_lock:
await self.app.actions.handle_agent_message(
await self.app.actions.render_message(
AgentMessage(
type=AgentMessageType.TOOL_PERMISSION_REQUEST,
data={
Expand Down Expand Up @@ -213,7 +213,7 @@ async def _can_use_tool(
)

if rejected_tool:
self.app.actions.post_system_message(
await self.app.actions.post_system_message(
f"Permission denied for {tool_name}"
)

Expand Down
149 changes: 0 additions & 149 deletions src/agent_chat_cli/core/message_bus.py

This file was deleted.

Loading