|
| 1 | +# Architecture |
| 2 | + |
| 3 | +Agent Chat CLI is a terminal-based chat interface for interacting with Claude agents, built with [Textual](https://textual.textualize.io/) and the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk). |
| 4 | + |
| 5 | +### Directory Structure |
| 6 | + |
| 7 | +``` |
| 8 | +src/agent_chat_cli/ |
| 9 | +├── app.py # Main Textual application entry point |
| 10 | +├── core/ |
| 11 | +│ ├── actions.py # User action handlers |
| 12 | +│ ├── agent_loop.py # Claude Agent SDK client wrapper |
| 13 | +│ ├── message_bus.py # Message routing from agent to UI |
| 14 | +│ ├── ui_state.py # Centralized UI state management |
| 15 | +│ └── styles.tcss # Textual CSS styles |
| 16 | +├── components/ |
| 17 | +│ ├── balloon_spinner.py # Animated spinner widget |
| 18 | +│ ├── caret.py # Input caret indicator |
| 19 | +│ ├── chat_history.py # Chat message container |
| 20 | +│ ├── flex.py # Horizontal flex container |
| 21 | +│ ├── header.py # App header with MCP server status |
| 22 | +│ ├── messages.py # Message data models and widgets |
| 23 | +│ ├── spacer.py # Empty spacer widget |
| 24 | +│ ├── thinking_indicator.py # "Agent is thinking" indicator |
| 25 | +│ ├── tool_permission_prompt.py # Tool permission request UI |
| 26 | +│ └── user_input.py # User text input widget |
| 27 | +└── utils/ |
| 28 | + ├── config.py # YAML config loading |
| 29 | + ├── enums.py # Shared enumerations |
| 30 | + ├── format_tool_input.py # Tool input formatting |
| 31 | + ├── logger.py # Logging setup |
| 32 | + ├── mcp_server_status.py # MCP server connection state |
| 33 | + ├── system_prompt.py # System prompt builder |
| 34 | + └── tool_info.py # Tool name parsing |
| 35 | +``` |
| 36 | + |
| 37 | +### Core Architecture |
| 38 | + |
| 39 | +The application follows a loosely coupled architecture with four main orchestration objects: |
| 40 | + |
| 41 | +``` |
| 42 | +┌─────────────────────────────────────────────────────────────┐ |
| 43 | +│ AgentChatCLIApp │ |
| 44 | +│ ┌───────────┐ ┌───────────┐ ┌─────────┐ ┌───────────┐ │ |
| 45 | +│ │ UIState │ │MessageBus │ │ Actions │ │ AgentLoop │ │ |
| 46 | +│ └─────┬─────┘ └─────┬─────┘ └────┬────┘ └─────┬─────┘ │ |
| 47 | +│ │ │ │ │ │ |
| 48 | +│ └──────────────┴─────────────┴──────────────┘ │ |
| 49 | +│ │ │ |
| 50 | +│ ┌─────────────────────────┴─────────────────────────────┐ │ |
| 51 | +│ │ Components │ │ |
| 52 | +│ │ Header │ ChatHistory │ ThinkingIndicator │ UserInput │ │ |
| 53 | +│ └────────────────────────────────────────────────────────┘ │ |
| 54 | +└─────────────────────────────────────────────────────────────┘ |
| 55 | +``` |
| 56 | + |
| 57 | +### Core Modules |
| 58 | + |
| 59 | +**UIState** (`core/ui_state.py`) |
| 60 | +Centralized management of UI state behaviors. Handles: |
| 61 | +- Thinking indicator visibility and cursor blink state |
| 62 | +- Tool permission prompt display/hide |
| 63 | +- Interrupt state tracking |
| 64 | + |
| 65 | +This class was introduced in PR #9 to consolidate scattered UI state logic from Actions and MessageBus into a single cohesive module. |
| 66 | + |
| 67 | +**MessageBus** (`core/message_bus.py`) |
| 68 | +Routes messages from the AgentLoop to appropriate UI components: |
| 69 | +- `STREAM_EVENT`: Streaming text chunks to AgentMessage widgets |
| 70 | +- `ASSISTANT`: Complete assistant responses with tool use blocks |
| 71 | +- `SYSTEM` / `USER`: System and user messages |
| 72 | +- `TOOL_PERMISSION_REQUEST`: Triggers permission prompt UI |
| 73 | +- `RESULT`: Signals completion, resets state |
| 74 | + |
| 75 | +**Actions** (`core/actions.py`) |
| 76 | +User-initiated action handlers: |
| 77 | +- `submit_user_message()`: Posts user message and queries agent |
| 78 | +- `interrupt()`: Cancels current agent operation |
| 79 | +- `new()`: Starts new conversation, clears history |
| 80 | +- `respond_to_tool_permission()`: Handles permission prompt responses |
| 81 | + |
| 82 | +**AgentLoop** (`core/agent_loop.py`) |
| 83 | +Manages the Claude Agent SDK client lifecycle: |
| 84 | +- Initializes `ClaudeSDKClient` with config and MCP servers |
| 85 | +- Processes incoming messages via async generator |
| 86 | +- Handles tool permission flow via `_can_use_tool()` callback |
| 87 | +- Manages `query_queue` and `permission_response_queue` for async communication |
| 88 | + |
| 89 | +### Message Flow |
| 90 | + |
| 91 | +1. User types in `UserInput` and presses Enter |
| 92 | +2. `Actions.submit_user_message()` posts to UI and enqueues to `AgentLoop.query_queue` |
| 93 | +3. `AgentLoop` sends query to Claude Agent SDK and streams responses |
| 94 | +4. Responses flow through `MessageBus.handle_agent_message()` to update UI |
| 95 | +5. Tool use triggers permission prompt via `UIState.show_permission_prompt()` |
| 96 | +6. User response flows back through `Actions.respond_to_tool_permission()` |
| 97 | + |
| 98 | +### Components |
| 99 | + |
| 100 | +**UserInput** (`components/user_input.py`) |
| 101 | +Text input with: |
| 102 | +- Enter to submit |
| 103 | +- Ctrl+J for newlines (PR #10) |
| 104 | +- Control commands: `exit`, `clear` |
| 105 | + |
| 106 | +**ToolPermissionPrompt** (`components/tool_permission_prompt.py`) |
| 107 | +Modal prompt for tool permission requests: |
| 108 | +- Shows tool name and MCP server |
| 109 | +- Enter to allow, ESC to deny, or type custom response |
| 110 | +- Manages focus to prevent input elsewhere while visible |
| 111 | + |
| 112 | +**ChatHistory** (`components/chat_history.py`) |
| 113 | +Container for message widgets, handles `MessagePosted` events. |
| 114 | + |
| 115 | +**ThinkingIndicator** (`components/thinking_indicator.py`) |
| 116 | +Animated indicator shown during agent processing. |
| 117 | + |
| 118 | +**Header** (`components/header.py`) |
| 119 | +Displays available MCP servers with connection status via `MCPServerStatus` subscription. |
| 120 | + |
| 121 | +### Configuration |
| 122 | + |
| 123 | +Configuration is loaded from `agent-chat-cli.config.yaml`: |
| 124 | + |
| 125 | +```yaml |
| 126 | +system_prompt: "prompt.md" # File path or literal string |
| 127 | +model: "claude-sonnet-4-20250514" |
| 128 | +permission_mode: "bypass_permissions" |
| 129 | + |
| 130 | +mcp_servers: |
| 131 | + server_name: |
| 132 | + description: "Server description" |
| 133 | + command: "npx" |
| 134 | + args: ["-y", "@some/mcp-server"] |
| 135 | + env: |
| 136 | + API_KEY: "$API_KEY" |
| 137 | + enabled: true |
| 138 | + prompt: "server_prompt.md" |
| 139 | + |
| 140 | +agents: |
| 141 | + agent_name: |
| 142 | + description: "Agent description" |
| 143 | + prompt: "agent_prompt.md" |
| 144 | + tools: ["tool1", "tool2"] |
| 145 | +``` |
| 146 | +
|
| 147 | +### Key Patterns |
| 148 | +
|
| 149 | +**Reactive Properties**: Textual's `reactive` and `var` are used for automatic UI updates when state changes (e.g., `ThinkingIndicator.is_thinking`, `ToolPermissionPrompt.is_visible`). |
| 150 | + |
| 151 | +**Async Queues**: Communication between UI and AgentLoop uses `asyncio.Queue` for decoupled async message passing. |
| 152 | + |
| 153 | +**Observer Pattern**: `MCPServerStatus` uses callback subscriptions to notify components of connection state changes. |
| 154 | + |
| 155 | +**TYPE_CHECKING Guards**: Circular import prevention via `if TYPE_CHECKING:` blocks for type hints. |
| 156 | + |
| 157 | +### Testing |
| 158 | + |
| 159 | +Tests use pytest with pytest-asyncio for async support and Textual's pilot testing framework for UI interactions. |
| 160 | + |
| 161 | +```bash |
| 162 | +make test |
| 163 | +``` |
0 commit comments