Agent Chat CLI is a Python TUI application built with Textual that provides an interactive chat interface for Claude AI with MCP (Model Context Protocol) server support.
Main Textual application that initializes and coordinates all components.
Textual widgets responsible for UI rendering:
- ChatHistory: Container that displays message widgets
- Message widgets: SystemMessage, UserMessage, AgentMessage, ToolMessage
- UserInput: Handles user text input and submission
- ThinkingIndicator: Shows when agent is processing
Manages the conversation loop with Claude SDK:
- Maintains async queue for user queries
- Handles streaming responses
- Parses SDK messages into structured AgentMessage objects
- Emits AgentMessageType events (STREAM_EVENT, ASSISTANT, RESULT)
- Manages session persistence via session_id
Routes agent messages to appropriate UI components:
- Handles streaming text updates
- Mounts tool use messages
- Controls thinking indicator state
- Manages scroll-to-bottom behavior
Centralizes all user-initiated actions and controls:
- quit(): Exits the application
- query(user_input): Sends user query to agent loop queue
- interrupt(): Stops streaming mid-execution by setting interrupt flag and calling SDK interrupt
- new(): Starts new conversation by sending NEW_CONVERSATION control command
- Manages UI state (thinking indicator, chat history clearing)
- Directly accesses agent_loop internals (query_queue, client, interrupting flag)
Actions are triggered via:
- Keybindings in app.py (ESC → action_interrupt, Ctrl+N → action_new)
- Text commands in user_input.py ("exit", "clear")
Loads and validates YAML configuration:
- Filters disabled MCP servers
- Loads prompts from files
- Expands environment variables
- Combines system prompt with MCP server prompts
User Input
↓
UserInput.on_input_submitted
↓
MessagePosted event → ChatHistory (immediate UI update)
↓
Actions.query(user_input) → AgentLoop.query_queue.put()
↓
Claude SDK (streaming response)
↓
AgentLoop._handle_message
↓
AgentMessage (typed message) → MessageBus.handle_agent_message
↓
Match on AgentMessageType:
- STREAM_EVENT → Update streaming message widget
- ASSISTANT → Mount tool use widgets
- RESULT → Reset thinking indicator
User Action (ESC, Ctrl+N, "clear", "exit")
↓
App.action_* (keybinding) OR UserInput (text command)
↓
Actions.interrupt() OR Actions.new() OR Actions.quit()
↓
AgentLoop internals:
- interrupt: Set interrupting flag + SDK interrupt
- new: Put ControlCommand.NEW_CONVERSATION on queue
- quit: App.exit()
AgentMessageType: Agent communication events
- ASSISTANT: Assistant message with content blocks
- STREAM_EVENT: Streaming text chunk
- RESULT: Response complete
- INIT, SYSTEM: Initialization and system events
ContentType: Content block types
- TEXT: Text content
- TOOL_USE: Tool call
- CONTENT_BLOCK_DELTA: SDK streaming event type
- TEXT_DELTA: SDK text delta type
ControlCommand: Control commands for agent loop
- NEW_CONVERSATION: Disconnect and reconnect SDK to start fresh session
- EXIT: User command to quit application
- CLEAR: User command to start new conversation
MessageType (components/messages.py): UI message types
- SYSTEM, USER, AGENT, TOOL
AgentMessage (utils/agent_loop.py): Structured message from agent loop
@dataclass
class AgentMessage:
type: AgentMessageType
data: AnyMessage (components/messages.py): UI message data
@dataclass
class Message:
type: MessageType
content: str
metadata: dict[str, Any] | None = NoneConfiguration is loaded from agent-chat-cli.config.yaml:
- system_prompt: Base system prompt (supports file paths)
- model: Claude model to use
- include_partial_messages: Enable streaming
- mcp_servers: MCP server configurations (filtered by enabled flag)
- agents: Named agent configurations
- disallowed_tools: Tool filtering
- permission_mode: Permission handling mode
MCP server prompts are automatically appended to the system prompt.
- exit: Quits the application
- clear: Starts a new conversation (clears history and reconnects)
- Ctrl+C: Quit application
- ESC: Interrupt streaming response
- Ctrl+N: Start new conversation
The agent loop supports session persistence and resumption via session_id:
AgentLoop.__init__accepts an optionalsession_idparameter- If provided, the session_id is passed to Claude SDK via the
resumeconfig option - This allows resuming a previous conversation with full context
- During SDK initialization, a SystemMessage with subtype "init" is received
- The message contains a
session_idin its data payload - AgentLoop extracts and stores this session_id:
agent_loop.py:65 - The session_id can be persisted and used to resume the session later
AgentLoop(session_id="abc123")
↓
config_dict["resume"] = session_id
↓
ClaudeSDKClient initialized with resume option
↓
SDK reconnects to previous session with full history
- User submits text → UserInput
- MessagePosted event → App
- App → MessageBus.on_message_posted
- MessageBus → ChatHistory.add_message
- MessageBus → Scroll to bottom
- AgentLoop receives SDK message
- Parse into AgentMessage with AgentMessageType
- MessageBus.handle_agent_message (match/case on type)
- Update UI components based on type
- Scroll to bottom
- Two distinct MessageType enums exist for different purposes (UI vs Agent events)
- Message bus manages stateful streaming (tracks current_agent_message)
- Config loading combines multiple prompts into final system_prompt
- Tool names follow format:
mcp__servername__toolname - Actions class provides single interface for all user-initiated operations
- Control commands are queued alongside user queries to ensure proper task ordering
- Agent loop processes both strings (user queries) and ControlCommands from the same queue
- Interrupt flag is checked on each streaming message to enable immediate stop